imas-python 0.1.dev1618__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of imas-python might be problematic. Click here for more details.

Files changed (108) hide show
  1. imas/__init__.py +34 -0
  2. imas/__main__.py +17 -0
  3. imas/_util.py +260 -0
  4. imas/_version.py +16 -0
  5. imas/assets/IDSDef2MDSpreTree.xsl +161 -0
  6. imas/assets/IDS_fake_toplevel.xml +318 -0
  7. imas/assets/IDS_minimal.xml +37 -0
  8. imas/assets/IDS_minimal_2.xml +37 -0
  9. imas/assets/IDS_minimal_struct_array.xml +43 -0
  10. imas/assets/IDS_minimal_types.xml +169 -0
  11. imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
  12. imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
  13. imas/assets/README.md +5 -0
  14. imas/assets/core_profiles.ids +2438 -0
  15. imas/assets/equilibrium.ids +3172 -0
  16. imas/backends/__init__.py +12 -0
  17. imas/backends/db_entry_impl.py +122 -0
  18. imas/backends/imas_core/__init__.py +4 -0
  19. imas/backends/imas_core/al_context.py +391 -0
  20. imas/backends/imas_core/db_entry_al.py +405 -0
  21. imas/backends/imas_core/db_entry_helpers.py +200 -0
  22. imas/backends/imas_core/imas_interface.py +216 -0
  23. imas/backends/imas_core/mdsplus_model.py +367 -0
  24. imas/backends/imas_core/uda_support.py +56 -0
  25. imas/backends/netcdf/__init__.py +4 -0
  26. imas/backends/netcdf/db_entry_nc.py +176 -0
  27. imas/backends/netcdf/ids2nc.py +306 -0
  28. imas/backends/netcdf/nc2ids.py +302 -0
  29. imas/backends/netcdf/nc_metadata.py +357 -0
  30. imas/backends/netcdf/nc_validate.py +53 -0
  31. imas/command/cli.py +239 -0
  32. imas/command/db_analysis.py +290 -0
  33. imas/command/helpers.py +45 -0
  34. imas/command/timer.py +77 -0
  35. imas/db_entry.py +802 -0
  36. imas/dd_helpers.py +300 -0
  37. imas/dd_zip.py +297 -0
  38. imas/exception.py +107 -0
  39. imas/ids_base.py +149 -0
  40. imas/ids_convert.py +920 -0
  41. imas/ids_coordinates.py +400 -0
  42. imas/ids_data_type.py +115 -0
  43. imas/ids_defs.py +171 -0
  44. imas/ids_factory.py +108 -0
  45. imas/ids_identifiers.py +125 -0
  46. imas/ids_metadata.py +307 -0
  47. imas/ids_path.py +254 -0
  48. imas/ids_primitive.py +510 -0
  49. imas/ids_struct_array.py +233 -0
  50. imas/ids_structure.py +331 -0
  51. imas/ids_toplevel.py +381 -0
  52. imas/setup_logging.py +64 -0
  53. imas/test/test_all_dd_versions.py +28 -0
  54. imas/test/test_cli.py +44 -0
  55. imas/test/test_dbentry.py +104 -0
  56. imas/test/test_dd_helpers.py +134 -0
  57. imas/test/test_dd_zip.py +29 -0
  58. imas/test/test_deepcopy.py +25 -0
  59. imas/test/test_exception.py +12 -0
  60. imas/test/test_get_sample.py +388 -0
  61. imas/test/test_hash.py +188 -0
  62. imas/test/test_helpers.py +354 -0
  63. imas/test/test_identifiers.py +105 -0
  64. imas/test/test_ids2nc.py +166 -0
  65. imas/test/test_ids_ascii_data.py +39 -0
  66. imas/test/test_ids_convert.py +415 -0
  67. imas/test/test_ids_coordinates.py +165 -0
  68. imas/test/test_ids_data_type.py +40 -0
  69. imas/test/test_ids_factory.py +35 -0
  70. imas/test/test_ids_metadata.py +66 -0
  71. imas/test/test_ids_mixin.py +20 -0
  72. imas/test/test_ids_path.py +192 -0
  73. imas/test/test_ids_primitive.py +109 -0
  74. imas/test/test_ids_struct_array.py +89 -0
  75. imas/test/test_ids_structure.py +70 -0
  76. imas/test/test_ids_toplevel.py +55 -0
  77. imas/test/test_ids_validate.py +343 -0
  78. imas/test/test_latest_dd_autofill.py +74 -0
  79. imas/test/test_latest_dd_manual.py +38 -0
  80. imas/test/test_latest_dd_resample.py +102 -0
  81. imas/test/test_lazy_loading.py +201 -0
  82. imas/test/test_list_occurrences.py +82 -0
  83. imas/test/test_minimal.py +37 -0
  84. imas/test/test_minimal_conversion.py +44 -0
  85. imas/test/test_minimal_io.py +21 -0
  86. imas/test/test_minimal_struct_array_io.py +60 -0
  87. imas/test/test_minimal_types.py +436 -0
  88. imas/test/test_minimal_types_io.py +70 -0
  89. imas/test/test_minimal_types_io_automatic.py +66 -0
  90. imas/test/test_nbc_change.py +408 -0
  91. imas/test/test_nc_autofill.py +13 -0
  92. imas/test/test_nc_entry.py +28 -0
  93. imas/test/test_nc_metadata.py +153 -0
  94. imas/test/test_nc_validation.py +197 -0
  95. imas/test/test_snippets.py +25 -0
  96. imas/test/test_static_ids.py +39 -0
  97. imas/test/test_str_1d.py +99 -0
  98. imas/test/test_time_slicing.py +182 -0
  99. imas/test/test_uda_support.py +12 -0
  100. imas/test/test_util.py +209 -0
  101. imas/training.py +45 -0
  102. imas/util.py +526 -0
  103. imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
  104. imas_python-0.1.dev1618.dist-info/METADATA +288 -0
  105. imas_python-0.1.dev1618.dist-info/RECORD +108 -0
  106. imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
  107. imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
  108. imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
imas/ids_factory.py ADDED
@@ -0,0 +1,108 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ """Tools for generating IDSs from a Data Dictionary version.
4
+ """
5
+
6
+ import logging
7
+ from functools import partial
8
+ from typing import Any, Iterable, Iterator, List, Optional
9
+
10
+ from imas import dd_zip
11
+ from imas.exception import IDSNameError
12
+ from imas.ids_toplevel import IDSToplevel
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IDSFactory:
18
+ """Factory class generating IDSToplevel elements for specific DD versions.
19
+
20
+ Example:
21
+
22
+ >>> factory = IDSFactory()
23
+ >>> factory.core_profiles()
24
+ <imas.ids_toplevel.IDSToplevel object at 0x7f6afa03cdf0>
25
+ >>> factory.new("core_profiles")
26
+ <imas.ids_toplevel.IDSToplevel object at 0x7f6afa03ccd0>
27
+ """
28
+
29
+ def __init__(
30
+ self, version: Optional[str] = None, xml_path: Optional[str] = None
31
+ ) -> None:
32
+ """Create a new IDS Factory
33
+
34
+ See :meth:`imas.dd_zip.dd_etree` for further details on the ``version`` and
35
+ ``xml_path`` arguments.
36
+
37
+ Args:
38
+ version: DD version string, e.g. "3.38.1".
39
+ xml_path: XML file containing data dictionary definition.
40
+ """
41
+ self._xml_path = xml_path
42
+ self._etree = dd_zip.dd_etree(version, xml_path)
43
+ self._ids_elements = {
44
+ ele.get("name"): ele for ele in self._etree.findall("IDS")
45
+ }
46
+
47
+ version_element = self._etree.find("version")
48
+ if version_element is not None:
49
+ self._version = version_element.text
50
+ elif version:
51
+ self._version = version
52
+ else:
53
+ logger.warning("Ignoring missing Data Dictionary version in loaded DD.")
54
+ self._version = "-1"
55
+ if version and version != self._version:
56
+ raise RuntimeError(
57
+ f"There is a mismatch between the requested DD version {version} and "
58
+ f"the actual loaded DD version {self._version}."
59
+ )
60
+
61
+ def __copy__(self) -> "IDSFactory":
62
+ return self
63
+
64
+ def __deepcopy__(self, memo) -> "IDSFactory":
65
+ return self
66
+
67
+ def __dir__(self) -> Iterable[str]:
68
+ return sorted(set(object.__dir__(self)).union(self._ids_elements))
69
+
70
+ def __getattr__(self, name: str) -> Any:
71
+ if name in self._ids_elements:
72
+ # Note: returning a partial to mimic AL HLI, e.g. factory.core_profiles()
73
+ return partial(IDSToplevel, self, self._ids_elements[name])
74
+ raise AttributeError(f"{type(self)!r} object has no attribute {name!r}")
75
+
76
+ def __iter__(self) -> Iterator[str]:
77
+ """Iterate over the IDS names defined by the loaded Data Dictionary"""
78
+ return iter(self._ids_elements)
79
+
80
+ def ids_names(self) -> List[str]:
81
+ """Get a list of all known IDS names in the loaded Data Dictionary"""
82
+ return list(self._ids_elements)
83
+
84
+ def new(self, ids_name: str, *, _lazy: bool = False) -> IDSToplevel:
85
+ """Create a new IDSToplevel element for the provided IDS name
86
+
87
+ Args:
88
+ ids_name: Name of the IDS toplevel to create, e.g. "core_profiles".
89
+
90
+ Keyword args:
91
+ _lazy: Internal usage only! Create an IDS Toplevel suitable for lazy loading
92
+ when set to True.
93
+ """
94
+ if ids_name not in self._ids_elements:
95
+ raise IDSNameError(ids_name, self)
96
+ return IDSToplevel(self, self._ids_elements[ids_name], _lazy)
97
+
98
+ def exists(self, ids_name: str) -> bool:
99
+ """Check if an IDS type with the given name exists."""
100
+ return ids_name in self._ids_elements
101
+
102
+ @property
103
+ def version(self) -> str:
104
+ """Get the DD version used by this IDS factory"""
105
+ return self._version
106
+
107
+ # dd_version is an alias for version
108
+ dd_version = version
@@ -0,0 +1,125 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ """imas-python module to support Data Dictionary identifiers.
4
+ """
5
+
6
+ import logging
7
+ from enum import Enum
8
+ from typing import Iterable, List, Type
9
+ from xml.etree.ElementTree import fromstring
10
+
11
+ from imas import dd_zip
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class IDSIdentifier(Enum):
17
+ """Base class for all identifier enums."""
18
+
19
+ def __new__(self, value: int, description: str):
20
+ obj = object.__new__(self)
21
+ obj._value_ = value
22
+ return obj
23
+
24
+ def __init__(self, value: int, description: str) -> None:
25
+ self.index = value
26
+ """Unique index for this identifier value."""
27
+ self.description = description
28
+ """Description for this identifier value."""
29
+
30
+ def __eq__(self, other):
31
+ if self is other:
32
+ return True
33
+ try:
34
+ other_name = str(other.name)
35
+ other_index = int(other.index)
36
+ other_description = str(other.description)
37
+ except (AttributeError, TypeError, ValueError):
38
+ # Attribute doesn't exist, or failed to convert
39
+ return NotImplemented
40
+ # Index must match
41
+ if other_index == self.index:
42
+ # Name may be left empty
43
+ if other_name == self.name or other_name == "":
44
+ # Description doesn't have to match, though we will warn when it doesn't
45
+ if other_description != self.description and other_description != "":
46
+ logger.warning(
47
+ "Description of %r does not match identifier description %r",
48
+ other.description,
49
+ self.description,
50
+ )
51
+ return True
52
+ else:
53
+ logger.warning(
54
+ "Name %r does not match identifier name %r, but indexes are equal.",
55
+ other.name,
56
+ self.name,
57
+ )
58
+ return False
59
+
60
+ @classmethod
61
+ def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
62
+ element = fromstring(xml)
63
+ enum_values = {}
64
+ for int_element in element.iterfind("int"):
65
+ name = int_element.get("name")
66
+ value = int_element.text
67
+ description = int_element.get("description")
68
+ enum_values[name] = (int(value), description)
69
+ # Create the enumeration
70
+ enum = cls(
71
+ identifier_name,
72
+ enum_values,
73
+ module=__name__,
74
+ qualname=f"{__name__}.{identifier_name}",
75
+ )
76
+ enum.__doc__ = element.find("header").text
77
+ return enum
78
+
79
+
80
+ class _IDSIdentifiers:
81
+ """Support class to list and get identifier objects."""
82
+
83
+ def __getattr__(self, name) -> Type[IDSIdentifier]:
84
+ if name not in self.identifiers:
85
+ raise AttributeError(f"Unknown identifier name: {name}")
86
+ xml = dd_zip.get_identifier_xml(name)
87
+ identifier = IDSIdentifier._from_xml(name, xml)
88
+ setattr(self, name, identifier)
89
+ return identifier
90
+
91
+ def __getitem__(self, name) -> Type[IDSIdentifier]:
92
+ if name not in self.identifiers:
93
+ raise KeyError(f"Unknown identifier name: {name}")
94
+ return getattr(self, name)
95
+
96
+ def __dir__(self) -> Iterable[str]:
97
+ return sorted(set(object.__dir__(self)).union(self.identifiers))
98
+
99
+ @property
100
+ def identifiers(self) -> List[str]:
101
+ return dd_zip.dd_identifiers()
102
+
103
+
104
+ identifiers = _IDSIdentifiers()
105
+ """Object to list and get identifiers.
106
+
107
+ Example:
108
+ .. code-block:: python
109
+
110
+ from imas import identifiers
111
+ # List all identifier names
112
+ for identifier_name in identifiers.identifiers:
113
+ print(identifier_name)
114
+ # Get a specific identifier
115
+ csid = identifiers.core_source_identifier
116
+ # Get and print information of an identifier value
117
+ print(csid.total)
118
+ print(csid.total.index)
119
+ print(csid.total.description)
120
+
121
+ # Item access is also possible
122
+ print(identifiers["edge_source_identifier"])
123
+
124
+ .. seealso:: :ref:`Identifiers`
125
+ """
imas/ids_metadata.py ADDED
@@ -0,0 +1,307 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ """Core of the imas-python interpreted IDS metadata
4
+ """
5
+ import re
6
+ import types
7
+ from enum import Enum
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Iterator, Optional, Tuple, Type
11
+ from xml.etree.ElementTree import Element
12
+
13
+ from imas.ids_coordinates import IDSCoordinate
14
+ from imas.ids_data_type import IDSDataType
15
+ from imas.ids_identifiers import IDSIdentifier, identifiers
16
+ from imas.ids_path import IDSPath
17
+
18
+
19
+ class IDSType(Enum):
20
+ """Data dictionary indication of the time-variation character of a DD node
21
+
22
+ The Data Model distinguishes between categories of data according to their
23
+ time-variation. ``constant`` data are data which are not varying within the context
24
+ of the data being referred to (e.g. pulse, simulation, calculation); ``static`` data
25
+ are likely to be constant over a wider range (e.g. nominal coil positions during
26
+ operation); ``dynamic`` data are those which vary in time within the context of the
27
+ data.
28
+
29
+ As in the Python HLI, imas-python only distinguishes between dynamic and non-dynamic
30
+ nodes.
31
+ """
32
+
33
+ NONE = None
34
+ """The DD node has no type attribute.
35
+ """
36
+
37
+ DYNAMIC = "dynamic"
38
+ """Data that is varying in time.
39
+ """
40
+
41
+ CONSTANT = "constant"
42
+ """Data that does not vary within the IDS.
43
+ """
44
+
45
+ STATIC = "static"
46
+ """Data that does not vary between multiple IDSs.
47
+ """
48
+
49
+ def __init__(self, name):
50
+ self.is_dynamic = name == "dynamic"
51
+
52
+
53
+ # This cache is for IDSMetadata for IDS toplevels
54
+ # Typical use case is one or two DD versions
55
+ # Currently the DD has ~70 unique IDSs, so this cache has plenty of size to store all
56
+ # IDSs of two DD versions.
57
+ #
58
+ # Perhaps the cache could be smaller, but that would be less efficient for the unit
59
+ # tests...
60
+ @lru_cache(maxsize=256)
61
+ def get_toplevel_metadata(structure_xml: Element) -> "IDSMetadata":
62
+ """Build metadata tree of an IDS toplevel element.
63
+
64
+ Args:
65
+ structure_xml: XML element belonging to an IDS toplevel (e.g. core_profiles).
66
+ """
67
+ if not _type_map:
68
+ _build_type_map()
69
+
70
+ # Delete the custom __setattr__ so __init__ can assign values:
71
+ orig_setattr = IDSMetadata.__setattr__
72
+ del IDSMetadata.__setattr__
73
+ try:
74
+ return IDSMetadata(structure_xml, "", None)
75
+ finally:
76
+ # Always restore the custom __setattr__ to avoid accidental data changes
77
+ IDSMetadata.__setattr__ = orig_setattr
78
+
79
+
80
+ _type_map: Dict[Tuple[IDSDataType, int], Type] = {}
81
+ """Map of IDSDataType and ndim to IDSBase implementation class."""
82
+
83
+
84
+ def _build_type_map():
85
+ """Populate _type_map.
86
+
87
+ This must be done in a separate function to avoid circular imports.
88
+ """
89
+ from imas.ids_primitive import (
90
+ IDSComplex0D,
91
+ IDSFloat0D,
92
+ IDSInt0D,
93
+ IDSNumericArray,
94
+ IDSString0D,
95
+ IDSString1D,
96
+ )
97
+ from imas.ids_struct_array import IDSStructArray
98
+ from imas.ids_structure import IDSStructure
99
+ from imas.ids_toplevel import IDSToplevel
100
+
101
+ _type_map[(None, 0)] = IDSToplevel
102
+ _type_map[(IDSDataType.STRUCTURE, 0)] = IDSStructure
103
+ _type_map[(IDSDataType.STRUCT_ARRAY, 1)] = IDSStructArray
104
+ _type_map[(IDSDataType.STR, 0)] = IDSString0D
105
+ _type_map[(IDSDataType.STR, 1)] = IDSString1D
106
+ _type_map[(IDSDataType.INT, 0)] = IDSInt0D
107
+ _type_map[(IDSDataType.FLT, 0)] = IDSFloat0D
108
+ _type_map[(IDSDataType.CPX, 0)] = IDSComplex0D
109
+ for dim in range(1, 7):
110
+ _type_map[(IDSDataType.INT, dim)] = IDSNumericArray
111
+ _type_map[(IDSDataType.FLT, dim)] = IDSNumericArray
112
+ _type_map[(IDSDataType.CPX, dim)] = IDSNumericArray
113
+
114
+
115
+ class IDSMetadata:
116
+ """Container for IDS Metadata stored in the Data Dictionary.
117
+
118
+ Metadata is everything saved in the attributes of variables in IDSDef.xml.
119
+ This includes for example documentation, its units, and coordinates.
120
+
121
+ Metadata of structure (and array of structures) child nodes can be obtained with the
122
+ indexing operator:
123
+
124
+ .. code-block:: python
125
+
126
+ core_profiles = imas.IDSFactory().core_profiles()
127
+ # Get the metadata of the time child of the profiles_1d array of structures
128
+ p1d_time_meta = core_profiles.metadata["profiles_1d/time"]
129
+
130
+ Note:
131
+ This class should not be instantiated directly, use
132
+ :func:`get_toplevel_metadata` instead.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ structure_xml: Element,
138
+ context_path: str,
139
+ parent_meta: Optional["IDSMetadata"],
140
+ ) -> None:
141
+ attrib = structure_xml.attrib
142
+ self._structure_xml = structure_xml
143
+ self._parent = parent_meta
144
+
145
+ # Mandatory attributes
146
+ self.name: str = attrib["name"]
147
+ """Name of the IDS node, for example ``"comment"``."""
148
+
149
+ # Context path: path relative to the nearest Array of Structures
150
+ if parent_meta is None: # Toplevel IDS
151
+ self._ctx_path = ""
152
+ elif context_path:
153
+ self._ctx_path = f"{context_path}/{self.name}"
154
+ else:
155
+ self._ctx_path = self.name
156
+
157
+ # These are special and used in imas-python logic,
158
+ # so we need to ensure proper values
159
+ maxoccur = attrib.get("maxoccur", "unbounded")
160
+ self.maxoccur: Optional[int] = (
161
+ None if maxoccur == "unbounded" else int(maxoccur)
162
+ )
163
+ """Maximum number of occurrences allowed in the MDS+ backend. Applies to IDS
164
+ toplevels and Arrays of Structures."""
165
+ self.data_type: IDSDataType
166
+ """Data type of the IDS node."""
167
+ self.ndim: int
168
+ """Number of dimensions (rank) of the IDS node."""
169
+ self.data_type, self.ndim = IDSDataType.parse(attrib.get("data_type", None))
170
+ self.path_string: str = attrib.get("path", "") # IDSToplevel has no path
171
+ """Path of this IDS node from the IDS toplevel, for example
172
+ ``"ids_properties/comment"``."""
173
+ self.path: IDSPath = IDSPath(self.path_string)
174
+ """Parsed path of this IDS node from the IDS toplevel, see also
175
+ :py:attr:`path_string`."""
176
+ self.path_doc: str = attrib.get("path_doc", "") # IDSToplevel has no path
177
+ """Path of this IDS node from the IDS toplevel, as shown in the Data Dictionary
178
+ documentation. For example ``"time_slice(itime)/profiles_2d(i1)/r(:,:)"``."""
179
+ self.type: IDSType = IDSType(attrib.get("type", None))
180
+ """Type of the IDS node, indicating if this node is time dependent. Possible
181
+ values are ``dynamic`` (i.e. time-dependent), ``constant`` and ``static``."""
182
+ self.timebasepath = attrib.get("timebasepath", "")
183
+ self.units: str = attrib.get("units", "")
184
+ """Units of this IDS node. For example ``"m.s^-2"``."""
185
+ if self.units == "as_parent" and parent_meta is not None:
186
+ self.units = parent_meta.units
187
+ self.documentation = attrib.get("documentation", None)
188
+ """Data dictionary-provided documentation for this IDS node."""
189
+
190
+ # timebasepath is not always defined in the DD XML, mainly not for struct_arrays
191
+ # Also, when it is defined, it may not be correct (DD 3.39.0)
192
+ if self.data_type is IDSDataType.STRUCT_ARRAY:
193
+ # https://git.iter.org/projects/IMAS/repos/access-layer/browse/pythoninterface/py_ids.xsl?at=refs%2Ftags%2F4.11.4#367-384
194
+ if self.type.is_dynamic:
195
+ self.timebasepath = self._ctx_path + "/time"
196
+ else:
197
+ self.timebasepath = ""
198
+ else: # IDSPrimitive
199
+ # https://git.iter.org/projects/IMAS/repos/access-layer/browse/pythoninterface/py_ids.xsl?at=refs%2Ftags%2F4.11.4#1524-1566
200
+ if self.timebasepath and (
201
+ not self.type.is_dynamic or self._parent._is_dynamic
202
+ ):
203
+ self.timebasepath = ""
204
+ self._is_dynamic = False
205
+ if self._parent is not None:
206
+ self._is_dynamic = self.type.is_dynamic or self._parent._is_dynamic
207
+
208
+ self.coordinates: "tuple[IDSCoordinate]"
209
+ """Tuple of coordinates of this node.
210
+
211
+ ``coordinates[0]`` is the coordinate of the first dimension, etc."""
212
+ self.coordinates_same_as: "tuple[IDSCoordinate]"
213
+ """Indicates quantities which share the same coordinate in a given dimension,
214
+ but the coordinate is not explicitly stored in the IDS."""
215
+ if self.ndim == 0:
216
+ self.coordinates = ()
217
+ self.coordinates_same_as = ()
218
+ else:
219
+ # Parse coordinates
220
+ coors = [IDSCoordinate("")] * self.ndim
221
+ coors_same_as = [IDSCoordinate("")] * self.ndim
222
+ for dim in range(self.ndim):
223
+ coor = f"coordinate{dim + 1}"
224
+ if coor in attrib:
225
+ coors[dim] = IDSCoordinate(attrib[coor])
226
+ setattr(self, coor, coors[dim])
227
+ if coor + "_same_as" in attrib:
228
+ coors_same_as[dim] = IDSCoordinate(attrib[coor + "_same_as"])
229
+ setattr(self, coor + "_same_as", coors_same_as[dim])
230
+ self.coordinates = tuple(coors)
231
+ self.coordinates_same_as = tuple(coors_same_as)
232
+
233
+ # Parse alternative coordinates
234
+ self.alternative_coordinates: "tuple[IDSPath]" = ()
235
+ """Quantities that can be used as coordinate instead of this node."""
236
+ if "alternative_coordinate1" in attrib:
237
+ self.alternative_coordinates = tuple(
238
+ IDSPath(coor) for coor in attrib["alternative_coordinate1"].split(";")
239
+ )
240
+
241
+ # Store any remaining attributes from the DD XML
242
+ for attr_name in attrib:
243
+ if attr_name not in self.__dict__ and not attr_name.startswith("_"):
244
+ self.__dict__[attr_name] = attrib[attr_name]
245
+
246
+ # Cache children in a read-only dict
247
+ ctx_path = "" if self.data_type is IDSDataType.STRUCT_ARRAY else self._ctx_path
248
+ self._children = types.MappingProxyType(
249
+ {
250
+ xml_child.get("name"): IDSMetadata(xml_child, ctx_path, self)
251
+ for xml_child in structure_xml
252
+ }
253
+ )
254
+
255
+ # Cache node type
256
+ self._node_type: Type = _type_map[self.data_type, self.ndim]
257
+ # AL expects ndim of STR types to be one more (STR_0D is 1D array of chars)
258
+ self._al_ndim = self.ndim + (self.data_type is IDSDataType.STR)
259
+
260
+ def __repr__(self) -> str:
261
+ return f"<IDSMetadata for '{self.name}'>"
262
+
263
+ def __setattr__(self, name: str, value: Any) -> None:
264
+ raise RuntimeError("Cannot set attribute: IDSMetadata is read-only.")
265
+
266
+ def __delattr__(self, name: str) -> None:
267
+ raise RuntimeError("Cannot delete attribute: IDSMetadata is read-only.")
268
+
269
+ def __copy__(self) -> "IDSMetadata":
270
+ return self # IDSMetadata is immutable
271
+
272
+ def __deepcopy__(self, memo: dict) -> "IDSMetadata":
273
+ return self # IDSMetadata is immutable
274
+
275
+ def __iter__(self) -> Iterator["IDSMetadata"]:
276
+ return iter(self._children.values())
277
+
278
+ def __getitem__(self, path) -> "IDSMetadata":
279
+ item = self
280
+ for part in re.split("[./]", path):
281
+ try:
282
+ item = item._children[part]
283
+ except KeyError:
284
+ raise KeyError(
285
+ f"Invalid path '{path}', '{item.name}' does not have a "
286
+ f"'{part}' element."
287
+ ) from None
288
+ return item
289
+
290
+ @property
291
+ def identifier_enum(self) -> Optional[Type[IDSIdentifier]]:
292
+ """The identifier enum for this IDS node (if available).
293
+
294
+ This property is an identifier enum (a subclass of
295
+ :py:class:`imas.ids_identifiers.IDSIdentifier`) if this node represents an
296
+ identifier, and the Data Dictionary defines the allowed identifier values.
297
+
298
+ This property is ``None`` when this node is not an identifier, or the Data
299
+ Dictionary does not define the allowed identifier values.
300
+
301
+ .. seealso:: :ref:`Identifiers`
302
+ """
303
+ doc_identifier = getattr(self, "doc_identifier", None)
304
+ if not doc_identifier:
305
+ return None
306
+ identifier_name = Path(doc_identifier).stem
307
+ return identifiers[identifier_name]