Skip to content

Commit 57034e4

Browse files
Merge branch 'iterorganization:develop' into feature/allow-slices-for-IDSStructArray
2 parents 9116f19 + 9c56a3e commit 57034e4

11 files changed

Lines changed: 547 additions & 37 deletions

File tree

.github/workflows/test_with_pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] # Test on multiple Python versions
13+
python-version: ["3.10", "3.11", "3.12", "3.13"] # Test on multiple Python versions
1414

1515
steps:
1616
- name: Checkout repository

conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ def pytest_addoption(parser):
7070
"hdf5": HDF5_BACKEND,
7171
"mdsplus": MDSPLUS_BACKEND,
7272
}
73+
try:
74+
from imas.db_entry import DBEntry
75+
from imas_core.exception import ImasCoreBackendException
76+
DBEntry("imas:mdsplus?path=dummy","r")
77+
except ImasCoreBackendException as iex:
78+
if "not available" in str(iex.message):
79+
_BACKENDS.pop("mdsplus")
7380

7481

7582
try:

docs/source/changelog.rst

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,44 @@
33
Changelog
44
=========
55

6+
What's new in IMAS-Python 2.1.0
7+
-------------------------------
8+
9+
Build
10+
'''''
11+
12+
- update Python version support (remove 3.8, add 3.13)
13+
- add dependency on `imas_core <https://pypi.org/project/imas-core/>`__
14+
15+
16+
Improvements
17+
''''''''''''
18+
19+
- :issue:`84`: improve `imas process-db-analysis`
20+
- :issue:`71`: take into account identifier aliases (introduced in DD 4.1)
21+
- :issue:`78`: disable *implicit* conversion when crossing a major version update
22+
- improve integration of UDA backend
23+
- cleaning old AL4 deprecated code
24+
- :issue:`59`: convert name+identifier (DD3) into description+name (DD4)
25+
- improve type hints (following PEP-585 and PEP-604)
26+
- improve performance of IDS deepcopy
27+
- :issue:`60`: improve `equilibrium` DD3->4 by converting `boundary_separatrix` into `contour_tree`
28+
- :issue:`22`: add custom conversion example in the doc for `em_coupling` IDS
29+
30+
31+
Bug fixes
32+
'''''''''
33+
34+
- fix testcases with coordinate validation issues
35+
- :issue:`80`: fix `imas print` when using netcdf and imas_core is not present
36+
- :issue:`61`: special DD3->4 rule to flip sign quantities missing the `cocos_label_transform attribute` in DD
37+
- :merge:`58`: fix unclear provenance capture
38+
- :merge:`57`: fix 0D arrays from lazy loading with netcdf
39+
- :issue:`55`: handle missing case when converting 3.42->4 (_tor->_phi)
40+
41+
42+
43+
644
What's new in IMAS-Python 2.0.1
745
-------------------------------
846

@@ -74,9 +112,9 @@ Bug fixes
74112
Dictionary 4.0.0 and 3.42.0. In other cases, the Data Dictionary version is
75113
now explicitly indicated.
76114

77-
- :issue:`IMAS-5560`: Fix a bug where IMASPy would not correctly recognize that
115+
- IMAS-5560: Fix a bug where IMASPy would not correctly recognize that
78116
the UDA backend is used.
79-
- :issue:`IMAS-5541`: Fix a bug when converting a closed contour to Data
117+
- IMAS-5541: Fix a bug when converting a closed contour to Data
80118
Dictionary version 4.0.0.
81119
- Work around incorrect Data Dictionary 3.x metadata when converting
82120
``flux_loop/flux`` in the ``magnetics`` IDS to Data Dictionary version 4.0.0.

docs/source/conf.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,21 @@
3939
iter_projects = "https://github.com/iterorganization/"
4040
dd_url = urljoin(iter_projects, "imas-data-dictionary/")
4141
al_url = urljoin(iter_projects, "imas-core/")
42-
issue_url = jira_url = "https://github.com/iterorganization/IMAS-Python/issues"
4342

4443
# IMAS-Python
4544
repository_url = f"{iter_projects}/{src_project}/"
4645
blob_url = repository_url
47-
mr_url = urljoin(repository_url, "/pulls")
46+
issue_url = urljoin(repository_url, "issues/")
47+
mr_url = urljoin(repository_url, "pull/")
4848

4949

5050
# Configuration of sphinx.ext.extlinks
5151
# See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
5252
# unique name: (base URL, label prefix)
5353
extlinks = {
5454
"src": (blob_url + "%s", "%s"),
55-
"issue": (issue_url + "%s", "%s"),
56-
"merge": (mr_url + "%s", "!%s"),
55+
"issue": (issue_url + "%s", "#%s"),
56+
"merge": (mr_url + "%s", "#%s"),
5757
"dd": (dd_url + "%s", "%s"),
5858
"al": (al_url + "%s", "%s"),
5959
"pypa": ("https://packaging.python.org/%s", None),

docs/source/identifiers.rst

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ enumerated list of options for defining, for example:
1111
a neutron, or a photon.
1212
- Plasma heating may come from neutral beam injection, electron cyclotron heating,
1313
ion cyclotron heating, lower hybrid heating, alpha particles.
14+
- These may have alternative naming conventions supported through aliases
15+
(e.g., "235U" and "U_235" for Uranium 235).
1416

15-
Identifiers are a list of possible valid labels. Each label has three
17+
Identifiers are a list of possible valid labels. Each label has up to four
1618
representations:
1719

1820
1. An index (integer)
1921
2. A name (short string)
2022
3. A description (long string)
23+
4. List of aliases (list of short strings)
2124

2225

2326
Identifiers in IMAS-Python
@@ -44,6 +47,15 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
4447
print(csid.total.index)
4548
print(csid.total.description)
4649
50+
# Access identifiers with aliases (when available)
51+
mid = imas.identifiers.materials_identifier
52+
print(mid["235U"].name) # Access by canonical name
53+
print(mid["U_235"].name) # Access by alias
54+
55+
# Both return the same object
56+
assert mid["235U"].name is mid["U_235"].name
57+
assert mid["235U"].name is mid.U_235.name
58+
4759
# Item access is also possible
4860
print(identifiers["edge_source_identifier"])
4961
@@ -64,8 +76,8 @@ Assigning identifiers in IMAS-Python
6476

6577
IMAS-Python implements smart assignment of identifiers. You may assign an identifier
6678
enum value (for example ``imas.identifiers.core_source_identifier.total``), a
67-
string (for example ``"total"``) or an integer (for example ``"1"``) to an
68-
identifier structure (for example ``core_profiles.source[0].identifier``) to set
79+
string (for example ``"total"`` or its alias), or an integer (for example ``"1"``)
80+
to an identifier structure (for example ``core_profiles.source[0].identifier``) to set
6981
all three child nodes ``name``, ``index`` and ``description`` in one go. See
7082
below example:
7183

@@ -86,6 +98,20 @@ below example:
8698
# 3. Assign an integer. This looks up the index in the identifier enum:
8799
core_sources.source[0].identifier = 1
88100
101+
# Identifiers can still be assigned with the old alias name for backward compatibility:
102+
wallids = imas.IDSFactory().wall()
103+
wallids.description_ggd.resize(1)
104+
wallids.description_ggd[0].material.resize(1)
105+
wallids.description_ggd[0].material[0].grid_subset.resize(1)
106+
mat = wallids.description_ggd[0].material[0].grid_subset[0].identifiers
107+
mat.names.extend([""] * 1)
108+
mid = imas.identifiers.materials_identifier
109+
# Assign using canonical name
110+
mat.names[0] = "235U"
111+
# Or assign using alias (equivalent to above)
112+
mat.names[0] = mid["U_235"].name
113+
mat.names[0] = mid.U_235.name
114+
89115
# Inspect the contents of the structure
90116
imas.util.inspect(core_sources.source[0].identifier)
91117
@@ -101,18 +127,78 @@ below example:
101127
imas.util.inspect(core_sources.source[1].identifier)
102128
103129
130+
Identifier aliases
131+
------------------
132+
133+
Some identifiers may have multiple aliases defined in the Data Dictionary. Aliases are
134+
former names kept as an option to ensure better backward compatibility after a change
135+
and support multiple naming conventions. An identifier can have any number of
136+
comma-separated aliases.
137+
138+
Aliases can be accessed in the same ways as canonical names, and all aliases for an
139+
identifier point to the same object.
140+
141+
Aliases that begin with a number (e.g., 235U) cannot be accessed using dot notation
142+
(e.g., material_identifier.235U) due to Python's syntax restrictions. Instead, such
143+
aliases must be accessed using dictionary-style indexing, for example:
144+
material_identifier["235U"].
145+
146+
.. code-block:: python
147+
:caption: Working with identifier aliases
148+
149+
import imas
150+
151+
# Get materials identifier which has some aliases defined
152+
mid = imas.identifiers.materials_identifier
153+
154+
# Access by canonical name
155+
uranium235_by_name = mid["235U"]
156+
print(f"Name: {uranium235_by_name.name}")
157+
print(f"Aliases: {uranium235_by_name.aliases}") # List of all aliases
158+
print(f"First alias: {uranium235_by_name.alias}") # First alias for compatibility
159+
print(f"Index: {uranium235_by_name.index}")
160+
print(f"Description: {uranium235_by_name.description}")
161+
162+
# Access by any alias - all return the same object
163+
uranium235_by_alias1 = mid["U_235"].name
164+
uranium235_by_alias2 = mid["Uranium_235"].name
165+
print(f"Same objects: {uranium235_by_name is uranium235_by_alias1 is uranium235_by_alias2}")
166+
167+
# You can also use attribute access for aliases (when valid Python identifiers)
168+
uranium235_by_attr = mid.U_235.name
169+
print(f"Same object: {uranium235_by_name is uranium235_by_attr}")
170+
171+
# When assigning to IDS structures, alias works the following way
172+
wallids = imas.IDSFactory().wall()
173+
wallids.description_ggd.resize(1)
174+
wallids.description_ggd[0].material.resize(1)
175+
wallids.description_ggd[0].material[0].grid_subset.resize(1)
176+
mat = wallids.description_ggd[0].material[0].grid_subset[0].identifiers
177+
mat.names.extend([""] * 1)
178+
mat.indices.resize(1)
179+
mat.descriptions.extend([""] * 1)
180+
mat.indices[0] = 20
181+
mat.descriptions[0] = "Uranium 235 isotope"
182+
183+
# These assignments are all equivalent:
184+
mat.names[0] = "235U" # canonical name
185+
mat.names[0] = mid["235U"].name # enum value
186+
mat.names[0] = mid.U_235.name # enum value via alias
187+
mat.names[0] = mid["U_235"].name # enum value via alias
188+
104189
Compare identifiers
105190
-------------------
106191

107192
Identifier structures can be compared against the identifier enum as well. They
108193
compare equal when:
109194

110195
1. ``index`` is an exact match
111-
2. ``name`` is an exact match, or ``name`` is not filled in the IDS node
196+
2. ``name`` is an exact match, or ``name`` matches an alias, or ``name`` is not filled in the IDS node
112197

113198
The ``description`` does not have to match with the Data Dictionary definition,
114199
but a warning is logged if the description in the IDS node does not match with
115-
the Data Dictionary description:
200+
the Data Dictionary description. The comparison also takes aliases into account,
201+
so an identifier will match both its canonical name and any defined alias:
116202

117203
.. code-block:: python
118204
:caption: Comparing identifiers
@@ -139,6 +225,15 @@ the Data Dictionary description:
139225
>>> core_sources.source[0].identifier.name = "totalX"
140226
>>> core_sources.source[0].identifier == csid.total
141227
False
228+
>>> # Alias comparison example with materials identifier
229+
>>> mid = imas.identifiers.materials_identifier
230+
>>> cxr = imas.IDSFactory().camera_x_rays()
231+
>>> mat = cxr.filter_window.material
232+
>>> mat.index = 20
233+
>>> mat.name = "U_235" # Using alias
234+
>>> # Compares equal to the canonical identifier even though name is alias
235+
>>> mat == mid["235U"].name
236+
True
142237
143238
144239
.. seealso::

imas/backends/netcdf/nc2ids.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ def run(self, lazy: bool) -> None:
157157
for index, node in indexed_tree_iter(self.ids, target_metadata):
158158
value = data[index]
159159
if value != getattr(var, "_FillValue", None):
160+
if isinstance(value, np.generic):
161+
value = value.item()
160162
# NOTE: bypassing IDSPrimitive.value.setter logic
161163
node._IDSPrimitive__value = value
162164

@@ -166,10 +168,16 @@ def run(self, lazy: bool) -> None:
166168
# here, we'll let IDSPrimitive.value.setter take care of it:
167169
self.ids[target_metadata.path].value = data
168170

169-
else:
171+
# We need to unpack 0D ints, floats and complex numbers. For better
172+
# performance this check is done outside the for-loop:
173+
elif metadata.ndim or metadata.data_type is IDSDataType.STR:
170174
for index, node in indexed_tree_iter(self.ids, target_metadata):
171175
# NOTE: bypassing IDSPrimitive.value.setter logic
172176
node._IDSPrimitive__value = data[index]
177+
else:
178+
for index, node in indexed_tree_iter(self.ids, target_metadata):
179+
# NOTE: bypassing IDSPrimitive.value.setter logic
180+
node._IDSPrimitive__value = data[index].item() # Unpack 0D value
173181

174182
def validate_variables(self) -> None:
175183
"""Validate that all variables in the netCDF Group exist and match the DD."""
@@ -365,7 +373,7 @@ def get_child(self, child):
365373
value = var[self.index]
366374

367375
if value is not None:
368-
if isinstance(value, np.ndarray):
376+
if isinstance(value, (np.ndarray, np.generic)):
369377
if value.ndim == 0: # Unpack 0D numpy arrays:
370378
value = value.item()
371379
else:

0 commit comments

Comments
 (0)