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
93 changes: 88 additions & 5 deletions docs/source/identifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ enumerated list of options for defining, for example:
a neutron, or a photon.
- Plasma heating may come from neutral beam injection, electron cyclotron heating,
ion cyclotron heating, lower hybrid heating, alpha particles.
- 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 three
Identifiers are a list of possible valid labels. Each label has up to four
representations:

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


Identifiers in IMAS-Python
Expand All @@ -44,6 +47,14 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
print(csid.total.index)
print(csid.total.description)

# Access identifiers with aliases (when available)
mid = imas.identifiers.materials_identifier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always use name attribute? In the IDS it stored as str('materials_identifier.235U')

import imas
mid = imas.identifiers.materials_identifier
factory2 = imas.IDSFactory().camera_x_rays()
mat2 = factory2.filter_window.material
mat2.name = mid.U_235 # Should we always use name attribute?
mat2.index = mid.U_235.index
mat2.description = mid.U_235.description
imas.util.inspect(mat2)

╭───── IDS structure: filter_window/material (DD version 4.0.1.dev324+g5180c8270.d20251021) ──────╮
│ Material of the filter window                                                                   │
│ ╭──────────────────────────────────────── Attributes ─────────────────────────────────────────╮ │
│ │ has_value = True                                                                            │ │
│ │  metadata = <IDSMetadata for 'material'>                                                    │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ ╭──────────────────────────────────────── Child nodes ────────────────────────────────────────╮ │
│ │ description = <IDSString0D (IDS:camera_x_rays, filter_window/material/description, STR_0D)> │ │
│ │               str('Uranium 235 isotope')                                                    │ │
│ │       index = <IDSInt0D (IDS:camera_x_rays, filter_window/material/index, INT_0D)>          │ │
│ │               int(20)                                                                       │ │
│ │        name = <IDSString0D (IDS:camera_x_rays, filter_window/material/name, STR_0D)>        │ │
│ │               str('materials_identifier.235U')                                              │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

print(mid["235U"]) # Access by canonical name
print(mid["U_235"]) # Access by alias

# Both return the same object
assert mid["235U"] is mid["U_235"]

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

Expand All @@ -64,8 +75,8 @@ Assigning identifiers in IMAS-Python

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

Expand All @@ -86,6 +97,15 @@ below example:
# 3. Assign an integer. This looks up the index in the identifier enum:
core_sources.source[0].identifier = 1

# Identifiers can still be assigned with the old alias name for backward compatibility:
materials = imas.IDSFactory().materials()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

materials ids is not present in the data dictionary. Could you use wall IDS which uses materials_indentfier.

materials.material.resize(1)
mid = imas.identifiers.materials_identifier
# Assign using canonical name
materials.material[0].identifier = "235U"
# Or assign using alias (equivalent to above)
materials.material[0].identifier = "U_235"

# Inspect the contents of the structure
imas.util.inspect(core_sources.source[0].identifier)

Expand All @@ -101,18 +121,72 @@ below example:
imas.util.inspect(core_sources.source[1].identifier)


Identifier aliases
------------------

Some identifiers may have multiple aliases defined in the Data Dictionary. Aliases are
former names kept as an option to ensure better backward compatibility after a change
and support multiple naming conventions. An identifier can have any number of
comma-separated aliases.

Aliases can be accessed in the same ways as canonical names, and all aliases for an
identifier point to the same object.

Aliases that begin with a number (e.g., 235U) cannot be accessed using dot notation
(e.g., material_identifier.235U) due to Python's syntax restrictions. Instead, such
aliases must be accessed using dictionary-style indexing, for example:
material_identifier["235U"].

.. code-block:: python
:caption: Working with identifier aliases

import imas

# Get materials identifier which has some aliases defined
mid = imas.identifiers.materials_identifier

# Access by canonical name
uranium235_by_name = mid["235U"]
print(f"Name: {uranium235_by_name.name}")
print(f"Aliases: {uranium235_by_name.aliases}") # List of all aliases
print(f"First alias: {uranium235_by_name.alias}") # First alias for compatibility
print(f"Index: {uranium235_by_name.index}")
print(f"Description: {uranium235_by_name.description}")

# Access by any alias - all return the same object
uranium235_by_alias1 = mid["U_235"]
uranium235_by_alias2 = mid["Uranium_235"]
print(f"Same objects: {uranium235_by_name is uranium235_by_alias1 is uranium235_by_alias2}")

# You can also use attribute access for aliases (when valid Python identifiers)
uranium235_by_attr = mid.U_235
print(f"Same object: {uranium235_by_name is uranium235_by_attr}")

# When assigning to IDS structures, any alias works the same way
materials = imas.IDSFactory().materials()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

materials ids is not present in the data dictionary. Could you use wall IDS which uses materials_indentfier.

materials.material.resize(1)

# These assignments are all equivalent:
materials.material[0].identifier = "235U" # canonical name
materials.material[0].identifier = "U_235" # first alias
materials.material[0].identifier = "Uranium_235" # another alias
materials.material[0].identifier = mid["235U"] # enum value
materials.material[0].identifier = mid.U_235 # enum value via alias


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

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

1. ``index`` is an exact match
2. ``name`` is an exact match, or ``name`` is not filled in the IDS node
2. ``name`` is an exact match, or ``name`` matches an alias, or ``name`` is not filled in the IDS node

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

.. code-block:: python
:caption: Comparing identifiers
Expand All @@ -139,6 +213,15 @@ the Data Dictionary description:
>>> core_sources.source[0].identifier.name = "totalX"
>>> core_sources.source[0].identifier == csid.total
False
>>> # Alias comparison example with materials identifier
>>> mid = imas.identifiers.materials_identifier
>>> materials = imas.IDSFactory().materials()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

materials ids is not present in the data dictionary. Could you use wall IDS which uses materials_indentfier.

>>> materials.material.resize(1)
>>> materials.material[0].identifier.index = 20
>>> materials.material[0].identifier.name = "U_235" # Using alias
>>> # Compares equal to the canonical identifier even though name is alias
>>> materials.material[0].identifier == mid["235U"]
True


.. seealso::
Expand Down
45 changes: 30 additions & 15 deletions imas/ids_identifiers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# This file is part of IMAS-Python.
# You should have received the IMAS-Python LICENSE file with this project.
"""IMAS-Python module to support Data Dictionary identifiers.
"""
"""IMAS-Python module to support Data Dictionary identifiers."""

import logging
from enum import Enum
Expand All @@ -16,16 +15,18 @@
class IDSIdentifier(Enum):
"""Base class for all identifier enums."""

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

def __init__(self, value: int, description: str) -> None:
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:
Expand All @@ -37,35 +38,49 @@ def __eq__(self, other):
except (AttributeError, TypeError, ValueError):
# Attribute doesn't exist, or failed to convert
return NotImplemented

# Index must match
if other_index == self.index:
# Name may be left empty
if other_name == self.name or other_name == "":
# Name may be left empty, or match name or alias
if (
other_name == self.name
or other_name == ""
or other_name in self.aliases
):
# Description doesn't have to match, though we will warn when it doesn't
if other_description != self.description and other_description != "":
if other_description not in (self.description, ""):
logger.warning(
"Description of %r does not match identifier description %r",
other.description,
self.description,
)
return True
else:
logger.warning(
"Name %r does not match identifier name %r, but indexes are equal.",
other.name,
self.name,
)

# If we get here with matching indexes but no name/alias match, warn
logger.warning(
"Name %r does not match identifier name %r, but indexes are equal.",
other.name,
self.name,
)
return False

@classmethod
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
description = int_element.get("description")
enum_values[name] = (int(value), 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)
for alias in aliases:
enum_values[alias] = (int(value), description, aliases)
# Create the enumeration
enum = cls(
identifier_name,
Expand Down
Loading
Loading