Coverage for python/diagnostic_and_simulation_base/nested_dictionary.py: 24%
102 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 13:12 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 13:12 +0000
1# mypy: ignore-errors
2# TODO: need to fix mypy errors
4import typing
5from collections import OrderedDict
7import numpy as np
10class ListOfNestedDict:
11 def __init__(self, nested_dict_data: "NestedDict") -> None:
12 """This passes in the entire dictionary"""
13 self.nested_dict_data = nested_dict_data
15 def __getitem__(self, key_in: str) -> typing.Any:
16 # Store the keys, until we get to the "data level"
18 # print(key_in)
19 if not hasattr(self, "keys"):
20 # initialising
21 self.keys = [key_in]
22 else:
23 # Store key
24 self.keys.append(key_in)
25 # print('starting __getitem__')
26 # print(self.keys)
27 # print(' ')
29 accumulated_results = [self.nested_dict_data[wild_card_key] for wild_card_key in self.nested_dict_data]
30 # print('accumulated results')
31 result_final_level = accumulated_results[0]
32 for key_now in self.keys:
33 result_final_level = result_final_level[key_now]
34 # print('got keys')
36 # print(type(result_final_level))
37 if isinstance(result_final_level, NestedDict):
38 return self
39 elif isinstance(result_final_level, ListOfNestedDict):
40 # keep going recursively, storing "self.keys" until we get to the data
41 return self
42 else:
43 result_to_return = []
44 for accumulated_result in accumulated_results:
45 result_final_level = accumulated_result
46 for key_now in self.keys:
47 result_final_level = result_final_level[key_now]
48 result_to_return.append(result_final_level)
50 result_to_return_np = np.array(result_to_return)
51 # move the 0th axis to the end - this gives the desired shape
52 new_axes = tuple(range(1, result_to_return_np.ndim)) + (0,)
53 result_to_return_np = np.transpose(result_to_return_np, axes=new_axes)
54 return result_to_return_np
56 # def __len__(self):
57 # return len(self.nested_dict_data.keys()) # this probably won't work???
60class NestedDict(OrderedDict):
61 """A custom nested dictionary
63 Wild card accumulator: In MDSplus, and all of our code, by convention time is the 0th index.
64 If there is vecor_data inside a node, then it is highly likely that it is a time-dependent quantity
66 Example, showing the array ordering:
67 ```python
68 from diagnostics_analysis_base import NestedDict
69 import numpy as np
71 x = NestedDict()
73 x["top_level_1"]["middle_level_1"]["bottom_level_1"] = np.random.rand(6, 9)
74 x["top_level_1"]["middle_level_1"]["bottom_level_2"] = np.random.rand(6, 9)
75 x["top_level_1"]["middle_level_2"]["bottom_level_1"] = np.random.rand(6, 9)
76 x["top_level_1"]["middle_level_2"]["bottom_level_2"] = np.random.rand(6, 9)
77 x["top_level_1"]["middle_level_3"]["bottom_level_1"] = np.random.rand(6, 9)
78 x["top_level_1"]["middle_level_3"]["bottom_level_2"] = np.random.rand(6, 9)
79 x["top_level_2"]["middle_level_1"]["bottom_level_1"] = np.random.rand(6, 9)
80 x["top_level_2"]["middle_level_1"]["bottom_level_2"] = np.random.rand(6, 9)
81 x["top_level_2"]["middle_level_2"]["bottom_level_1"] = np.random.rand(6, 9)
82 x["top_level_2"]["middle_level_2"]["bottom_level_2"] = np.random.rand(6, 9)
83 x["top_level_2"]["middle_level_3"]["bottom_level_1"] = np.random.rand(6, 9)
84 x["top_level_2"]["middle_level_3"]["bottom_level_2"] = np.random.rand(6, 9)
85 x["top_level_3"]["middle_level_1"]["bottom_level_1"] = np.random.rand(6, 9)
86 x["top_level_3"]["middle_level_1"]["bottom_level_2"] = np.random.rand(6, 9)
87 x["top_level_3"]["middle_level_2"]["bottom_level_1"] = np.random.rand(6, 9)
88 x["top_level_3"]["middle_level_2"]["bottom_level_2"] = np.random.rand(6, 9)
89 x["top_level_3"]["middle_level_3"]["bottom_level_1"] = np.random.rand(6, 9)
90 x["top_level_3"]["middle_level_3"]["bottom_level_2"] = np.random.rand(6, 9)
91 x["top_level_4"]["middle_level_1"]["bottom_level_1"] = np.random.rand(6, 9)
92 x["top_level_4"]["middle_level_1"]["bottom_level_2"] = np.random.rand(6, 9)
93 x["top_level_4"]["middle_level_2"]["bottom_level_1"] = np.random.rand(6, 9)
94 x["top_level_4"]["middle_level_2"]["bottom_level_2"] = np.random.rand(6, 9)
95 x["top_level_4"]["middle_level_3"]["bottom_level_1"] = np.random.rand(6, 9)
96 x["top_level_4"]["middle_level_3"]["bottom_level_2"] = np.random.rand(6, 9)
98 y = x["*"]["*"]["*"]
99 assert y.shape == (6, 9, 2, 3, 4)
100 ```
101 """
103 def __missing__(self, key: str) -> "NestedDict":
104 """Add to the missing dict"""
105 value = self[key] = type(self)()
106 return value
108 def to_dictionary(self) -> dict[str, typing.Any]:
109 """Convert NestedDict to a standard python `dict`"""
110 result = {}
111 for key, value in self.items():
112 if isinstance(value, NestedDict):
113 result[key] = value.to_dictionary()
114 else:
115 result[key] = value
116 return result
118 def __setitem__(
119 self,
120 key: str,
121 value: typing.Any,
122 ) -> None:
123 """Override __setitem__ to ensure all dictionaries are converted to NestedDict."""
124 value = _convert_to_nested_dict(value)
125 # TODO: figure out why this fails mypy tests?
126 OrderedDict.__setitem__(self, key, value)
128 def update(
129 self,
130 dict_to_add: dict[typing.Any, typing.Any] | None = None,
131 ) -> None:
132 """Recursively updates this dictionary with another dictionary,
133 ensuring that nested dictionaries are converted to NestedDict."""
135 if dict_to_add is None:
136 dict_to_add = {}
138 for key, value in dict_to_add.items():
139 if isinstance(value, dict):
140 # If the key exists and is already a NestedDict, update recursively
141 if key in self and isinstance(self[key], NestedDict):
142 self[key].update(value)
143 else:
144 # Otherwise, convert the value to NestedDict and assign it
145 self[key] = NestedDict(value)
146 else:
147 # Set the value directly if it's not a dictionary
148 self[key] = value
150 # def update(
151 # self,
152 # dict_to_add: dict | None = None,
153 # ) -> None:
154 # """Recursively updates this dictionary with another dictionary or kwargs,
155 # ensuring that nested dictionaries are converted to NestedDict."""
157 # if dict_to_add is None:
158 # dict_to_add = {}
160 # nested_dict_to_add = _convert_to_nested_dict(dict_to_add)
162 # for key, value in dict_to_add.items():
163 # if isinstance(value, dict):
164 # # Recursively update or set as NestedDict
165 # self[key] = _convert_to_nested_dict(value)
166 # else:
167 # self[key] = value
169 def print_data(self, indent: int = 0) -> str:
170 """Recursively generates JSON-like string with proper formatting."""
171 spacing = " " * indent # 2 spaces per level of indentation
172 items = []
173 for key, value in self.items():
174 if isinstance(value, NestedDict):
175 # Recursively generate nested dicts
176 items.append(f'{spacing} "{key}": {{')
177 items.append(value.print_data(indent + 1)) # Recursive call for nested dict
178 items.append(f"{spacing} }}")
179 else:
180 # Handle other types, using repr() for proper formatting
181 items.append(f'{spacing} "{key}": {repr(value)}')
183 return "\n".join(items) # Return the joined string without leading/trailing newlines
185 def __str__(self) -> str:
186 """Override __str__ to use print_data for JSON-like string representation."""
187 return "{\n" + self.print_data() + "\n}"
189 def __getitem__(self, key: str) -> typing.Any:
190 # wild card
191 if key == "*":
192 # use wild_card and retrieve the data
193 accumulated_results = [self[wild_card_key] for wild_card_key in self]
194 # test if data is at this level or not
195 if isinstance(accumulated_results[0], NestedDict):
196 return ListOfNestedDict(self)
197 else:
198 # Stack the arrays along a new dimension (axis=0)
199 stacked_results = np.stack(accumulated_results, axis=-1) # Stack along the last axis
200 return stacked_results
202 # Normal behaviour
203 # TODO: this fails `ty` type checking
204 return OrderedDict.__getitem__(self, key)
206 # TODO: need to fix mypy tests
207 def print_keys(self, d: None = None, path: list[str] | None = None) -> None:
208 if d is None:
209 # TODO: this fails `ty` type checking. I think this is because we are re-defining `d` from type None to type NestedDict
210 d = self # Use the current instance as the dictionary
212 if path is None:
213 path = []
215 # Check if keys_long is already initialized in self
216 if not hasattr(self, "keys_long"):
217 self.keys_long = []
218 self.data_type_long = []
220 # TODO: this fails `ty` type checking
221 for key, value in d.items():
222 new_path = path + [f'["{key}"]']
224 if isinstance(value, dict):
225 # Recursively handle nested dictionaries
226 # print("BUXTON: error")
227 self.print_keys(value, new_path)
228 else:
229 # Prepare the type information
230 if isinstance(value, np.ndarray):
231 type_info = f" = np.ndarray; shape={value.shape}"
232 elif isinstance(value, list):
233 type_info = f" = list; length={len(value)}"
234 else:
235 type_info = f" = {type(value).__name__}"
237 # Print the path with the type info
238 self.keys_long.append("".join(new_path)) # Store key path
239 self.data_type_long.append(type_info) # Store type info
241 if path == []: # Check if we are at the top level
242 # Find the length of the longest key path
243 max_key_length = max(len(key) for key in self.keys_long)
245 # Print with proper alignment
246 for key, type_info in zip(self.keys_long, self.data_type_long):
247 # Calculate required spaces for alignment
248 spaces = " " * (max_key_length - len(key) + 4) # 4 spaces for padding
249 print(f"{key}{spaces}{type_info}")
252# BUXTON: is "typing.Any" only option??
253def _convert_to_nested_dict(value: typing.Any) -> typing.Any:
254 """Helper function to recursively convert all dictionaries to NestedDict."""
255 if isinstance(value, OrderedDict) and not isinstance(value, NestedDict):
256 # Create a new NestedDict and populate it using a for loop
257 nested = NestedDict()
258 for k, v in value.items():
259 nested[k] = _convert_to_nested_dict(v) # Recursively convert
260 return nested
261 return value