From c56790f7ae07cf42c863ecb36867dbeb6b245276 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Mon, 3 Aug 2026 16:00:36 +0200 Subject: [PATCH] add support to fill static IDS nodes --- tests/test_exporter.py | 18 +++++++++++ tests/test_waveform.py | 51 ++++++++++++++++++++++++++++++ waveform_editor/export/exporter.py | 9 ++++++ waveform_editor/waveform.py | 37 +++++++++++++++++++--- 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 0b32e634..b6d0357d 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -647,6 +647,24 @@ def test_export_constant(tmp_path): assert np.array_equal(ids.beam[2].phase.angle, [3.3e3] * 3) +def test_export_constant_static_field(tmp_path): + """A constant waveform bound to a static DD node must export its single value.""" + + yaml_str = """ + globals: + dd_version: 4.0.0 + edge_profiles: + edge_profiles/midplane/name: + - {type: constant, value: asdf, duration: 2} + """ + file_path = f"{tmp_path}/test.nc" + times = np.array([0, 1, 2]) + _export_ids(file_path, yaml_str, times) + with imas.DBEntry(file_path, "r", dd_version="4.0.0") as dbentry: + ids = dbentry.get("edge_profiles", autoconvert=False) + assert ids.midplane.name == "asdf" + + def test_export_typed_waveforms(tmp_path): """Check that constant waveforms of each supported value type""" diff --git a/tests/test_waveform.py b/tests/test_waveform.py index 9942af6f..b0525c75 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -381,6 +381,57 @@ def test_dtype_str_dd_path(): assert waveform.annotations +def test_static_0d_dd_path(): + """A constant waveform may fill a static (non time-dependent) 0D DD node.""" + waveform = Waveform( + waveform=[ + {"user_type": "constant", "user_value": "a comment", "line_number": 1} + ], + name="ec_launchers/ids_properties/comment", + dd_version=DD_VERSION, + ) + assert not waveform.annotations + + +def test_static_0d_dd_path_rejects_varying_waveform(): + """A static DD node cannot hold different values at different times""" + waveform = Waveform( + waveform=[ + { + "user_type": "constant", + "user_value": "a", + "user_duration": 1, + "line_number": 1, + }, + { + "user_type": "constant", + "user_value": "b", + "user_duration": 1, + "line_number": 2, + }, + ], + name="ec_launchers/ids_properties/comment", + dd_version=DD_VERSION, + ) + assert waveform.annotations + + +def test_1d_dd_path(): + """Tests 1D waveforms whose coordinate is and isn't time""" + waveform = Waveform( + waveform=[{"user_type": "constant", "user_value": 5, "line_number": 1}], + name="ec_launchers/beam(1)/phase/angle", + dd_version=DD_VERSION, + ) + assert not waveform.annotations + waveform = Waveform( + waveform=[{"user_type": "constant", "user_value": 5, "line_number": 1}], + name="core_profiles/profiles_1d/electrons/temperature", + dd_version=DD_VERSION, + ) + assert waveform.annotations + + def test_no_metadata_allows_any_type(): """A waveform whose path does not resolve to any DD node is not restricted to any particular value type.""" diff --git a/waveform_editor/export/exporter.py b/waveform_editor/export/exporter.py index 3201ed0e..dd0ba6cb 100644 --- a/waveform_editor/export/exporter.py +++ b/waveform_editor/export/exporter.py @@ -232,6 +232,15 @@ def _fill_nodes_recursively(self, node, path, values, path_index=0, fill=True): """ if path_index == len(path.parts): if fill: + if ( + not node.metadata.type.is_dynamic + and isinstance(values, np.ndarray) + and values.ndim > 0 + ): + # A static IDS node must be filled by a single constant tendency, so + # every entry of `values` should be identical. Collapse it to one + assert np.all(values == values[0]) + values = values[0] node.value = values return part = path.parts[path_index] diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 7a7a69b7..1385b567 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -221,16 +221,45 @@ def _validate_value_type(self): self.annotations.add(0, error_msg) return + if self.metadata is None: + return + # If a valid DD path is chosen, check if the value_type matches the DD type - if ( - self.metadata is not None - and self.metadata.data_type not in IDS_DATATYPES[self.value_type] - ): + if self.metadata.data_type not in IDS_DATATYPES[self.value_type]: error_msg = ( "Type is not valid here: this waveform expects a " f"{self.metadata.data_type}.\n" ) self.annotations.add(self.tendencies[0].line_number, error_msg) + return + + if self.metadata.ndim > 1: + error_msg = ( + f"{self.metadata.data_type.name}_{self.metadata.ndim}D quantities are " + "not supported.\n" + ) + self.annotations.add(self.tendencies[0].line_number, error_msg) + return + + if self.metadata.ndim == 1 and not self.metadata.timebasepath: + error_msg = ( + "This 1D quantity's coordinate is not time, so it cannot be filled " + "by a waveform.\n" + ) + self.annotations.add(self.tendencies[0].line_number, error_msg) + return + + # A static DD node cannot hold different values, so it may only be filled + # with a single constant tendency + if not self.metadata.type.is_dynamic and ( + len(self.tendencies) != 1 + or not isinstance(self.tendencies[0], ConstantTendency) + ): + error_msg = ( + "This DD node does not vary in time, so it can only " + "be filled by a single constant tendency.\n" + ) + self.annotations.add(self.tendencies[0].line_number, error_msg) def update_annotations(self, event=None): """Merges the annotations of the individual tendencies into the annotations