Skip to content

Commit cf7a590

Browse files
authored
[GH#71] Take into account identifier aliases (#77)
1 parent 96738c8 commit cf7a590

File tree

3 files changed

+321
-21
lines changed

3 files changed

+321
-21
lines changed

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/ids_identifiers.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# This file is part of IMAS-Python.
22
# You should have received the IMAS-Python LICENSE file with this project.
3-
"""IMAS-Python module to support Data Dictionary identifiers.
4-
"""
3+
"""IMAS-Python module to support Data Dictionary identifiers."""
54

65
import logging
76
from enum import Enum
@@ -16,16 +15,18 @@
1615
class IDSIdentifier(Enum):
1716
"""Base class for all identifier enums."""
1817

19-
def __new__(self, value: int, description: str):
20-
obj = object.__new__(self)
18+
def __new__(cls, value: int, description: str, aliases: list = []):
19+
obj = object.__new__(cls)
2120
obj._value_ = value
2221
return obj
2322

24-
def __init__(self, value: int, description: str) -> None:
23+
def __init__(self, value: int, description: str, aliases: list = []) -> None:
2524
self.index = value
2625
"""Unique index for this identifier value."""
2726
self.description = description
2827
"""Description for this identifier value."""
28+
self.aliases = aliases
29+
"""Alternative names for this identifier value."""
2930

3031
def __eq__(self, other):
3132
if self is other:
@@ -37,35 +38,49 @@ def __eq__(self, other):
3738
except (AttributeError, TypeError, ValueError):
3839
# Attribute doesn't exist, or failed to convert
3940
return NotImplemented
41+
4042
# Index must match
4143
if other_index == self.index:
42-
# Name may be left empty
43-
if other_name == self.name or other_name == "":
44+
# Name may be left empty, or match name or alias
45+
if (
46+
other_name == self.name
47+
or other_name == ""
48+
or other_name in self.aliases
49+
):
4450
# Description doesn't have to match, though we will warn when it doesn't
45-
if other_description != self.description and other_description != "":
51+
if other_description not in (self.description, ""):
4652
logger.warning(
4753
"Description of %r does not match identifier description %r",
4854
other.description,
4955
self.description,
5056
)
5157
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+
59+
# If we get here with matching indexes but no name/alias match, warn
60+
logger.warning(
61+
"Name %r does not match identifier name %r, but indexes are equal.",
62+
other.name,
63+
self.name,
64+
)
5865
return False
5966

6067
@classmethod
6168
def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
6269
element = fromstring(xml)
6370
enum_values = {}
71+
aliases = {}
6472
for int_element in element.iterfind("int"):
6573
name = int_element.get("name")
6674
value = int_element.text
6775
description = int_element.get("description")
68-
enum_values[name] = (int(value), description)
76+
# alias attribute may contain multiple comma-separated aliases
77+
alias_attr = int_element.get("alias", "")
78+
aliases = [a.strip() for a in alias_attr.split(",") if a.strip()]
79+
# Canonical entry: use the canonical 'name' as key
80+
enum_values[name] = (int(value), description, aliases)
81+
# Also add alias names as enum *aliases* (they become enum attributes)
82+
for alias in aliases:
83+
enum_values[alias] = (int(value), description, aliases)
6984
# Create the enumeration
7085
enum = cls(
7186
identifier_name,

0 commit comments

Comments
 (0)