From c7be2cf347e150b2892493fb1ff699638cca1554 Mon Sep 17 00:00:00 2001 From: wasikj Date: Tue, 9 Jun 2026 06:51:23 +0200 Subject: [PATCH 1/4] Grid identifiers (WIP) --- backend/ibex/data_source/imas_python_source.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/ibex/data_source/imas_python_source.py b/backend/ibex/data_source/imas_python_source.py index 91d57203..fb95db38 100644 --- a/backend/ibex/data_source/imas_python_source.py +++ b/backend/ibex/data_source/imas_python_source.py @@ -628,6 +628,17 @@ def _check_data_is_leaf_node(self, data) -> None: elif isinstance(data, IDSStructure): raise NotALeafNodeException("Cannot serialize non-leaf node") + def _find_grid_type(self, data_node: IDSNumericArray): + + try: + grid_type_node = data_node._parent.grid_type + grid_index = grid_type_node.name + identifiers = imas.identifiers.poloidal_plane_coordinates_identifier + identifier_description = ... + except AttributeError: + ... + + def get_plot_data( self, uri: str, @@ -803,6 +814,9 @@ def get_plot_data( except ValueError: coord_data_shape = "irregular" + + #if re.search(r"dim[0-9]", coord.split("/")[-1]): + #_find_grid_type()... c = { "name": coord.split("/")[-1], "target": f"#{ids}/{target}", From 049578a34e21426fe4c5b61b5bfa0e67b48e2e62 Mon Sep 17 00:00:00 2001 From: wasikj Date: Tue, 9 Jun 2026 13:48:10 +0200 Subject: [PATCH 2/4] dimX aliases (WIP) --- .../ibex/data_source/imas_python_source.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/backend/ibex/data_source/imas_python_source.py b/backend/ibex/data_source/imas_python_source.py index fb95db38..3d0e2b86 100644 --- a/backend/ibex/data_source/imas_python_source.py +++ b/backend/ibex/data_source/imas_python_source.py @@ -628,16 +628,32 @@ def _check_data_is_leaf_node(self, data) -> None: elif isinstance(data, IDSStructure): raise NotALeafNodeException("Cannot serialize non-leaf node") - def _find_grid_type(self, data_node: IDSNumericArray): + def _generate_grid_quantity_alias(self, grid_node: IDSNumericArray): + """ + Generates alias and unit for selected grid node. Assumes grid_node.name == "dimX" X=(1...N) + :param grid_node: + :return: + """ + if grid_node._parent is None or grid_node._parent._parent is None: + return None + if not re.search(r"dim[1-9]", grid_node.metadata.name): + return None + + dim_index = int(grid_node.metadata.name[-1]) - 1 # dim1->0, dim2->1 etc... + # assume grid_node is located inside XXX/grid/ and grid_type is located in XXX/grid_type + grid_type_index = grid_node._parent._parent.grid_type.index + + units = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).units.split(",") try: - grid_type_node = data_node._parent.grid_type - grid_index = grid_type_node.name - identifiers = imas.identifiers.poloidal_plane_coordinates_identifier - identifier_description = ... + axis_labels = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).axis_labels.split(",") + result_alias = axis_labels[dim_index] + except AttributeError: - ... + description = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).description + result_alias = "ALIAS" + return {"alias": result_alias, "unit": units[dim_index]} def get_plot_data( self, @@ -814,9 +830,14 @@ def get_plot_data( except ValueError: coord_data_shape = "irregular" - - #if re.search(r"dim[0-9]", coord.split("/")[-1]): - #_find_grid_type()... + alias = None + alias_unit = None + if re.search(r"dim[1-9]", coord.split("/")[-1]): + alias_dict = self._generate_grid_quantity_alias(first_value) + if alias_dict is not None: + alias = alias_dict["alias"] + alias_unit = alias_dict["unit"] + print(f"==== GENERATED ALIAS FOR : {coord.split('/')[-1]} = {alias} ||| {alias_unit}") c = { "name": coord.split("/")[-1], "target": f"#{ids}/{target}", From b5db488a9dab0f1be1296790086c4e2df669d6d7 Mon Sep 17 00:00:00 2001 From: wasikj Date: Thu, 11 Jun 2026 12:05:29 +0200 Subject: [PATCH 3/4] Introduce dim1/dim2... aliases --- .../ibex/data_source/imas_python_source.py | 44 ++++++++++++------- .../ibex/endpoints/schemas/data_schemas.py | 4 +- backend/tests/conftest.py | 5 +++ backend/tests/test_data_endpoints.py | 27 ++++++++++++ 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/backend/ibex/data_source/imas_python_source.py b/backend/ibex/data_source/imas_python_source.py index 3d0e2b86..1be49429 100644 --- a/backend/ibex/data_source/imas_python_source.py +++ b/backend/ibex/data_source/imas_python_source.py @@ -631,29 +631,39 @@ def _check_data_is_leaf_node(self, data) -> None: def _generate_grid_quantity_alias(self, grid_node: IDSNumericArray): """ Generates alias and unit for selected grid node. Assumes grid_node.name == "dimX" X=(1...N) - :param grid_node: + :param grid_node: IDSNode (named dimX, X = [1...N]) :return: """ + result = {"alias": None, "unit_alias": None} if grid_node._parent is None or grid_node._parent._parent is None: - return None + return result if not re.search(r"dim[1-9]", grid_node.metadata.name): - return None - - dim_index = int(grid_node.metadata.name[-1]) - 1 # dim1->0, dim2->1 etc... + return result # assume grid_node is located inside XXX/grid/ and grid_type is located in XXX/grid_type grid_type_index = grid_node._parent._parent.grid_type.index + if grid_type_index == imas.ids_defs.EMPTY_INT: + return result - units = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).units.split(",") + dim_index = int(grid_node.metadata.name[-1]) - 1 # dim1->0, dim2->1 etc... + # Extract units try: - axis_labels = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).axis_labels.split(",") - result_alias = axis_labels[dim_index] + units = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).units.split(",") + result["unit_alias"] = units[dim_index] + except (ValueError, KeyError, AttributeError): + result["unit_alias"] = None + # Extract axis labels + try: + axis_labels = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).axis_labels.split(",") + result["alias"] = axis_labels[dim_index] except AttributeError: description = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).description - result_alias = "ALIAS" + match = re.findall(r"(\w+)=(dim[1-9])", description) + axis_labels = {v: k for k, v in match} + result["alias"] = axis_labels.get(grid_node.metadata.name, None) - return {"alias": result_alias, "unit": units[dim_index]} + return result def get_plot_data( self, @@ -830,18 +840,20 @@ def get_plot_data( except ValueError: coord_data_shape = "irregular" + coord_name = coord.split("/")[-1] alias = None alias_unit = None - if re.search(r"dim[1-9]", coord.split("/")[-1]): + if re.search(r"dim[1-9]", coord_name): alias_dict = self._generate_grid_quantity_alias(first_value) - if alias_dict is not None: - alias = alias_dict["alias"] - alias_unit = alias_dict["unit"] - print(f"==== GENERATED ALIAS FOR : {coord.split('/')[-1]} = {alias} ||| {alias_unit}") + alias = alias_dict["alias"] + alias_unit = alias_dict["unit_alias"] + c = { - "name": coord.split("/")[-1], + "name": coord_name, + "alias": alias, "target": f"#{ids}/{target}", "unit": first_value.metadata.units, + "unit_alias": alias_unit, "shape": coord_data_shape, # coord_data could be np.ndarray or list[np.ndarray] "downsampled_shape": coord_data_shape, "ndim": first_value.metadata.ndim, diff --git a/backend/ibex/endpoints/schemas/data_schemas.py b/backend/ibex/endpoints/schemas/data_schemas.py index 68052c77..1fbb7e3a 100644 --- a/backend/ibex/endpoints/schemas/data_schemas.py +++ b/backend/ibex/endpoints/schemas/data_schemas.py @@ -17,10 +17,12 @@ class PlotDataCoordinateModel(BaseModel): """Intermediate model for /data/plot_data endpoint""" name: str = Field(description="Node name", examples=["dim1"]) + alias: str = Field(description="Node name alias", examples=["Height"]) target: str = Field( description="Which node coordinate is it", examples=["#equilibrium/time_slice[0]/profiles_2d[0]/psi"] ) - unit: str = Field(description="Data units", examples=[""]) + unit: str = Field(description="Data units", examples=["m", "mixed"]) + unit_alias: str = Field(description="Data units alias", examples=["m"]) shape: list[int] | str = Field(description="Shape of the data", examples=[[129]]) downsampled_shape: list[int] | str = Field(description="Shape of the data after downsampling", examples=[[129]]) ndim: int = Field(description="Number of data dimensions stored in node", examples=[1]) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 220783e1..a7d06e3a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -88,8 +88,13 @@ def entry_path(tmp_path_factory): ion.temperature = np.array([[i, +1, i + 2], [i + 10, i + 11, i + 12], [i + 20, i + 21, i + 32]]) profiles_2d.grid.dim1 = np.array([0, 1, 2]) profiles_2d.grid.dim2 = np.array([0, 1, 2]) + profiles_2d.grid.volume_element = np.array([[1.0, 2.0, 3.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], dtype=float) i += 10 + # for coordinate aliases/units + core_profiles.profiles_2d[0].grid_type = 1 + core_profiles.profiles_2d[1].grid_type = 2 + entry.put(core_profiles) entry.close() diff --git a/backend/tests/test_data_endpoints.py b/backend/tests/test_data_endpoints.py index 5ddd8f51..85e35e8b 100644 --- a/backend/tests/test_data_endpoints.py +++ b/backend/tests/test_data_endpoints.py @@ -122,3 +122,30 @@ def test_plot_data_1_N_coord(entry_path): assert numeric_coordinate["ndim"] == 1 assert numeric_coordinate["path"] == "" assert numeric_coordinate["description"] == "1...N" + + +def test_plot_data_coordinate_aliases(entry_path): + + parameters = { + "uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[0]/grid/volume_element", + } + response = pytest.test_client.get("/data/plot_data", params=parameters) + response_body = response.json() + + assert response.status_code == 200 + + dim1_coordinate = response_body["data"]["coordinates"][0] + assert dim1_coordinate["alias"].lower() == "r" + assert dim1_coordinate["unit_alias"] is None + + parameters = { + "uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[1]/grid/volume_element", + } + response = pytest.test_client.get("/data/plot_data", params=parameters) + response_body = response.json() + + assert response.status_code == 200 + + dim1_coordinate = response_body["data"]["coordinates"][0] + assert dim1_coordinate["alias"].lower() == "rho" + assert dim1_coordinate["unit_alias"] is None From 1eb853d44d441c73cee7930add97e17e8e2c0810 Mon Sep 17 00:00:00 2001 From: wasikj Date: Fri, 12 Jun 2026 11:57:26 +0200 Subject: [PATCH 4/4] Move alias to "name" --- backend/ibex/data_source/imas_python_source.py | 6 +++--- backend/tests/test_data_endpoints.py | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/ibex/data_source/imas_python_source.py b/backend/ibex/data_source/imas_python_source.py index 1be49429..61110056 100644 --- a/backend/ibex/data_source/imas_python_source.py +++ b/backend/ibex/data_source/imas_python_source.py @@ -848,12 +848,12 @@ def get_plot_data( alias = alias_dict["alias"] alias_unit = alias_dict["unit_alias"] + coord_name = alias if alias else coord_name + units = alias_unit if alias_unit else first_value.metadata.units c = { "name": coord_name, - "alias": alias, "target": f"#{ids}/{target}", - "unit": first_value.metadata.units, - "unit_alias": alias_unit, + "unit": units, "shape": coord_data_shape, # coord_data could be np.ndarray or list[np.ndarray] "downsampled_shape": coord_data_shape, "ndim": first_value.metadata.ndim, diff --git a/backend/tests/test_data_endpoints.py b/backend/tests/test_data_endpoints.py index 85e35e8b..723c5d0f 100644 --- a/backend/tests/test_data_endpoints.py +++ b/backend/tests/test_data_endpoints.py @@ -93,13 +93,13 @@ def test_plot_data_2d(entry_path): assert time_coordinate["description"] == "Generic time" dim1_coordinate = response_body["data"]["coordinates"][0] - assert dim1_coordinate["name"] == "dim1" + assert dim1_coordinate["name"] == "R" # alias for dim1 assert dim1_coordinate["target"] == "#core_profiles/profiles_2d[:]/ion[:]/temperature" assert dim1_coordinate["shape"] == [5, 3] assert dim1_coordinate["path"] == "#core_profiles/profiles_2d[:]/grid/dim1" dim2_coordinate = response_body["data"]["coordinates"][1] - assert dim2_coordinate["name"] == "dim2" + assert dim2_coordinate["name"] == "Z" # alias for dim2 assert dim2_coordinate["target"] == "#core_profiles/profiles_2d[:]/ion[:]/temperature" assert dim2_coordinate["shape"] == [5, 3] assert dim2_coordinate["path"] == "#core_profiles/profiles_2d[:]/grid/dim2" @@ -135,8 +135,7 @@ def test_plot_data_coordinate_aliases(entry_path): assert response.status_code == 200 dim1_coordinate = response_body["data"]["coordinates"][0] - assert dim1_coordinate["alias"].lower() == "r" - assert dim1_coordinate["unit_alias"] is None + assert dim1_coordinate["name"].lower() == "r" parameters = { "uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[1]/grid/volume_element", @@ -147,5 +146,4 @@ def test_plot_data_coordinate_aliases(entry_path): assert response.status_code == 200 dim1_coordinate = response_body["data"]["coordinates"][0] - assert dim1_coordinate["alias"].lower() == "rho" - assert dim1_coordinate["unit_alias"] is None + assert dim1_coordinate["name"].lower() == "rho"