Skip to content

Commit 3d22acd

Browse files
authored
Feature/replace dim x with identifier (#86)
1 parent 4ce2c23 commit 3d22acd

4 files changed

Lines changed: 97 additions & 6 deletions

File tree

backend/ibex/data_source/imas_python_source.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,43 @@ def _leaf_node_coordinates_contain_time(self, leaf_node_path: str, coordinates_t
678678

679679
return False
680680

681+
def _generate_grid_quantity_alias(self, grid_node: IDSNumericArray):
682+
"""
683+
Generates alias and unit for selected grid node. Assumes grid_node.name == "dimX" X=(1...N)
684+
:param grid_node: IDSNode (named dimX, X = [1...N])
685+
:return:
686+
"""
687+
result = {"axis_label": None, "unit": None}
688+
if grid_node._parent is None or grid_node._parent._parent is None:
689+
return result
690+
if not re.search(r"dim[1-9]", grid_node.metadata.name):
691+
return result
692+
693+
# assume grid_node is located inside XXX/grid/<node> and grid_type is located in XXX/grid_type
694+
grid_type_index = grid_node._parent._parent.grid_type.index
695+
if grid_type_index == imas.ids_defs.EMPTY_INT:
696+
return result
697+
698+
dim_index = int(grid_node.metadata.name[-1]) - 1 # dim1->0, dim2->1 etc...
699+
# Extract units
700+
try:
701+
units = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).units.split(",")
702+
result["unit"] = units[dim_index]
703+
except (ValueError, KeyError, AttributeError):
704+
result["unit"] = None
705+
706+
# Extract axis labels
707+
try:
708+
axis_labels = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).axis_labels.split(",")
709+
result["axis_label"] = axis_labels[dim_index]
710+
except AttributeError:
711+
description = imas.identifiers.poloidal_plane_coordinates_identifier(grid_type_index).description
712+
match = re.findall(r"(\w+)=(dim[1-9])", description)
713+
axis_labels = {v: k for k, v in match}
714+
result["axis_label"] = axis_labels.get(grid_node.metadata.name, None)
715+
716+
return result
717+
681718
def get_geometry_overlay_nodes(
682719
self,
683720
uri: str,
@@ -982,10 +1019,20 @@ def get_plot_data(self, plot_data_query: PlotDataRequestModel) -> dict:
9821019
except ValueError:
9831020
coord_data_shape = "irregular"
9841021

1022+
coord_name = coord.split("/")[-1]
1023+
axis_label = None
1024+
unit = None
1025+
if re.search(r"dim[1-9]", coord_name):
1026+
labels_dict = self._generate_grid_quantity_alias(first_value)
1027+
axis_label = labels_dict["axis_label"]
1028+
unit = labels_dict["unit"]
1029+
1030+
coord_name = axis_label if axis_label else coord_name
1031+
units = unit if unit else first_value.metadata.units
9851032
c = {
986-
"name": coord.split("/")[-1],
1033+
"name": coord_name,
9871034
"target": f"#{ids}/{target}",
988-
"unit": first_value.metadata.units,
1035+
"unit": units,
9891036
"shape": coord_data_shape, # coord_data could be np.ndarray or list[np.ndarray]
9901037
"downsampled_shape": coord_data_shape,
9911038
"ndim": first_value.metadata.ndim,

backend/ibex/endpoints/schemas/response_data_schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class PlotDataCoordinateModel(BaseModel):
2020
target: str = Field(
2121
description="Which node coordinate is it", examples=["#equilibrium/time_slice[0]/profiles_2d[0]/psi"]
2222
)
23-
unit: str = Field(description="Data units", examples=[""])
23+
unit: str = Field(description="Data units", examples=["m", "mixed"])
2424
shape: list[int] | str = Field(description="Shape of the data", examples=[[129]])
2525
downsampled_shape: list[int] | str = Field(description="Shape of the data after downsampling", examples=[[129]])
2626
ndim: int = Field(description="Number of data dimensions stored in node", examples=[1])

backend/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,16 @@ def entry_path(tmp_path_factory):
9090
)
9191
profiles_2d.grid.dim1 = np.array([0, 1, 2], dtype=float)
9292
profiles_2d.grid.dim2 = np.array([0, 1, 2], dtype=float)
93+
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)
9394
i += 10
9495

9596
# ===== for data smoothing (must be time-based) =====
9697
core_profiles.global_quantities.ip = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
9798

99+
# for coordinate aliases/units
100+
core_profiles.profiles_2d[0].grid_type = 1
101+
core_profiles.profiles_2d[1].grid_type = 2
102+
98103
entry.put(core_profiles)
99104

100105
# ===== for geometry overlay =====

backend/tests/test_data_endpoints.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import pytest
22
import numpy as np
3+
from packaging.version import Version
4+
import imas
5+
6+
_IMAS_GE_2_3 = Version(imas.__version__) >= Version("2.3.0")
37

48

59
def test_status_codes(entry_path):
@@ -123,7 +127,8 @@ def test_plot_data_smoothing_with_wrong_target_node(entry_path):
123127
assert response.status_code == 466
124128

125129

126-
def test_plot_data_2d(entry_path):
130+
@pytest.mark.parametrize("expected_unit", ["m"] if _IMAS_GE_2_3 else ["mixed"])
131+
def test_plot_data_2d(entry_path, expected_unit):
127132
parameters = {
128133
"uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[:]/ion[:]/temperature",
129134
}
@@ -147,13 +152,15 @@ def test_plot_data_2d(entry_path):
147152
assert time_coordinate["description"] == "Generic time"
148153

149154
dim1_coordinate = response_body["data"]["coordinates"][0]
150-
assert dim1_coordinate["name"] == "dim1"
155+
assert dim1_coordinate["name"] == "R" # alias for dim1
156+
assert dim1_coordinate["unit"] == expected_unit
151157
assert dim1_coordinate["target"] == "#core_profiles/profiles_2d[:]/ion[:]/temperature"
152158
assert dim1_coordinate["shape"] == [5, 3]
153159
assert dim1_coordinate["path"] == "#core_profiles/profiles_2d[:]/grid/dim1"
154160

155161
dim2_coordinate = response_body["data"]["coordinates"][1]
156-
assert dim2_coordinate["name"] == "dim2"
162+
assert dim2_coordinate["name"] == "Z" # alias for dim2
163+
assert dim2_coordinate["unit"] == expected_unit
157164
assert dim2_coordinate["target"] == "#core_profiles/profiles_2d[:]/ion[:]/temperature"
158165
assert dim2_coordinate["shape"] == [5, 3]
159166
assert dim2_coordinate["path"] == "#core_profiles/profiles_2d[:]/grid/dim2"
@@ -205,3 +212,35 @@ def test_plot_data_requires_savgol_window_length_and_polyorder(entry_path):
205212
)
206213
assert response.status_code == 422
207214
assert "savgol_smoothing_polyorder is required" in response.text
215+
216+
217+
@pytest.mark.parametrize("expected_unit", [("m", "rad")] if _IMAS_GE_2_3 else [("mixed", "mixed")])
218+
def test_plot_data_coordinate_aliases(entry_path, expected_unit):
219+
220+
parameters = {
221+
"uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[0]/grid/volume_element",
222+
}
223+
response = pytest.test_client.get("/data/plot_data", params=parameters)
224+
response_body = response.json()
225+
226+
assert response.status_code == 200
227+
228+
dim1_coordinate = response_body["data"]["coordinates"][0]
229+
assert dim1_coordinate["name"].lower() == "r"
230+
assert dim1_coordinate["unit"].lower() == expected_unit[0]
231+
232+
parameters = {
233+
"uri": f"imas:hdf5?path={entry_path}#core_profiles/profiles_2d[1]/grid/volume_element",
234+
}
235+
response = pytest.test_client.get("/data/plot_data", params=parameters)
236+
response_body = response.json()
237+
238+
assert response.status_code == 200
239+
240+
dim1_coordinate = response_body["data"]["coordinates"][0]
241+
dim2_coordinate = response_body["data"]["coordinates"][1]
242+
assert dim1_coordinate["name"].lower() == "rho"
243+
assert dim1_coordinate["unit"].lower() == expected_unit[0]
244+
245+
assert dim2_coordinate["name"].lower() == "theta"
246+
assert dim2_coordinate["unit"].lower() == expected_unit[1]

0 commit comments

Comments
 (0)