Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions docs/source/identifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ enumerated list of options for defining, for example:
- These may have alternative naming conventions supported through aliases
(e.g., "235U" and "U_235" for Uranium 235).

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

1. An index (integer)
2. A name (short string)
3. A description (long string)
4. List of aliases (list of short strings)

.. seealso::
`Data Dictionary documentation for identifiers
<https://imas-data-dictionary.readthedocs.io/en/latest/identifiers.html>`__


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

Each identifier option provides the following attributes:

- ``name``: the name of the option.
- ``index``: the integer index value of the option.
- ``description``: a longer string describing the option.
- ``aliases``: a list of aliases that can be used instead of the name.
- ``units``: optional information about the units of the quantities that are affected by
the identifier option. Take, for example, the `poloidal plan coordinate identifier
<https://imas-data-dictionary.readthedocs.io/en/stable/generated/identifier/poloidal_plane_coordinates_identifier.html>`__
which affects the units of ``grid/dim1`` and ``grid/dim2``.

.. versionadded:: 2.1.0 ``aliases`` for identifiers.
.. versionadded:: 2.3.0 ``units`` metadata.

.. code-block:: python
:caption: Accessing identifiers

Expand All @@ -47,6 +64,9 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
print(csid.total.index)
print(csid.total.description)

# Search identifier options by their index value
print(csid(1))

# Access identifiers with aliases (when available)
mid = imas.identifiers.materials_identifier
print(mid["235U"].name) # Access by canonical name
Expand All @@ -57,12 +77,12 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
assert mid["235U"].name is mid.U_235.name

# Item access is also possible
print(identifiers["edge_source_identifier"])
print(imas.identifiers["edge_source_identifier"])

# You can use imas.util.inspect to list all options
imas.util.inspect(identifiers.ggd_identifier)
imas.util.inspect(imas.identifiers.ggd_identifier)
# And also to get more details of a specific option
imas.util.inspect(identifiers.ggd_identifier.SN)
imas.util.inspect(imas.identifiers.ggd_identifier.SN)

# When an IDS node is an identifier, you can use
# metadata.identifier_enum to get the identifier
Expand Down Expand Up @@ -186,6 +206,7 @@ material_identifier["235U"].
mat.names[0] = mid.U_235.name # enum value via alias
mat.names[0] = mid["U_235"].name # enum value via alias


Compare identifiers
-------------------

Expand Down
43 changes: 28 additions & 15 deletions imas/ids_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
from enum import Enum
from typing import Iterable, List, Type
from typing import Iterable, List, Type, Optional
from xml.etree.ElementTree import fromstring

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

def __new__(cls, value: int, description: str, aliases: list = []):
name: str
"""Name of this identifier value."""
index: int
"""Unique index for this identifier value."""
description: str
"""Description for this identifier value."""
aliases: list[str]
"""Alternative names for this identifier value."""
units: Optional[str]
"""Units of the quantity/quantities altered by this identifier. May be ``None`` if
the Data Dictionary doesn't provide this metadata."""

def __new__(
cls, value: int, description: str, aliases: list[str], units: Optional[str]
):
obj = object.__new__(cls)
obj._value_ = value
obj.index = value
obj.description = description
obj.aliases = aliases
obj.units = units
return obj

def __init__(self, value: int, description: str, aliases: list = []) -> None:
self.index = value
"""Unique index for this identifier value."""
self.description = description
"""Description for this identifier value."""
self.aliases = aliases
"""Alternative names for this identifier value."""

def __eq__(self, other):
if self is other:
return True
Expand Down Expand Up @@ -68,19 +78,22 @@ def __eq__(self, other):
def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
element = fromstring(xml)
enum_values = {}
aliases = {}
for int_element in element.iterfind("int"):
name = int_element.get("name")
value = int_element.text
assert value is not None
description = int_element.get("description")
# alias attribute may contain multiple comma-separated aliases
alias_attr = int_element.get("alias", "")
aliases = [a.strip() for a in alias_attr.split(",") if a.strip()]
# Canonical entry: use the canonical 'name' as key
enum_values[name] = (int(value), description, aliases)
# Also add alias names as enum *aliases* (they become enum attributes)
units = int_element.get("units")

# Identifier can be looked up by its name or any of its aliases:
enumvalue = (int(value), description, aliases, units)
enum_values[name] = enumvalue
for alias in aliases:
enum_values[alias] = (int(value), description, aliases)
enum_values[alias] = enumvalue

# Create the enumeration
enum = cls(
identifier_name,
Expand Down
27 changes: 18 additions & 9 deletions imas/test/test_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
from imas.ids_factory import IDSFactory
from imas.ids_identifiers import IDSIdentifier, identifiers

has_aliases = Version(importlib.metadata.version("imas_data_dictionaries")) >= Version(
"4.1.0"
)
requires_aliases = pytest.mark.skipif(
not has_aliases, reason="Requires DD 4.1.0 for identifier aliases"
requires_dd4_1 = pytest.mark.skipif(
Version(importlib.metadata.version("imas_data_dictionaries")) < Version("4.1.0"),
reason="Test requires DD 4.1.0 for additional identifier metadata",
)


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


@requires_aliases
@requires_dd4_1
def test_identifier_struct_assignment_with_aliases():
"""Test identifier struct assignment with aliases using materials_identifier."""
mid = identifiers.materials_identifier
Expand Down Expand Up @@ -174,7 +172,7 @@ def test_invalid_identifier_assignment():
cs.source[0].identifier = -1


@requires_aliases
@requires_dd4_1
def test_identifier_aliases():
"""Test identifier enum aliases functionality."""
mid = identifiers.materials_identifier
Expand All @@ -197,7 +195,7 @@ def test_identifier_aliases():
assert mid[alias] is mid.U_235


@requires_aliases
@requires_dd4_1
def test_identifier_alias_equality():
"""Test that identifiers with aliases are equal when comparing names and aliases."""
mid = identifiers.materials_identifier
Expand Down Expand Up @@ -276,7 +274,7 @@ def test_identifier_alias_equality():
assert mat5.descriptions[2] == mid["U_235"].description


@requires_aliases
@requires_dd4_1
def test_identifier_alias_equality_non_ggd():
"""Test identifier aliases functionality on non-ggd material"""
mid = identifiers.materials_identifier
Expand All @@ -293,3 +291,14 @@ def test_identifier_alias_equality_non_ggd():
summary_ids.wall.material.name = "235U" # Use canonical name
assert summary_ids.wall.material == mid["235U"]
assert summary_ids.wall.material == mid["U_235"]


@requires_dd4_1
def test_identifier_units():
ppcid = identifiers.poloidal_plane_coordinates_identifier
assert ppcid.rectangular.units == "m,m"
assert ppcid.inverse.units == "m,rad"

# materials identifier doesn't have units (and I don't expect they'll ever get any)
mid = identifiers.materials_identifier
assert mid.W.units is None