From 409cc3fc60b8eb5d4e6c5c78c1a499a20c515bf8 Mon Sep 17 00:00:00 2001 From: xjules Date: Mon, 14 Oct 2024 09:56:45 +0200 Subject: [PATCH] Add run experiment with design matrix to ensemble experiment panel - Prefil active realization box with realizations from design matrix - Use design_matrix parameters in ensemble experiment - add test run cli with design matrix and poly example - add test that save parameters internalize DataFrame parameters in the storage - add merge function to merge design parameters with existing parameters --- src/ert/config/design_matrix.py | 61 ++++++++++-- src/ert/enkf_main.py | 34 +++++-- .../simulation/ensemble_experiment_panel.py | 9 +- src/ert/run_models/ensemble_experiment.py | 33 ++++++- test-data/ert/poly_design/.ert_runpath_list | 10 ++ test-data/ert/poly_design/POLY_EVAL | 1 + test-data/ert/poly_design/coeff_priors | 3 + test-data/ert/poly_design/observations | 5 + test-data/ert/poly_design/poly.ert | 10 ++ test-data/ert/poly_design/poly_design.xlsx | Bin 0 -> 5558 bytes test-data/ert/poly_design/poly_eval.py | 18 ++++ test-data/ert/poly_design/poly_obs_data.txt | 5 + .../cli/analysis/test_design_matrix.py | 93 ++++++++++++++++++ .../gui/simulation/test_run_dialog.py | 20 +++- .../test_design_matrix.py | 70 +++++++++++++ tests/ert/unit_tests/test_libres_facade.py | 55 ++++++++++- 16 files changed, 405 insertions(+), 22 deletions(-) create mode 100644 test-data/ert/poly_design/.ert_runpath_list create mode 100644 test-data/ert/poly_design/POLY_EVAL create mode 100644 test-data/ert/poly_design/coeff_priors create mode 100644 test-data/ert/poly_design/observations create mode 100644 test-data/ert/poly_design/poly.ert create mode 100644 test-data/ert/poly_design/poly_design.xlsx create mode 100755 test-data/ert/poly_design/poly_eval.py create mode 100644 test-data/ert/poly_design/poly_obs_data.txt create mode 100644 tests/ert/ui_tests/cli/analysis/test_design_matrix.py diff --git a/src/ert/config/design_matrix.py b/src/ert/config/design_matrix.py index 9ef318e9c9f..3eb29869bb6 100644 --- a/src/ert/config/design_matrix.py +++ b/src/ert/config/design_matrix.py @@ -11,15 +11,10 @@ from ert.config.gen_kw_config import GenKwConfig, TransformFunctionDefinition from ._option_dict import option_dict -from .parsing import ( - ConfigValidationError, - ErrorInfo, -) +from .parsing import ConfigValidationError, ErrorInfo if TYPE_CHECKING: - from ert.config import ( - ParameterConfig, - ) + from ert.config import ParameterConfig DESIGN_MATRIX_GROUP = "DESIGN_MATRIX" @@ -74,6 +69,58 @@ def from_config_list(cls, config_list: List[str]) -> "DesignMatrix": default_sheet=default_sheet, ) + def merge_with_existing_parameters( + self, existing_parameters: List[ParameterConfig] + ) -> tuple[List[ParameterConfig], ParameterConfig | None]: + """ + This method merges the design matrix parameters with the existing parameters and + returns the new list of existing parameters, wherein we drop GEN_KW group having a full overlap with the design matrix group. + GEN_KW group that was dropped will acquire a new name from the design matrix group. + Additionally, the ParameterConfig which is the design matrix group is returned separately. + + Args: + existing_parameters (List[ParameterConfig]): List of existing parameters + + Raises: + ConfigValidationError: If there is a partial overlap between the design matrix group and any existing GEN_KW group + + Returns: + tuple[List[ParameterConfig], ParameterConfig]: List of existing parameters and the dedicated design matrix group + """ + if self.parameter_configuration is None: + self.read_design_matrix() + + if self.parameter_configuration is None or not isinstance( + self.parameter_configuration[DESIGN_MATRIX_GROUP], GenKwConfig + ): + return existing_parameters, None + + new_param_config: List[ParameterConfig] = [] + + design_parameter_group = self.parameter_configuration[DESIGN_MATRIX_GROUP] + design_keys = [] + if isinstance(design_parameter_group, GenKwConfig): + design_keys = design_parameter_group.getKeyWords() + + design_group_added = False + for genkw_group in existing_parameters: + if not isinstance(genkw_group, GenKwConfig): + new_param_config += [genkw_group] + continue + existing_keys = genkw_group.getKeyWords() + if set(existing_keys) == set(design_keys): + design_parameter_group.name = genkw_group.name + design_group_added = True + elif set(design_keys) & set(existing_keys): + raise ConfigValidationError( + "Overlapping parameter names found in design matrix!" + ) + else: + new_param_config += [genkw_group] + if not design_group_added: + new_param_config += [design_parameter_group] + return new_param_config, design_parameter_group + def read_design_matrix( self, ) -> None: diff --git a/src/ert/enkf_main.py b/src/ert/enkf_main.py index 5e776a548d9..2dfce6bca45 100644 --- a/src/ert/enkf_main.py +++ b/src/ert/enkf_main.py @@ -19,6 +19,8 @@ ) import orjson +import pandas as pd +import xarray as xr from numpy.random import SeedSequence from ert.config.ert_config import forward_model_data_to_json @@ -26,13 +28,8 @@ from ert.config.model_config import ModelConfig from ert.substitutions import Substitutions -from .config import ( - ExtParamConfig, - Field, - GenKwConfig, - ParameterConfig, - SurfaceConfig, -) +from .config import ExtParamConfig, Field, GenKwConfig, ParameterConfig, SurfaceConfig +from .config.design_matrix import DESIGN_MATRIX_GROUP from .run_arg import RunArg from .runpaths import Runpaths @@ -162,6 +159,29 @@ def _seed_sequence(seed: Optional[int]) -> int: return int_seed +def save_design_matrix_to_ensemble( + design_matrix_df: pd.DataFrame, + ensemble: Ensemble, + active_realizations: Iterable[int], + design_group_name: str = DESIGN_MATRIX_GROUP, +) -> None: + assert not design_matrix_df.empty + for realization_nr in active_realizations: + row = design_matrix_df.loc[realization_nr][DESIGN_MATRIX_GROUP] + ds = xr.Dataset( + { + "values": ("names", list(row.values)), + "transformed_values": ("names", list(row.values)), + "names": list(row.keys()), + } + ) + ensemble.save_parameters( + design_group_name, + realization_nr, + ds, + ) + + def sample_prior( ensemble: Ensemble, active_realizations: Iterable[int], diff --git a/src/ert/gui/simulation/ensemble_experiment_panel.py b/src/ert/gui/simulation/ensemble_experiment_panel.py index dd2cf2e513e..417f97c057c 100644 --- a/src/ert/gui/simulation/ensemble_experiment_panel.py +++ b/src/ert/gui/simulation/ensemble_experiment_panel.py @@ -15,7 +15,7 @@ from ert.gui.tools.design_matrix.design_matrix_panel import DesignMatrixPanel from ert.mode_definitions import ENSEMBLE_EXPERIMENT_MODE from ert.run_models import EnsembleExperiment -from ert.validation import RangeStringArgument +from ert.validation import ActiveRange, RangeStringArgument from ert.validation.proper_name_argument import ExperimentValidation, ProperNameArgument from .experiment_config_panel import ExperimentConfigPanel @@ -85,6 +85,13 @@ def __init__( design_matrix = analysis_config.design_matrix if design_matrix is not None: + if design_matrix.design_matrix_df is None: + design_matrix.read_design_matrix() + + if design_matrix.active_realizations: + self._active_realizations_field.setText( + ActiveRange(design_matrix.active_realizations).rangestring + ) show_dm_param_button = QPushButton("Show parameters") show_dm_param_button.setObjectName("show-dm-parameters") show_dm_param_button.setMinimumWidth(50) diff --git a/src/ert/run_models/ensemble_experiment.py b/src/ert/run_models/ensemble_experiment.py index 13d8a3a8411..b001e93ccc2 100644 --- a/src/ert/run_models/ensemble_experiment.py +++ b/src/ert/run_models/ensemble_experiment.py @@ -6,7 +6,7 @@ import numpy as np -from ert.enkf_main import sample_prior +from ert.enkf_main import sample_prior, save_design_matrix_to_ensemble from ert.ensemble_evaluator import EvaluatorServerConfig from ert.storage import Ensemble, Experiment, Storage from ert.trace import tracer @@ -63,10 +63,26 @@ def run_experiment( restart: bool = False, ) -> None: self.log_at_startup() + # If design matrix is present, we try to merge design matrix parameters + # to the experiment parameters and set new active realizations + parameters_config = self.ert_config.ensemble_config.parameter_configuration + design_matrix = self.ert_config.analysis_config.design_matrix + design_matrix_group = None + if design_matrix is not None: + parameters_config, design_matrix_group = ( + design_matrix.merge_with_existing_parameters(parameters_config) + ) + + assert design_matrix.active_realizations is not None + self.active_realizations = design_matrix.active_realizations if not restart: self.experiment = self._storage.create_experiment( name=self.experiment_name, - parameters=self.ert_config.ensemble_config.parameter_configuration, + parameters=( + [*parameters_config, design_matrix_group] + if design_matrix_group is not None + else parameters_config + ), observations=self.ert_config.observations, responses=self.ert_config.ensemble_config.response_configuration, ) @@ -89,12 +105,25 @@ def run_experiment( np.array(self.active_realizations, dtype=bool), ensemble=self.ensemble, ) + sample_prior( self.ensemble, np.where(self.active_realizations)[0], random_seed=self.random_seed, ) + if ( + design_matrix_group is not None + and design_matrix is not None + and design_matrix.design_matrix_df is not None + ): + save_design_matrix_to_ensemble( + design_matrix.design_matrix_df, + self.ensemble, + np.where(self.active_realizations)[0], + design_matrix_group.name, + ) + self._evaluate_and_postprocess( run_args, self.ensemble, diff --git a/test-data/ert/poly_design/.ert_runpath_list b/test-data/ert/poly_design/.ert_runpath_list new file mode 100644 index 00000000000..0e20a7e9aa6 --- /dev/null +++ b/test-data/ert/poly_design/.ert_runpath_list @@ -0,0 +1,10 @@ +000 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-0/iter-0 poly.ert-0 000 +001 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-1/iter-0 poly.ert-1 000 +002 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-2/iter-0 poly.ert-2 000 +003 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-3/iter-0 poly.ert-3 000 +004 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-4/iter-0 poly.ert-4 000 +005 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-5/iter-0 poly.ert-5 000 +006 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-6/iter-0 poly.ert-6 000 +007 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-7/iter-0 poly.ert-7 000 +008 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-8/iter-0 poly.ert-8 000 +009 /data/workspace/ert/test-data/ert/poly_design/poly_out/realization-9/iter-0 poly.ert-9 000 diff --git a/test-data/ert/poly_design/POLY_EVAL b/test-data/ert/poly_design/POLY_EVAL new file mode 100644 index 00000000000..8c0137b18c5 --- /dev/null +++ b/test-data/ert/poly_design/POLY_EVAL @@ -0,0 +1 @@ +EXECUTABLE poly_eval.py diff --git a/test-data/ert/poly_design/coeff_priors b/test-data/ert/poly_design/coeff_priors new file mode 100644 index 00000000000..32eac89cf81 --- /dev/null +++ b/test-data/ert/poly_design/coeff_priors @@ -0,0 +1,3 @@ +a UNIFORM 0 1 +b UNIFORM 0 2 +c UNIFORM 0 5 diff --git a/test-data/ert/poly_design/observations b/test-data/ert/poly_design/observations new file mode 100644 index 00000000000..942197b60c4 --- /dev/null +++ b/test-data/ert/poly_design/observations @@ -0,0 +1,5 @@ +GENERAL_OBSERVATION POLY_OBS { + DATA = POLY_RES; + INDEX_LIST = 0,2,4,6,8; + OBS_FILE = poly_obs_data.txt; +}; diff --git a/test-data/ert/poly_design/poly.ert b/test-data/ert/poly_design/poly.ert new file mode 100644 index 00000000000..467add55731 --- /dev/null +++ b/test-data/ert/poly_design/poly.ert @@ -0,0 +1,10 @@ +QUEUE_OPTION LOCAL MAX_RUNNING 10 +RUNPATH poly_out/realization-/iter- +OBS_CONFIG observations +NUM_REALIZATIONS 10 +MIN_REALIZATIONS 1 +GEN_DATA POLY_RES RESULT_FILE:poly.out +DESIGN_MATRIX poly_design.xlsx DESIGN_SHEET:DesignSheet01 DEFAULT_SHEET:DefaultSheet +GEN_KW COEFFS coeff_priors +INSTALL_JOB poly_eval POLY_EVAL +FORWARD_MODEL poly_eval diff --git a/test-data/ert/poly_design/poly_design.xlsx b/test-data/ert/poly_design/poly_design.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3f94bdbe72f7f69dc264be67d3cb0812d85e4fbb GIT binary patch literal 5558 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 DFoms* literal 0 HcmV?d00001 diff --git a/test-data/ert/poly_design/poly_eval.py b/test-data/ert/poly_design/poly_eval.py new file mode 100755 index 00000000000..200f4bcd99f --- /dev/null +++ b/test-data/ert/poly_design/poly_eval.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import json + + +def _load_coeffs(filename): + with open(filename, encoding="utf-8") as f: + return json.load(f)["COEFFS"] + + +def _evaluate(coeffs, x): + return coeffs["a"] * x**2 + coeffs["b"] * x + coeffs["c"] + + +if __name__ == "__main__": + coeffs = _load_coeffs("parameters.json") + output = [_evaluate(coeffs, x) for x in range(10)] + with open("poly.out", "w", encoding="utf-8") as f: + f.write("\n".join(map(str, output))) diff --git a/test-data/ert/poly_design/poly_obs_data.txt b/test-data/ert/poly_design/poly_obs_data.txt new file mode 100644 index 00000000000..930111cada1 --- /dev/null +++ b/test-data/ert/poly_design/poly_obs_data.txt @@ -0,0 +1,5 @@ +2.1457049781272213 0.6 +8.769219841380755 1.4 +12.388014786122742 3.0 +25.600464531354252 5.4 +42.35204755970952 8.6 diff --git a/tests/ert/ui_tests/cli/analysis/test_design_matrix.py b/tests/ert/ui_tests/cli/analysis/test_design_matrix.py new file mode 100644 index 00000000000..a0c994ef169 --- /dev/null +++ b/tests/ert/ui_tests/cli/analysis/test_design_matrix.py @@ -0,0 +1,93 @@ +import os +import stat +from textwrap import dedent + +import numpy as np +import pandas as pd +import pytest + +from ert.config import ErtConfig +from ert.mode_definitions import ENSEMBLE_EXPERIMENT_MODE +from ert.storage import open_storage +from tests.ert.ui_tests.cli.run_cli import run_cli + + +@pytest.mark.usefixtures("copy_poly_case") +def test_run_poly_example_with_design_matrix(): + design_matrix = "poly_design.xlsx" + num_realizations = 10 + a_values = list(range(num_realizations)) + design_matrix_df = pd.DataFrame( + { + "REAL": list(range(num_realizations)), + "a": a_values, + } + ) + default_sheet_df = pd.DataFrame([["b", 1], ["c", 2]]) + with pd.ExcelWriter(design_matrix) as xl_write: + design_matrix_df.to_excel(xl_write, index=False, sheet_name="DesignSheet01") + default_sheet_df.to_excel( + xl_write, index=False, sheet_name="DefaultSheet", header=False + ) + + with open("poly.ert", "w", encoding="utf-8") as fout: + fout.write( + dedent( + """\ + QUEUE_OPTION LOCAL MAX_RUNNING 10 + RUNPATH poly_out/realization-/iter- + NUM_REALIZATIONS 10 + MIN_REALIZATIONS 1 + GEN_DATA POLY_RES RESULT_FILE:poly.out + DESIGN_MATRIX poly_design.xlsx DESIGN_SHEET:DesignSheet01 DEFAULT_SHEET:DefaultSheet + INSTALL_JOB poly_eval POLY_EVAL + FORWARD_MODEL poly_eval + """ + ) + ) + + with open("poly_eval.py", "w", encoding="utf-8") as f: + f.write( + dedent( + """\ + #!/usr/bin/env python + import numpy as np + import sys + import json + + def _load_coeffs(filename): + with open(filename, encoding="utf-8") as f: + return json.load(f)["DESIGN_MATRIX"] + + def _evaluate(coeffs, x): + return coeffs["a"] * x**2 + coeffs["b"] * x + coeffs["c"] + + if __name__ == "__main__": + coeffs = _load_coeffs("parameters.json") + output = [_evaluate(coeffs, x) for x in range(10)] + with open("poly.out", "w", encoding="utf-8") as f: + f.write("\\n".join(map(str, output))) + """ + ) + ) + os.chmod( + "poly_eval.py", + os.stat("poly_eval.py").st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + ) + + run_cli( + ENSEMBLE_EXPERIMENT_MODE, + "--disable-monitor", + "poly.ert", + "--experiment-name", + "test-experiment", + ) + storage_path = ErtConfig.from_file("poly.ert").ens_path + with open_storage(storage_path) as storage: + experiment = storage.get_experiment_by_name("test-experiment") + params = experiment.get_ensemble_by_name("default").load_parameters( + "DESIGN_MATRIX" + )["values"] + np.testing.assert_array_equal(params[:, 0], a_values) + np.testing.assert_array_equal(params[:, 1], 10 * [1]) + np.testing.assert_array_equal(params[:, 2], 10 * [2]) diff --git a/tests/ert/unit_tests/gui/simulation/test_run_dialog.py b/tests/ert/unit_tests/gui/simulation/test_run_dialog.py index 42339b215d0..d48c2408515 100644 --- a/tests/ert/unit_tests/gui/simulation/test_run_dialog.py +++ b/tests/ert/unit_tests/gui/simulation/test_run_dialog.py @@ -2,6 +2,7 @@ from queue import SimpleQueue from unittest.mock import MagicMock, Mock, patch +import pandas as pd import pytest from pytestqt.qtbot import QtBot from qtpy import QtWidgets @@ -705,15 +706,26 @@ def test_that_stdout_and_stderr_buttons_react_to_file_content( def test_that_design_matrix_show_parameters_button_is_visible( design_matrix_entry, qtbot: QtBot, storage ): - xls_filename = "design_matrix.xls" - with open(f"{xls_filename}", "w", encoding="utf-8"): - pass + xls_filename = "design_matrix.xlsx" + design_matrix_df = pd.DataFrame( + { + "REAL": list(range(3)), + "a": [0, 1, 2], + } + ) + default_sheet_df = pd.DataFrame([["b", 1], ["c", 2]]) + with pd.ExcelWriter(xls_filename) as xl_write: + design_matrix_df.to_excel(xl_write, index=False, sheet_name="DesignSheet01") + default_sheet_df.to_excel( + xl_write, index=False, sheet_name="DefaultSheet", header=False + ) + config_file = "minimal_config.ert" with open(config_file, "w", encoding="utf-8") as f: f.write("NUM_REALIZATIONS 1") if design_matrix_entry: f.write( - f"\nDESIGN_MATRIX {xls_filename} DESIGN_SHEET:DesignSheet01 DEFAULT_SHEET:DefaultValues" + f"\nDESIGN_MATRIX {xls_filename} DESIGN_SHEET:DesignSheet01 DEFAULT_SHEET:DefaultSheet" ) args_mock = Mock() diff --git a/tests/ert/unit_tests/sensitivity_analysis/test_design_matrix.py b/tests/ert/unit_tests/sensitivity_analysis/test_design_matrix.py index 2326f76bc3a..fa7c45f3592 100644 --- a/tests/ert/unit_tests/sensitivity_analysis/test_design_matrix.py +++ b/tests/ert/unit_tests/sensitivity_analysis/test_design_matrix.py @@ -3,6 +3,76 @@ import pytest from ert.config.design_matrix import DESIGN_MATRIX_GROUP, DesignMatrix +from ert.config.gen_kw_config import GenKwConfig, TransformFunctionDefinition + + +@pytest.mark.parametrize( + "parameters, error_msg", + [ + pytest.param( + ["a", "b"], + "", + id="genkw_replaced", + ), + pytest.param( + ["a"], + "Overlapping parameter names found in design matrix!", + id="error", + ), + pytest.param( + [], + "", + id="DESIGN_MATRIX_GROUP", + ), + ], +) +def test_read_and_merge_with_existing_parameters(tmp_path, parameters, error_msg): + extra_genkw_config = None + if parameters: + extra_genkw_config = GenKwConfig( + name="COEFFS", + forward_init=False, + template_file="", + transform_function_definitions=[ + TransformFunctionDefinition(param, "UNIFORM", [0, 1]) + for param in parameters + ], + output_file="kw.txt", + update=True, + ) + + realizations = [0, 1, 2] + design_path = tmp_path / "design_matrix.xlsx" + design_matrix_df = pd.DataFrame( + { + "REAL": realizations, + "a": [1, 2, 3], + "b": [0, 2, 0], + } + ) + default_sheet_df = pd.DataFrame([["a", 1], ["b", 4]]) + with pd.ExcelWriter(design_path) as xl_write: + design_matrix_df.to_excel(xl_write, index=False, sheet_name="DesignSheet01") + default_sheet_df.to_excel( + xl_write, index=False, sheet_name="DefaultValues", header=False + ) + design_matrix = DesignMatrix(design_path, "DesignSheet01", "DefaultValues") + design_matrix.read_design_matrix() + if len(parameters) == 2: + new_config_parameters, design_group = ( + design_matrix.merge_with_existing_parameters([extra_genkw_config]) + ) + assert len(new_config_parameters) == 0 + assert design_group.name == "COEFFS" + elif len(parameters) == 1: + with pytest.raises(ValueError, match=error_msg): + design_matrix.merge_with_existing_parameters([extra_genkw_config]) + else: + new_config_parameters, design_group = ( + design_matrix.merge_with_existing_parameters([extra_genkw_config]) + ) + assert len(new_config_parameters) == 2 + assert design_group.name == DESIGN_MATRIX_GROUP def test_reading_design_matrix(tmp_path): diff --git a/tests/ert/unit_tests/test_libres_facade.py b/tests/ert/unit_tests/test_libres_facade.py index be24bc0d8ae..55155765221 100644 --- a/tests/ert/unit_tests/test_libres_facade.py +++ b/tests/ert/unit_tests/test_libres_facade.py @@ -2,12 +2,15 @@ from datetime import datetime, timedelta from textwrap import dedent +import numpy as np import pytest +from pandas import ExcelWriter from pandas.core.frame import DataFrame from resdata.summary import Summary from ert.config import ErtConfig -from ert.enkf_main import sample_prior +from ert.config.design_matrix import DESIGN_MATRIX_GROUP, DesignMatrix +from ert.enkf_main import sample_prior, save_design_matrix_to_ensemble from ert.libres_facade import LibresFacade from ert.storage import open_storage @@ -241,3 +244,53 @@ def test_load_gen_kw_not_sorted(storage, tmpdir, snapshot): data = ensemble.load_all_gen_kw_data() snapshot.assert_match(data.round(12).to_csv(), "gen_kw_unsorted") + + +@pytest.mark.parametrize( + "reals, expect_error", + [ + pytest.param( + list(range(10)), + False, + id="correct_active_realizations", + ), + pytest.param([10, 11], True, id="incorrect_active_realizations"), + ], +) +def test_save_parameters_to_storage_from_design_dataframe( + tmp_path, reals, expect_error +): + design_path = tmp_path / "design_matrix.xlsx" + ensemble_size = 10 + a_values = np.random.default_rng().uniform(-5, 5, 10) + b_values = np.random.default_rng().uniform(-5, 5, 10) + c_values = np.random.default_rng().uniform(-5, 5, 10) + design_matrix_df = DataFrame({"a": a_values, "b": b_values, "c": c_values}) + with ExcelWriter(design_path) as xl_write: + design_matrix_df.to_excel(xl_write, index=False, sheet_name="DesignSheet01") + DataFrame().to_excel( + xl_write, index=False, sheet_name="DefaultValues", header=False + ) + design_matrix = DesignMatrix(design_path, "DesignSheet01", "DefaultValues") + design_matrix.read_design_matrix() + with open_storage(tmp_path / "storage", mode="w") as storage: + experiment_id = storage.create_experiment( + parameters=[design_matrix.parameter_configuration[DESIGN_MATRIX_GROUP]] + ) + ensemble = storage.create_ensemble( + experiment_id, name="default", ensemble_size=ensemble_size + ) + if expect_error: + with pytest.raises(KeyError): + save_design_matrix_to_ensemble( + design_matrix.design_matrix_df, ensemble, reals + ) + else: + save_design_matrix_to_ensemble( + design_matrix.design_matrix_df, ensemble, reals + ) + params = ensemble.load_parameters(DESIGN_MATRIX_GROUP)["values"] + all(params.names.values == ["a", "b", "c"]) + np.testing.assert_array_almost_equal(params[:, 0], a_values) + np.testing.assert_array_almost_equal(params[:, 1], b_values) + np.testing.assert_array_almost_equal(params[:, 2], c_values)