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.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- 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
|
imas/ids_identifiers.py
ADDED
|
@@ -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]
|