diff --git a/docs/changes/newsfragments/8570.improved b/docs/changes/newsfragments/8570.improved new file mode 100644 index 00000000000..d4d05674123 --- /dev/null +++ b/docs/changes/newsfragments/8570.improved @@ -0,0 +1,3 @@ +Fixed NetCDF export of measurements without setpoint dependencies, including +scalar and array-valued parameters measured with ``do0d`` or ``dond``. +The exported data retains its shape using parameter-specific dimensions. diff --git a/src/qcodes/dataset/exporters/export_to_xarray.py b/src/qcodes/dataset/exporters/export_to_xarray.py index 1877849000b..26507bcba24 100644 --- a/src/qcodes/dataset/exporters/export_to_xarray.py +++ b/src/qcodes/dataset/exporters/export_to_xarray.py @@ -268,6 +268,29 @@ def _xarray_data_set_direct( shape = sub_dict[name].shape expected_size = prod(shape) + if not deps: + dimensions = tuple(f"{name}_dim_{axis}" for axis in range(len(shape))) + + def reshape_without_dependencies(data: npt.NDArray) -> npt.NDArray: + if data.size != expected_size: + raise ValueError( + f"Parameter contains {data.size} values, " + f"but {expected_size} were expected" + ) + return data.reshape(shape) + + independent_data_vars: dict[str, tuple[tuple[str, ...], npt.NDArray]] = { + name: (dimensions, reshape_without_dependencies(sub_dict[name])) + } + for inf in inferred: + if inf.name in sub_dict: + independent_data_vars[inf.name] = ( + dimensions, + reshape_without_dependencies(sub_dict[inf.name]), + ) + + return xr.Dataset(independent_data_vars) + if len(deps) != len(shape): raise ValueError( f"Parameter {name!r} has shape {shape}, but has {len(deps)} dependencies" diff --git a/tests/dataset/dond/test_do0d.py b/tests/dataset/dond/test_do0d.py index f8065f2f323..a7e61e12f9e 100644 --- a/tests/dataset/dond/test_do0d.py +++ b/tests/dataset/dond/test_do0d.py @@ -2,6 +2,7 @@ import matplotlib.axes import numpy as np import pytest +import xarray as xr from hypothesis import HealthCheck, given, settings from qcodes import config, validators @@ -95,6 +96,38 @@ def test_do0d_output_data(_param) -> None: assert loaded_data == np.array([_param.get()]) +@pytest.mark.usefixtures("experiment") +def test_do0d_export_to_netcdf(_param, tmp_path) -> None: + dataset = do0d(_param, do_plot=False)[0] + + dataset.export(export_type="netcdf", path=tmp_path) + + export_path = dataset.export_info.export_paths["nc"] + with xr.open_dataset(export_path) as exported_dataset: + assert exported_dataset[_param.name].dims == (f"{_param.name}_dim_0",) + np.testing.assert_array_equal( + exported_dataset[_param.name].values, np.array([_param.get()]) + ) + + +@pytest.mark.usefixtures("experiment") +def test_do0d_array_export_to_netcdf(tmp_path) -> None: + param = ArrayshapedParam( + name="paramwitharrayval", vals=validators.Arrays(shape=(10,)) + ) + dataset = do0d(param, do_plot=False)[0] + expected_data = dataset.get_parameter_data()[param.name][param.name] + + dataset.export(export_type="netcdf", path=tmp_path) + + export_path = dataset.export_info.export_paths["nc"] + with xr.open_dataset(export_path) as exported_dataset: + assert exported_dataset[param.name].dims == (f"{param.name}_dim_0",) + np.testing.assert_array_equal( + exported_dataset[param.name].values, expected_data + ) + + @pytest.mark.usefixtures("experiment") @pytest.mark.parametrize( "multiparamtype", diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index 217dc94c99e..7183f6fa6a0 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -194,6 +194,16 @@ def _make_direct_export_dataset(experiment: Experiment) -> DataSet: return dataset +@pytest.fixture(name="independent_export_dataset") +def _make_independent_export_dataset(experiment: Experiment) -> DataSet: + dataset = new_data_set("independent_export_dataset") + signalparam = ParamSpecBase("signal", "numeric") + inferredparam = ParamSpecBase("inferred", "numeric") + idps = InterDependencies_(inferences={inferredparam: (signalparam,)}) + dataset.set_interdependencies(idps) + return dataset + + @pytest.fixture(name="mock_dataset_grid_incomplete") def _make_mock_dataset_grid_incomplete(experiment: Experiment) -> DataSet: dataset = new_data_set("dataset") @@ -1756,6 +1766,51 @@ def test_xarray_data_set_direct_skips_missing_inferred_data( assert set(xarray_dataset.data_vars) == {"signal"} +@pytest.mark.parametrize("shape", [(), (1,), (4,), (2, 2)]) +@pytest.mark.parametrize("include_inferred", [True, False]) +def test_xarray_data_set_direct_without_dependencies( + independent_export_dataset: DataSet, + shape: tuple[int, ...], + include_inferred: bool, +) -> None: + signal = np.arange(np.prod(shape, dtype=int)).reshape(shape) + data = {"signal": signal} + if include_inferred: + data["inferred"] = (signal + 10).ravel() + + xarray_dataset = _xarray_data_set_direct(independent_export_dataset, "signal", data) + + dimensions = tuple(f"signal_dim_{axis}" for axis in range(len(shape))) + assert set(xarray_dataset.coords) == set() + assert xarray_dataset["signal"].dims == dimensions + assert xarray_dataset["signal"].shape == shape + assert_array_equal(xarray_dataset["signal"].values, signal) + if include_inferred: + assert set(xarray_dataset.data_vars) == {"signal", "inferred"} + assert xarray_dataset["inferred"].dims == dimensions + assert xarray_dataset["inferred"].shape == shape + assert_array_equal(xarray_dataset["inferred"].values, signal + 10) + else: + assert set(xarray_dataset.data_vars) == {"signal"} + + +@pytest.mark.parametrize("inferred_size", [0, 3, 5]) +def test_xarray_data_set_direct_without_dependencies_rejects_invalid_inferred_size( + independent_export_dataset: DataSet, + inferred_size: int, +) -> None: + data = { + "signal": np.arange(4).reshape(2, 2), + "inferred": np.arange(inferred_size), + } + + with pytest.raises( + ValueError, + match=f"^Parameter contains {inferred_size} values, but 4 were expected$", + ): + _xarray_data_set_direct(independent_export_dataset, "signal", data) + + def test_multi_index_options_incomplete_grid( mock_dataset_grid_incomplete: DataSet, ) -> None: