From 175b160a0eb82673668b67816f1046fd426185f2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 15 Dec 2025 11:31:54 +0100 Subject: [PATCH 1/5] Add failing test based on the bug report in 7717 --- ...st_parameter_with_setpoints_has_control.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/dataset/test_parameter_with_setpoints_has_control.py diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py new file mode 100644 index 000000000000..9892f39ce5b7 --- /dev/null +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +import numpy as np + +from qcodes.dataset import Measurement +from qcodes.parameters import ManualParameter, ParameterWithSetpoints +from qcodes.validators import Arrays + +if TYPE_CHECKING: + from qcodes.dataset.experiment_container import Experiment + + +def test_parameter_with_setpoints_has_control(experiment: "Experiment"): + class MySp(ParameterWithSetpoints): + def unpack_self(self, value): + res = super().unpack_self(value) + res.append((p1, p1())) + return res + + mp = ManualParameter("mp", vals=Arrays(shape=(10,)), initial_value=np.arange(10)) + p1 = ParameterWithSetpoints( + "p1", vals=Arrays(shape=(10,)), setpoints=(mp,), set_cmd=None + ) + p2 = MySp("p2", vals=Arrays(shape=(10,)), setpoints=(mp,), set_cmd=None) + p2.has_control_of.add(p1) + + p1(np.linspace(-1, 1, 10)) + p2(np.random.randn(10)) + + meas = Measurement() + meas.register_parameter(p2) + with meas.run() as ds: + ds.add_result((p2, p2())) + + xds = ds.dataset.to_xarray_dataset() # does not unravel to grid + + assert ( + list(xds.sizes.keys()) == ["mp"] + ) # without p1 this correctly has mp as the only dim, with p1 this is turned into a generic 'index' dim From cc7a8662b699c7abbad21c4a53fc380f763d6876 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 18 Mar 2026 09:52:20 +0100 Subject: [PATCH 2/5] Fix 7717 --- src/qcodes/dataset/descriptions/dependencies.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/qcodes/dataset/descriptions/dependencies.py b/src/qcodes/dataset/descriptions/dependencies.py index 3ec82477dc05..43c374c57036 100644 --- a/src/qcodes/dataset/descriptions/dependencies.py +++ b/src/qcodes/dataset/descriptions/dependencies.py @@ -287,6 +287,16 @@ def top_level_parameters(self) -> tuple[ParamSpecBase, ...]: for node_id, in_degree in self._dependency_subgraph.in_degree if in_degree == 0 } + # Parameters that are inferred from other parameters (have outgoing + # edges in the inference subgraph) should not be independent top-level + # parameters, since their data is part of the tree of the parameter + # they are inferred from. + parameters_inferred_from_others = { + self._node_to_paramspec(node_id) + for node_id, out_degree in self._inference_subgraph.out_degree + if out_degree > 0 + } + dependency_top_level = dependency_top_level - parameters_inferred_from_others standalone_top_level = { self._node_to_paramspec(node_id) for node_id, degree in self._graph.degree From 7ce13b2d65a1deb246f019febc2596c63d77c77f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 18 Mar 2026 09:56:04 +0100 Subject: [PATCH 3/5] Remove outdated comments --- tests/dataset/test_parameter_with_setpoints_has_control.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 9892f39ce5b7..7bd2bfc41fc1 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -32,8 +32,6 @@ def unpack_self(self, value): with meas.run() as ds: ds.add_result((p2, p2())) - xds = ds.dataset.to_xarray_dataset() # does not unravel to grid + xds = ds.dataset.to_xarray_dataset() - assert ( - list(xds.sizes.keys()) == ["mp"] - ) # without p1 this correctly has mp as the only dim, with p1 this is turned into a generic 'index' dim + assert list(xds.sizes.keys()) == ["mp"] From 6de4b2cb2d8a232f331e7155f0a29293bae633fc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 18 Mar 2026 10:06:23 +0100 Subject: [PATCH 4/5] Improve test for 7717 --- ...st_parameter_with_setpoints_has_control.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 7bd2bfc41fc1..c939171f29c9 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING import numpy as np +import numpy.testing as npt from qcodes.dataset import Measurement from qcodes.parameters import ManualParameter, ParameterWithSetpoints @@ -17,21 +18,51 @@ def unpack_self(self, value): res.append((p1, p1())) return res - mp = ManualParameter("mp", vals=Arrays(shape=(10,)), initial_value=np.arange(10)) + mp_data = np.arange(10) + p1_data = np.linspace(-1, 1, 10) + + mp = ManualParameter("mp", vals=Arrays(shape=(10,)), initial_value=mp_data) p1 = ParameterWithSetpoints( "p1", vals=Arrays(shape=(10,)), setpoints=(mp,), set_cmd=None ) p2 = MySp("p2", vals=Arrays(shape=(10,)), setpoints=(mp,), set_cmd=None) p2.has_control_of.add(p1) - p1(np.linspace(-1, 1, 10)) - p2(np.random.randn(10)) + p1(p1_data) + p2_data = np.random.randn(10) + p2(p2_data) meas = Measurement() meas.register_parameter(p2) + + # Only p2 should be top-level; p1 is inferred from p2 + interdeps = meas._interdeps + top_level_names = [p.name for p in interdeps.top_level_parameters] + assert top_level_names == ["p2"] + with meas.run() as ds: ds.add_result((p2, p2())) + # Verify raw parameter data has exactly one row per parameter + raw_data = ds.dataset.get_parameter_data() + assert list(raw_data.keys()) == ["p2"], "Only p2 should be a top-level result" + for name, arr in raw_data["p2"].items(): + assert arr.shape == (1, 10), ( + f"Expected shape (1, 10) for {name}, got {arr.shape}" + ) + xds = ds.dataset.to_xarray_dataset() + # mp should be the only dimension (not a generic 'index') assert list(xds.sizes.keys()) == ["mp"] + assert xds.sizes["mp"] == 10 + + # mp values used as coordinate axis + npt.assert_array_equal(xds.coords["mp"].values, mp_data) + + # p2 is the primary data variable with correct values + assert "p2" in xds.data_vars + npt.assert_array_almost_equal(xds["p2"].values, p2_data) + + # p1 data is retrievable from the raw parameter data + npt.assert_array_almost_equal(raw_data["p2"]["p1"].ravel(), p1_data) From 3c254d8fad09c5ee28fdf58a7522bfca2b01ea3e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 18 Mar 2026 13:04:40 +0100 Subject: [PATCH 5/5] Add infeered data to exported dataset --- .../dataset/exporters/export_to_xarray.py | 65 ++++++++++++++++++- ...st_parameter_with_setpoints_has_control.py | 6 +- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/qcodes/dataset/exporters/export_to_xarray.py b/src/qcodes/dataset/exporters/export_to_xarray.py index 03a79b51b397..dfa32f8d52e7 100644 --- a/src/qcodes/dataset/exporters/export_to_xarray.py +++ b/src/qcodes/dataset/exporters/export_to_xarray.py @@ -6,6 +6,7 @@ from math import prod from typing import TYPE_CHECKING, Literal +import numpy as np from packaging import version as p_version from qcodes.dataset.linked_datasets.links import links_to_str @@ -61,6 +62,56 @@ def _calculate_index_shape(idx: pd.Index | pd.MultiIndex) -> dict[Hashable, int] return expanded_shape +def _add_inferred_data_vars( + dataset: DataSetProtocol, + name: str, + sub_dict: Mapping[str, npt.NDArray], + xr_dataset: xr.Dataset, +) -> xr.Dataset: + """Add inferred parameters as data variables to an xarray dataset. + + Parameters that are inferred from the top-level measurement parameter + and present in sub_dict but not yet in the dataset are added as data + variables along the existing dimensions. + """ + + interdeps = dataset.description.interdeps + meas_paramspec = interdeps.graph.nodes[name]["value"] + _, deps, inferred = interdeps.all_parameters_in_tree_by_group(meas_paramspec) + + dep_names = {dep.name for dep in deps} + dims = tuple(d for d in xr_dataset.dims) + + for inf in inferred: + if inf.name in dep_names: + continue + if inf.name in xr_dataset: + continue + if inf.name not in sub_dict: + continue + + inf_data = sub_dict[inf.name] + if inf_data.dtype == np.dtype("O"): + try: + flat = np.concatenate(inf_data) + except ValueError: + flat = inf_data.ravel() + else: + flat = inf_data.ravel() + + # Only add if the data length matches the existing dataset size + expected_size = 1 + for d in dims: + expected_size *= xr_dataset.sizes[d] + if flat.shape[0] == expected_size: + xr_dataset[inf.name] = ( + dims, + flat.reshape(tuple(xr_dataset.sizes[d] for d in dims)), + ) + + return xr_dataset + + def _load_to_xarray_dataset_dict_no_metadata( dataset: DataSetProtocol, datadict: Mapping[str, Mapping[str, npt.NDArray]], @@ -100,7 +151,9 @@ def _load_to_xarray_dataset_dict_no_metadata( interdeps=dataset.description.interdeps, dependent_parameter=name, ).to_xarray() - xr_dataset_dict[name] = xr_dataset + xr_dataset_dict[name] = _add_inferred_data_vars( + dataset, name, sub_dict, xr_dataset + ) elif index_is_unique: df = _data_to_dataframe( sub_dict, @@ -108,9 +161,12 @@ def _load_to_xarray_dataset_dict_no_metadata( interdeps=dataset.description.interdeps, dependent_parameter=name, ) - xr_dataset_dict[name] = _xarray_data_set_from_pandas_multi_index( + xr_dataset = _xarray_data_set_from_pandas_multi_index( dataset, use_multi_index, name, df, index ) + xr_dataset_dict[name] = _add_inferred_data_vars( + dataset, name, sub_dict, xr_dataset + ) else: df = _data_to_dataframe( sub_dict, @@ -118,7 +174,10 @@ def _load_to_xarray_dataset_dict_no_metadata( interdeps=dataset.description.interdeps, dependent_parameter=name, ) - xr_dataset_dict[name] = df.reset_index().to_xarray() + xr_dataset = df.reset_index().to_xarray() + xr_dataset_dict[name] = _add_inferred_data_vars( + dataset, name, sub_dict, xr_dataset + ) return xr_dataset_dict diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index c939171f29c9..a2170af65192 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -64,5 +64,9 @@ def unpack_self(self, value): assert "p2" in xds.data_vars npt.assert_array_almost_equal(xds["p2"].values, p2_data) - # p1 data is retrievable from the raw parameter data + # p1 is included as a data variable (inferred from p2) with correct values + assert "p1" in xds.data_vars + npt.assert_array_almost_equal(xds["p1"].values, p1_data) + + # p1 data is also retrievable from the raw parameter data npt.assert_array_almost_equal(raw_data["p2"]["p1"].ravel(), p1_data)