diff --git a/src/ert/config/analysis_config.py b/src/ert/config/analysis_config.py index 6c8fbc8aa56..1a5304549a8 100644 --- a/src/ert/config/analysis_config.py +++ b/src/ert/config/analysis_config.py @@ -16,12 +16,14 @@ ConfigKeys, ConfigValidationError, ConfigWarning, + ErrorInfo, ) logger = logging.getLogger(__name__) DEFAULT_ANALYSIS_MODE = AnalysisMode.ENSEMBLE_SMOOTHER ObservationGroups = List[str] +from ._option_dict import option_dict @dataclass @@ -31,6 +33,45 @@ class UpdateSettings: auto_scale_observations: List[ObservationGroups] = field(default_factory=list) +@dataclass +class DesignMatrixArgs: + xls_filename: Path + designsheet: str + defaultsheet: str + + @classmethod + def from_config_list(cls, config_list: List[str]) -> "DesignMatrixArgs": + filename = Path(config_list[0]) + options = option_dict(config_list, 1) + design_sheet = options.get("DESIGN_SHEET") + default_sheet = options.get("DEFAULT_SHEET") + errors = [] + if filename.suffix not in { + ".xlsx", + ".xls", + }: + errors.append( + ErrorInfo( + f"DESIGN_MATRIX must be of format .xls or .xslx; is {filename}" + ).set_context(config_list) + ) + if design_sheet is None: + errors.append( + ErrorInfo("Missing required DESIGN_SHEET").set_context(config_list) + ) + if default_sheet is None: + errors.append( + ErrorInfo("Missing required DEFAULT_SHEET").set_context(config_list) + ) + if errors: + raise ConfigValidationError.from_collected(errors) + return cls( + xls_filename=filename, + designsheet=design_sheet, + defaultsheet=default_sheet, + ) + + @dataclass class AnalysisConfig: max_runtime: Optional[int] = None @@ -40,6 +81,7 @@ class AnalysisConfig: ies_module: IESSettings = field(default_factory=IESSettings) observation_settings: UpdateSettings = field(default_factory=UpdateSettings) num_iterations: int = 1 + design_matrix_args: Optional[DesignMatrixArgs] = None @no_type_check @classmethod @@ -78,6 +120,9 @@ def from_dict(cls, config_dict: ConfigDict) -> "AnalysisConfig": ) min_realization = min(min_realization, num_realization) + + design_matrix_config_list = config_dict.get(ConfigKeys.DESIGN_MATRIX, None) + options: Dict[str, Dict[str, Any]] = {"STD_ENKF": {}, "IES_ENKF": {}} observation_settings: Dict[str, Any] = { "alpha": config_dict.get(ConfigKeys.ENKF_ALPHA, 3.0), @@ -189,6 +234,11 @@ def from_dict(cls, config_dict: ConfigDict) -> "AnalysisConfig": observation_settings=obs_settings, es_module=es_settings, ies_module=ies_settings, + design_matrix_args=DesignMatrixArgs.from_config_list( + design_matrix_config_list + ) + if design_matrix_config_list is not None + else None, ) return config diff --git a/src/ert/config/parsing/config_keywords.py b/src/ert/config/parsing/config_keywords.py index d9aefd291e2..956216b7c5f 100644 --- a/src/ert/config/parsing/config_keywords.py +++ b/src/ert/config/parsing/config_keywords.py @@ -27,6 +27,7 @@ class ConfigKeys(StrEnum): JOB_SCRIPT = "JOB_SCRIPT" JOBNAME = "JOBNAME" MAX_SUBMIT = "MAX_SUBMIT" + DESIGN_MATRIX = "DESIGN_MATRIX" NUM_REALIZATIONS = "NUM_REALIZATIONS" MIN_REALIZATIONS = "MIN_REALIZATIONS" OBS_CONFIG = "OBS_CONFIG" diff --git a/src/ert/config/parsing/config_schema.py b/src/ert/config/parsing/config_schema.py index 6d5d7ecbcbe..6adba3c9cba 100644 --- a/src/ert/config/parsing/config_schema.py +++ b/src/ert/config/parsing/config_schema.py @@ -261,6 +261,20 @@ def install_job_directory_keyword() -> SchemaItem: ) +def design_matrix_keyword() -> SchemaItem: + return SchemaItem( + kw=ConfigKeys.DESIGN_MATRIX, + argc_min=3, + argc_max=3, + type_map=[ + SchemaItemType.EXISTING_PATH, + SchemaItemType.STRING, + SchemaItemType.STRING, + ], + multi_occurrence=False, + ) + + class ConfigSchemaDict(SchemaItemDict): def check_required( self, @@ -345,6 +359,7 @@ def init_user_config_schema() -> ConfigSchemaDict: positive_int_keyword(ConfigKeys.NUM_CPU), positive_int_keyword(ConfigKeys.MAX_RUNNING), string_keyword(ConfigKeys.REALIZATION_MEMORY), + design_matrix_keyword(), queue_system_keyword(False), queue_option_keyword(), job_script_keyword(), diff --git a/tests/ert/unit_tests/config/test_analysis_config.py b/tests/ert/unit_tests/config/test_analysis_config.py index 3c594d73771..ed4126e5556 100644 --- a/tests/ert/unit_tests/config/test_analysis_config.py +++ b/tests/ert/unit_tests/config/test_analysis_config.py @@ -21,6 +21,7 @@ def test_analysis_config_from_file_is_same_as_from_dict(): NUM_REALIZATIONS 10 MIN_REALIZATIONS 10 ANALYSIS_SET_VAR STD_ENKF ENKF_TRUNCATION 0.8 + DESIGN_MATRIX my_design_matrix.xlsx DESIGN_SHEET:my_sheet DEFAULT_SHEET:my_default_sheet """ ) ).analysis_config == AnalysisConfig.from_dict( @@ -30,6 +31,11 @@ def test_analysis_config_from_file_is_same_as_from_dict(): ConfigKeys.ANALYSIS_SET_VAR: [ ("STD_ENKF", "ENKF_TRUNCATION", 0.8), ], + ConfigKeys.DESIGN_MATRIX: [ + "my_design_matrix.xlsx" + "DESIGN_SHEET:my_sheet" + "DEFAULT_SHEET:my_default_sheet" + ], } ) @@ -80,6 +86,23 @@ def test_invalid_min_realization_raises_config_validation_error(): ) +def test_invalid_design_matrix_format_raises_validation_error(): + with pytest.raises( + ConfigValidationError, + match="DESIGN_MATRIX must be of format .xls or .xslx; is 'my_matrix.txt'", + ): + AnalysisConfig.from_dict( + { + ConfigKeys.NUM_REALIZATIONS: 1, + ConfigKeys.DESIGN_MATRIX: [ + "my_matrix.txt", + "DESIGN_SHEET:sheet1", + "DEFAULT_SHEET:sheet2", + ], + } + ) + + def test_invalid_min_realization_percentage_raises_config_validation_error(): with pytest.raises( ConfigValidationError,