Skip to content

Commit 8caae6d

Browse files
maarten-icolivhoenen
authored andcommitted
Add units metadata to Identifiers
N.B. This commit also includes a refactor to remove IDSIdentifier.__init__ and use __new__ only. See https://docs.python.org/3/howto/enum.html#when-to-use-new-vs-init
1 parent dceda68 commit 8caae6d

3 files changed

Lines changed: 73 additions & 30 deletions

File tree

‎docs/source/identifiers.rst‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ enumerated list of options for defining, for example:
1414
- These may have alternative naming conventions supported through aliases
1515
(e.g., "235U" and "U_235" for Uranium 235).
1616

17-
Identifiers are a list of possible valid labels. Each label has up to four
18-
representations:
17+
Identifiers are a list of possible valid options. Each option has three representations
18+
that are stored in an IDS:
1919

2020
1. An index (integer)
2121
2. A name (short string)
2222
3. A description (long string)
23-
4. List of aliases (list of short strings)
23+
24+
.. seealso::
25+
`Data Dictionary documentation for identifiers
26+
<https://imas-data-dictionary.readthedocs.io/en/latest/identifiers.html>`__
2427

2528

2629
Identifiers in IMAS-Python
@@ -32,6 +35,20 @@ constructed on-demand from the loaded Data Dictionary definitions.
3235
All identifier enums can be accessed through ``imas.identifiers``. A list of
3336
the available identifiers is stored as ``imas.identifiers.identifiers``.
3437

38+
Each identifier option provides the following attributes:
39+
40+
- ``name``: the name of the option.
41+
- ``index``: the integer index value of the option.
42+
- ``description``: a longer string describing the option.
43+
- ``aliases``: a list of aliases that can be used instead of the name.
44+
- ``units``: optional information about the units of the quantities that are affected by
45+
the identifier option. Take, for example, the `poloidal plan coordinate identifier
46+
<https://imas-data-dictionary.readthedocs.io/en/stable/generated/identifier/poloidal_plane_coordinates_identifier.html>`__
47+
which affects the units of ``grid/dim1`` and ``grid/dim2``.
48+
49+
.. versionadded:: 2.1.0 ``aliases`` for identifiers.
50+
.. versionadded:: 2.3.0 ``units`` metadata.
51+
3552
.. code-block:: python
3653
:caption: Accessing identifiers
3754
@@ -47,6 +64,9 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
4764
print(csid.total.index)
4865
print(csid.total.description)
4966
67+
# Search identifier options by their index value
68+
print(csid(1))
69+
5070
# Access identifiers with aliases (when available)
5171
mid = imas.identifiers.materials_identifier
5272
print(mid["235U"].name) # Access by canonical name
@@ -57,12 +77,12 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
5777
assert mid["235U"].name is mid.U_235.name
5878
5979
# Item access is also possible
60-
print(identifiers["edge_source_identifier"])
80+
print(imas.identifiers["edge_source_identifier"])
6181
6282
# You can use imas.util.inspect to list all options
63-
imas.util.inspect(identifiers.ggd_identifier)
83+
imas.util.inspect(imas.identifiers.ggd_identifier)
6484
# And also to get more details of a specific option
65-
imas.util.inspect(identifiers.ggd_identifier.SN)
85+
imas.util.inspect(imas.identifiers.ggd_identifier.SN)
6686
6787
# When an IDS node is an identifier, you can use
6888
# metadata.identifier_enum to get the identifier
@@ -186,6 +206,7 @@ material_identifier["235U"].
186206
mat.names[0] = mid.U_235.name # enum value via alias
187207
mat.names[0] = mid["U_235"].name # enum value via alias
188208
209+
189210
Compare identifiers
190211
-------------------
191212

‎imas/ids_identifiers.py‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from enum import Enum
7-
from typing import Iterable, List, Type
7+
from typing import Iterable, List, Type, Optional
88
from xml.etree.ElementTree import fromstring
99

1010
from imas import dd_zip
@@ -15,19 +15,29 @@
1515
class IDSIdentifier(Enum):
1616
"""Base class for all identifier enums."""
1717

18-
def __new__(cls, value: int, description: str, aliases: list = []):
18+
name: str
19+
"""Name of this identifier value."""
20+
index: int
21+
"""Unique index for this identifier value."""
22+
description: str
23+
"""Description for this identifier value."""
24+
aliases: list[str]
25+
"""Alternative names for this identifier value."""
26+
units: Optional[str]
27+
"""Units of the quantity/quantities altered by this identifier. May be ``None`` if
28+
the Data Dictionary doesn't provide this metadata."""
29+
30+
def __new__(
31+
cls, value: int, description: str, aliases: list[str], units: Optional[str]
32+
):
1933
obj = object.__new__(cls)
2034
obj._value_ = value
35+
obj.index = value
36+
obj.description = description
37+
obj.aliases = aliases
38+
obj.units = units
2139
return obj
2240

23-
def __init__(self, value: int, description: str, aliases: list = []) -> None:
24-
self.index = value
25-
"""Unique index for this identifier value."""
26-
self.description = description
27-
"""Description for this identifier value."""
28-
self.aliases = aliases
29-
"""Alternative names for this identifier value."""
30-
3141
def __eq__(self, other):
3242
if self is other:
3343
return True
@@ -68,19 +78,22 @@ def __eq__(self, other):
6878
def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
6979
element = fromstring(xml)
7080
enum_values = {}
71-
aliases = {}
7281
for int_element in element.iterfind("int"):
7382
name = int_element.get("name")
7483
value = int_element.text
84+
assert value is not None
7585
description = int_element.get("description")
7686
# alias attribute may contain multiple comma-separated aliases
7787
alias_attr = int_element.get("alias", "")
7888
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)
89+
units = int_element.get("units")
90+
91+
# Identifier can be looked up by its name or any of its aliases:
92+
enumvalue = (int(value), description, aliases, units)
93+
enum_values[name] = enumvalue
8294
for alias in aliases:
83-
enum_values[alias] = (int(value), description, aliases)
95+
enum_values[alias] = enumvalue
96+
8497
# Create the enumeration
8598
enum = cls(
8699
identifier_name,

‎imas/test/test_identifiers.py‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
from imas.ids_factory import IDSFactory
77
from imas.ids_identifiers import IDSIdentifier, identifiers
88

9-
has_aliases = Version(importlib.metadata.version("imas_data_dictionaries")) >= Version(
10-
"4.1.0"
11-
)
12-
requires_aliases = pytest.mark.skipif(
13-
not has_aliases, reason="Requires DD 4.1.0 for identifier aliases"
9+
requires_dd4_1 = pytest.mark.skipif(
10+
Version(importlib.metadata.version("imas_data_dictionaries")) < Version("4.1.0"),
11+
reason="Test requires DD 4.1.0 for additional identifier metadata",
1412
)
1513

1614

@@ -112,7 +110,7 @@ def test_identifiers_with_aliases():
112110
assert identifier.CxHy.aliases == ["alias1", "alias2", "3alias"]
113111

114112

115-
@requires_aliases
113+
@requires_dd4_1
116114
def test_identifier_struct_assignment_with_aliases():
117115
"""Test identifier struct assignment with aliases using materials_identifier."""
118116
mid = identifiers.materials_identifier
@@ -174,7 +172,7 @@ def test_invalid_identifier_assignment():
174172
cs.source[0].identifier = -1
175173

176174

177-
@requires_aliases
175+
@requires_dd4_1
178176
def test_identifier_aliases():
179177
"""Test identifier enum aliases functionality."""
180178
mid = identifiers.materials_identifier
@@ -197,7 +195,7 @@ def test_identifier_aliases():
197195
assert mid[alias] is mid.U_235
198196

199197

200-
@requires_aliases
198+
@requires_dd4_1
201199
def test_identifier_alias_equality():
202200
"""Test that identifiers with aliases are equal when comparing names and aliases."""
203201
mid = identifiers.materials_identifier
@@ -276,7 +274,7 @@ def test_identifier_alias_equality():
276274
assert mat5.descriptions[2] == mid["U_235"].description
277275

278276

279-
@requires_aliases
277+
@requires_dd4_1
280278
def test_identifier_alias_equality_non_ggd():
281279
"""Test identifier aliases functionality on non-ggd material"""
282280
mid = identifiers.materials_identifier
@@ -293,3 +291,14 @@ def test_identifier_alias_equality_non_ggd():
293291
summary_ids.wall.material.name = "235U" # Use canonical name
294292
assert summary_ids.wall.material == mid["235U"]
295293
assert summary_ids.wall.material == mid["U_235"]
294+
295+
296+
@requires_dd4_1
297+
def test_identifier_units():
298+
ppcid = identifiers.poloidal_plane_coordinates_identifier
299+
assert ppcid.rectangular.units == "m,m"
300+
assert ppcid.inverse.units == "m,rad"
301+
302+
# materials identifier doesn't have units (and I don't expect they'll ever get any)
303+
mid = identifiers.materials_identifier
304+
assert mid.W.units is None

0 commit comments

Comments
 (0)