From 9a3c864607c891791a023e54abe6cea4706bf9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eide?= Date: Tue, 3 Sep 2024 15:24:01 +0200 Subject: [PATCH] Convert ErtConfig to dataclass --- src/ert/config/design_matrix.py | 10 ++-- src/ert/config/ensemble_config.py | 15 +++-- src/ert/config/ert_config.py | 55 ++++++++++++++----- src/ert/config/field.py | 2 +- src/ert/config/forward_model_step.py | 9 +++ src/ert/config/general_observation.py | 10 ++-- src/ert/config/observations.py | 10 ++-- src/ert/config/queue_config.py | 3 +- src/ert/config/refcase.py | 8 +-- src/ert/config/workflow.py | 11 ++-- src/ert/config/workflow_job.py | 2 +- src/ert/substitutions.py | 41 +++++++++++++- src/everest/simulator/everest_to_ert.py | 10 ++-- .../config/observations_generator.py | 9 ++- .../ert/unit_tests/config/test_ert_config.py | 25 ++++++++- .../unit_tests/config/test_observations.py | 13 ++--- .../test_that_storage_matches/parameters | 2 +- 17 files changed, 168 insertions(+), 67 deletions(-) diff --git a/src/ert/config/design_matrix.py b/src/ert/config/design_matrix.py index 25c200ba35e..f866766e41c 100644 --- a/src/ert/config/design_matrix.py +++ b/src/ert/config/design_matrix.py @@ -29,10 +29,12 @@ class DesignMatrix: xls_filename: Path design_sheet: str default_sheet: str - num_realizations: Optional[int] = None - active_realizations: Optional[List[bool]] = None - design_matrix_df: Optional[pd.DataFrame] = None - parameter_configuration: Optional[Dict[str, ParameterConfig]] = None + + def __post_init__(self) -> None: + self.num_realizations: Optional[int] = None + self.active_realizations: Optional[List[bool]] = None + self.design_matrix_df: Optional[pd.DataFrame] = None + self.parameter_configuration: Optional[Dict[str, ParameterConfig]] = None @classmethod def from_config_list(cls, config_list: List[str]) -> "DesignMatrix": diff --git a/src/ert/config/ensemble_config.py b/src/ert/config/ensemble_config.py index 0908cad5610..24ffc6be561 100644 --- a/src/ert/config/ensemble_config.py +++ b/src/ert/config/ensemble_config.py @@ -15,7 +15,8 @@ from ert.field_utils import get_shape -from .field import Field +from .ext_param_config import ExtParamConfig +from .field import Field as FieldConfig from .gen_data_config import GenDataConfig from .gen_kw_config import GenKwConfig from .parameter_config import ParameterConfig @@ -49,8 +50,12 @@ def _get_abs_path(file: Optional[str]) -> Optional[str]: @dataclass class EnsembleConfig: grid_file: Optional[str] = None - response_configs: Dict[str, ResponseConfig] = field(default_factory=dict) - parameter_configs: Dict[str, ParameterConfig] = field(default_factory=dict) + response_configs: Dict[str, Union[SummaryConfig, GenDataConfig]] = field( + default_factory=dict + ) + parameter_configs: Dict[ + str, GenKwConfig | FieldConfig | SurfaceConfig | ExtParamConfig + ] = field(default_factory=dict) refcase: Optional[Refcase] = None def __post_init__(self) -> None: @@ -92,7 +97,7 @@ def from_dict(cls, config_dict: ConfigDict) -> EnsembleConfig: grid_file_path, ) from err - def make_field(field_list: List[str]) -> Field: + def make_field(field_list: List[str]) -> FieldConfig: if grid_file_path is None: raise ConfigValidationError.with_context( "In order to use the FIELD keyword, a GRID must be supplied.", @@ -103,7 +108,7 @@ def make_field(field_list: List[str]) -> Field: f"Grid file {grid_file_path} did not contain dimensions", grid_file_path, ) - return Field.from_config_list(grid_file_path, dims, field_list) + return FieldConfig.from_config_list(grid_file_path, dims, field_list) parameter_configs = ( [GenKwConfig.from_config_list(g) for g in gen_kw_list] diff --git a/src/ert/config/ert_config.py b/src/ert/config/ert_config.py index 4c3241be5cb..61d5032439d 100644 --- a/src/ert/config/ert_config.py +++ b/src/ert/config/ert_config.py @@ -4,13 +4,14 @@ import logging import os from collections import defaultdict -from dataclasses import dataclass, field +from dataclasses import field from datetime import datetime from os import path from pathlib import Path from typing import ( Any, ClassVar, + DefaultDict, Dict, List, Optional, @@ -24,6 +25,8 @@ import polars from pydantic import ValidationError as PydanticValidationError +from pydantic import field_validator +from pydantic.dataclasses import dataclass from typing_extensions import Self from ert.plugins import ErtPluginManager @@ -49,6 +52,7 @@ ConfigWarning, ErrorInfo, ForwardModelStepKeys, + HistorySource, HookRuntime, init_forward_model_schema, init_site_config_schema, @@ -256,7 +260,9 @@ class ErtConfig: queue_config: QueueConfig = field(default_factory=QueueConfig) workflow_jobs: Dict[str, WorkflowJob] = field(default_factory=dict) workflows: Dict[str, Workflow] = field(default_factory=dict) - hooked_workflows: Dict[HookRuntime, List[Workflow]] = field(default_factory=dict) + hooked_workflows: DefaultDict[HookRuntime, List[Workflow]] = field( + default_factory=lambda: defaultdict(list) + ) runpath_file: Path = Path(DEFAULT_RUNPATH_FILE) ert_templates: List[Tuple[str, str]] = field(default_factory=list) installed_forward_model_steps: Dict[str, ForwardModelStep] = field( @@ -269,6 +275,14 @@ class ErtConfig: observation_config: List[ Tuple[str, Union[HistoryValues, SummaryValues, GenObsValues]] ] = field(default_factory=list) + enkf_obs: EnkfObs = field(default_factory=EnkfObs) + + @field_validator("substitutions", mode="before") + @classmethod + def convert_to_substitutions(cls, v: Dict[str, str]) -> Substitutions: + if isinstance(v, Substitutions): + return v + return Substitutions(v) def __eq__(self, other: object) -> bool: if not isinstance(other, ErtConfig): @@ -298,8 +312,6 @@ def __post_init__(self) -> None: if self.user_config_file else os.getcwd() ) - self.enkf_obs: EnkfObs = self._create_observations(self.observation_config) - self.observations: Dict[str, polars.DataFrame] = self.enkf_obs.datasets @staticmethod @@ -456,7 +468,7 @@ def from_dict(cls, config_dict) -> Self: errors.append(err) obs_config_file = config_dict.get(ConfigKeys.OBS_CONFIG) - obs_config_content = None + obs_config_content = [] try: if obs_config_file: if path.isfile(obs_config_file) and path.getsize(obs_config_file) == 0: @@ -487,6 +499,19 @@ def from_dict(cls, config_dict) -> Self: [key] for key in summary_obs if key not in summary_keys ] ensemble_config = EnsembleConfig.from_dict(config_dict=config_dict) + if model_config: + observations = cls._create_observations( + obs_config_content, + ensemble_config, + model_config.time_map, + model_config.history_source, + ) + else: + errors.append( + ConfigValidationError( + "Not possible to validate observations without valid model config" + ) + ) except ConfigValidationError as err: errors.append(err) @@ -519,6 +544,7 @@ def from_dict(cls, config_dict) -> Self: model_config=model_config, user_config_file=config_file_path, observation_config=obs_config_content, + enkf_obs=observations, ) @classmethod @@ -970,24 +996,25 @@ def _installed_forward_model_steps_from_dict( def preferred_num_cpu(self) -> int: return int(self.substitutions.get(f"<{ConfigKeys.NUM_CPU}>", 1)) + @staticmethod def _create_observations( - self, obs_config_content: Optional[ Dict[str, Union[HistoryValues, SummaryValues, GenObsValues]] ], + ensemble_config: EnsembleConfig, + time_map: Optional[List[datetime]], + history: HistorySource, ) -> EnkfObs: if not obs_config_content: return EnkfObs({}, []) obs_vectors: Dict[str, ObsVector] = {} obs_time_list: Sequence[datetime] = [] - if self.ensemble_config.refcase is not None: - obs_time_list = self.ensemble_config.refcase.all_dates - elif self.model_config.time_map is not None: - obs_time_list = self.model_config.time_map + if ensemble_config.refcase is not None: + obs_time_list = ensemble_config.refcase.all_dates + elif time_map is not None: + obs_time_list = time_map - history = self.model_config.history_source time_len = len(obs_time_list) - ensemble_config = self.ensemble_config config_errors: List[ErrorInfo] = [] for obs_name, values in obs_config_content: try: @@ -1059,7 +1086,7 @@ def _get_files_in_directory(job_path, errors): def _substitutions_from_dict(config_dict) -> Substitutions: - subst_list = Substitutions() + subst_list = {} for key, val in config_dict.get("DEFINE", []): subst_list[key] = val @@ -1077,7 +1104,7 @@ def _substitutions_from_dict(config_dict) -> Substitutions: for key, val in config_dict.get("DATA_KW", []): subst_list[key] = val - return subst_list + return Substitutions(subst_list) @no_type_check diff --git a/src/ert/config/field.py b/src/ert/config/field.py index 520ff9d82d1..79691f83bbc 100644 --- a/src/ert/config/field.py +++ b/src/ert/config/field.py @@ -3,13 +3,13 @@ import logging import os import time -from dataclasses import dataclass from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, List, Optional, Union, overload import numpy as np import xarray as xr +from pydantic.dataclasses import dataclass from typing_extensions import Self from ert.field_utils import FieldFileFormat, Shape, read_field, read_mask, save_field diff --git a/src/ert/config/forward_model_step.py b/src/ert/config/forward_model_step.py index 8e5026e7805..04a353e2a7b 100644 --- a/src/ert/config/forward_model_step.py +++ b/src/ert/config/forward_model_step.py @@ -5,12 +5,14 @@ from dataclasses import dataclass, field from typing import ( ClassVar, + Dict, Literal, Optional, TypedDict, Union, ) +from pydantic import field_validator from typing_extensions import NotRequired, Unpack from ert.config.parsing.config_errors import ConfigWarning @@ -172,6 +174,13 @@ class ForwardModelStep: "_ERT_RUNPATH": "", } + @field_validator("private_args", mode="before") + @classmethod + def convert_to_substitutions(cls, v: Dict[str, str]) -> Substitutions: + if isinstance(v, Substitutions): + return v + return Substitutions(v) + def validate_pre_experiment(self, fm_step_json: ForwardModelStepJSON) -> None: """ Raise errors pertaining to the environment not being diff --git a/src/ert/config/general_observation.py b/src/ert/config/general_observation.py index d2a31ccc07b..40744641f7a 100644 --- a/src/ert/config/general_observation.py +++ b/src/ert/config/general_observation.py @@ -1,17 +1,17 @@ from __future__ import annotations from dataclasses import dataclass +from typing import List import numpy as np -import numpy.typing as npt @dataclass(eq=False) class GenObservation: - values: npt.NDArray[np.double] - stds: npt.NDArray[np.double] - indices: npt.NDArray[np.int32] - std_scaling: npt.NDArray[np.double] + values: List[float] + stds: List[float] + indices: List[int] + std_scaling: List[float] def __post_init__(self) -> None: for val in self.stds: diff --git a/src/ert/config/observations.py b/src/ert/config/observations.py index dd204b45e81..b89d1f2fbc2 100644 --- a/src/ert/config/observations.py +++ b/src/ert/config/observations.py @@ -1,5 +1,5 @@ import os -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Union @@ -39,8 +39,8 @@ def history_key(key: str) -> str: @dataclass class EnkfObs: - obs_vectors: Dict[str, ObsVector] - obs_time: List[datetime] + obs_vectors: Dict[str, ObsVector] = field(default_factory=dict) + obs_time: List[datetime] = field(default_factory=list) def __post_init__(self) -> None: grouped: Dict[str, List[polars.DataFrame]] = {} @@ -394,7 +394,9 @@ def _create_gen_obs( f"index list ({indices}) must be of equal length", obs_file if obs_file is not None else "", ) - return GenObservation(values, stds, indices, std_scaling) + return GenObservation( + values.tolist(), stds.tolist(), indices.tolist(), std_scaling.tolist() + ) @classmethod def _handle_general_observation( diff --git a/src/ert/config/queue_config.py b/src/ert/config/queue_config.py index 9ed6653b263..61a76f68acc 100644 --- a/src/ert/config/queue_config.py +++ b/src/ert/config/queue_config.py @@ -8,7 +8,6 @@ from typing import Any, Dict, List, Literal, Mapping, Optional, Union, no_type_check import pydantic -from pydantic import Field from pydantic.dataclasses import dataclass from typing_extensions import Annotated @@ -270,7 +269,7 @@ class QueueConfig: queue_system: QueueSystem = QueueSystem.LOCAL queue_options: Union[ LsfQueueOptions, TorqueQueueOptions, SlurmQueueOptions, LocalQueueOptions - ] = Field(default_factory=LocalQueueOptions, discriminator="name") + ] = pydantic.Field(default_factory=LocalQueueOptions, discriminator="name") queue_options_test_run: LocalQueueOptions = field(default_factory=LocalQueueOptions) stop_long_running: bool = False diff --git a/src/ert/config/refcase.py b/src/ert/config/refcase.py index f5c1fbb4964..3c62873f1a3 100644 --- a/src/ert/config/refcase.py +++ b/src/ert/config/refcase.py @@ -1,14 +1,12 @@ from dataclasses import dataclass from datetime import datetime from typing import ( - Any, List, Optional, Sequence, ) import numpy as np -import numpy.typing as npt from ._read_summary import read_summary from .parsing.config_dict import ConfigDict @@ -21,7 +19,7 @@ class Refcase: start_date: datetime keys: List[str] dates: Sequence[datetime] - values: npt.NDArray[Any] + values: List[List[float]] def __eq__(self, other: object) -> bool: if not isinstance(other, Refcase): @@ -50,5 +48,7 @@ def from_config_dict(cls, config_dict: ConfigDict) -> Optional["Refcase"]: raise ConfigValidationError(f"Could not read refcase: {err}") from err return ( - cls(start_date, refcase_keys, time_map, data) if data is not None else None + cls(start_date, refcase_keys, time_map, data.tolist()) + if data is not None + else None ) diff --git a/src/ert/config/workflow.py b/src/ert/config/workflow.py index 9016401a8b9..7f5d3f3d93a 100644 --- a/src/ert/config/workflow.py +++ b/src/ert/config/workflow.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple from .parsing import ConfigValidationError, ErrorInfo, init_workflow_schema, parse @@ -11,14 +12,10 @@ from .workflow_job import WorkflowJob +@dataclass class Workflow: - def __init__( - self, - src_file: str, - cmd_list: List[Tuple[WorkflowJob, Any]], - ): - self.src_file = src_file - self.cmd_list = cmd_list + src_file: str + cmd_list: List[Tuple[WorkflowJob, Any]] def __len__(self) -> int: return len(self.cmd_list) diff --git a/src/ert/config/workflow_job.py b/src/ert/config/workflow_job.py index af816329ec1..4eea83c515d 100644 --- a/src/ert/config/workflow_job.py +++ b/src/ert/config/workflow_job.py @@ -82,7 +82,7 @@ def from_file(cls, config_file: str, name: Optional[str] = None) -> "WorkflowJob arg_types_list = cls._make_arg_types_list(content_dict) return cls( name=name, - internal=content_dict.get("INTERNAL"), # type: ignore + internal=bool(content_dict.get("INTERNAL", False)), # type: ignore min_args=content_dict.get("MIN_ARG"), # type: ignore max_args=content_dict.get("MAX_ARG"), # type: ignore arg_types=arg_types_list, diff --git a/src/ert/substitutions.py b/src/ert/substitutions.py index bf1973a04d4..bda5dc09983 100644 --- a/src/ert/substitutions.py +++ b/src/ert/substitutions.py @@ -2,7 +2,11 @@ import logging import re -from typing import Optional +from typing import Any, Optional + +from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import core_schema logger = logging.getLogger(__name__) _PATTERN = re.compile("<[^<>]+>") @@ -67,6 +71,41 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"Substitutions({self._concise_representation()})" + @classmethod + def __get_pydantic_core_schema__( + cls, + _source_type: Any, + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + def _serialize(instance: Any, info: Any) -> Any: + return dict(instance) + + from_str_schema = core_schema.chain_schema( + [ + core_schema.str_schema(), + core_schema.no_info_plain_validator_function(cls), + ] + ) + + return core_schema.json_or_python_schema( + json_schema=from_str_schema, + python_schema=core_schema.union_schema( + [ + from_str_schema, + core_schema.is_instance_schema(cls), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema( + _serialize, info_arg=True + ), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return handler(core_schema.str_schema()) + def _replace_strings(subst_list: Substitutions, string: str) -> Optional[str]: start = 0 diff --git a/src/everest/simulator/everest_to_ert.py b/src/everest/simulator/everest_to_ert.py index d4dce80ab7f..5151172d3c2 100644 --- a/src/everest/simulator/everest_to_ert.py +++ b/src/everest/simulator/everest_to_ert.py @@ -538,12 +538,10 @@ def _get_variables( # configuration key. When initializing an ERT config object, it is ignored. # It is used by the Simulator object to inject ExtParamConfig nodes. for control in ever_config.controls or []: - ens_config.parameter_configs[control.name] = ( - ExtParamConfig( - name=control.name, - input_keys=_get_variables(control.variables), - output_file=control.name + ".json", - ) + ens_config.parameter_configs[control.name] = ExtParamConfig( + name=control.name, + input_keys=_get_variables(control.variables), + output_file=control.name + ".json", ) return ert_config diff --git a/tests/ert/unit_tests/config/observations_generator.py b/tests/ert/unit_tests/config/observations_generator.py index 90da212276f..9853ab2113c 100644 --- a/tests/ert/unit_tests/config/observations_generator.py +++ b/tests/ert/unit_tests/config/observations_generator.py @@ -137,7 +137,7 @@ def general_observations(draw, ensemble_keys, std_cutoff, names): } val_type = draw(st.sampled_from(["value", "obs_file"])) if val_type == "value": - kws["value"] = draw(st.floats(allow_nan=False)) + kws["value"] = draw(st.floats(allow_nan=False, allow_infinity=False)) if val_type == "obs_file": kws["obs_file"] = draw(names) kws["error"] = None @@ -243,7 +243,10 @@ def observations(draw, ensemble_keys, summary_keys, std_cutoff, start_date): st.builds( HistoryObservation, error=st.floats( - min_value=std_cutoff, allow_nan=False, allow_infinity=False + min_value=std_cutoff, + max_value=1e20, + allow_nan=False, + allow_infinity=False, ), segment=st.lists( st.builds( @@ -253,12 +256,14 @@ def observations(draw, ensemble_keys, summary_keys, std_cutoff, start_date): stop=st.integers(min_value=1, max_value=10), error=st.floats( min_value=0.01, + max_value=1e20, allow_nan=False, allow_infinity=False, exclude_min=True, ), error_min=st.floats( min_value=0.0, + max_value=1e20, allow_nan=False, allow_infinity=False, exclude_min=True, diff --git a/tests/ert/unit_tests/config/test_ert_config.py b/tests/ert/unit_tests/config/test_ert_config.py index 9accd20c4a8..0e95a057738 100644 --- a/tests/ert/unit_tests/config/test_ert_config.py +++ b/tests/ert/unit_tests/config/test_ert_config.py @@ -11,6 +11,7 @@ import pytest from hypothesis import assume, given, settings from hypothesis import strategies as st +from pydantic import RootModel, TypeAdapter from ert.config import AnalysisConfig, ConfigValidationError, ErtConfig, HookRuntime from ert.config.ert_config import ( @@ -26,7 +27,6 @@ ContextList, ContextString, ) -from ert.config.parsing.observations_parser import ObservationConfigError from ert.config.parsing.queue_system import QueueSystem from .config_dict_generator import config_generators @@ -456,6 +456,25 @@ def test_that_creating_ert_config_from_dict_is_same_as_from_file( @pytest.mark.integration_test +@pytest.mark.filterwarnings("ignore::ert.config.ConfigWarning") +@pytest.mark.usefixtures("set_site_config") +@settings(max_examples=20) +@given(config_generators()) +def test_that_ert_config_is_serializable(tmp_path_factory, config_generator): + filename = "config.ert" + with config_generator(tmp_path_factory, filename) as config_values: + ert_config = ErtConfig.from_dict( + config_values.to_config_dict("config.ert", os.getcwd()) + ) + config_json = json.loads(RootModel[ErtConfig](ert_config).model_dump_json()) + from_json = ErtConfig(**config_json) + assert from_json == ert_config + + +def test_that_ert_config_has_valid_schema(): + TypeAdapter(ErtConfig).json_schema() + + @pytest.mark.filterwarnings("ignore::ert.config.ConfigWarning") @pytest.mark.usefixtures("set_site_config") @settings(max_examples=10) @@ -1378,7 +1397,7 @@ def test_no_timemap_or_refcase_provides_clear_error(): print(line, end="") with pytest.raises( - ObservationConfigError, + ConfigValidationError, match="Missing REFCASE or TIME_MAP for observations: WPR_DIFF_1", ): ErtConfig.from_file("snake_oil.ert") @@ -1419,7 +1438,7 @@ def test_that_multiple_errors_are_shown_when_generating_observations(): continue print(line, end="") - with pytest.raises(ObservationConfigError) as err: + with pytest.raises(ConfigValidationError) as err: _ = ErtConfig.from_file("snake_oil.ert") expected_errors = [ diff --git a/tests/ert/unit_tests/config/test_observations.py b/tests/ert/unit_tests/config/test_observations.py index ace152a3ad5..c32fb8b3c5c 100644 --- a/tests/ert/unit_tests/config/test_observations.py +++ b/tests/ert/unit_tests/config/test_observations.py @@ -4,7 +4,6 @@ from pathlib import Path from textwrap import dedent -import numpy as np import pytest from hypothesis import given, settings from hypothesis import strategies as st @@ -181,10 +180,10 @@ def test_summary_obs_invalid_observation_std(std): def test_gen_obs_invalid_observation_std(std): with pytest.raises(ValueError, match="must be strictly > 0"): GenObservation( - np.array(range(len(std))), - np.array(std), - np.array(range(len(std))), - np.array(range(len(std))), + list(range(len(std))), + list(std), + list(range(len(std))), + list(range(len(std))), ) @@ -303,7 +302,7 @@ def test_that_index_list_is_read(tmpdir): ) observations = ErtConfig.from_file("config.ert").enkf_obs - assert observations["OBS"].observations[0].indices.tolist() == [0, 2, 4, 6, 8] + assert observations["OBS"].observations[0].indices == [0, 2, 4, 6, 8] def test_that_index_file_is_read(tmpdir): @@ -337,7 +336,7 @@ def test_that_index_file_is_read(tmpdir): ) observations = ErtConfig.from_file("config.ert").enkf_obs - assert observations["OBS"].observations[0].indices.tolist() == [0, 2, 4, 6, 8] + assert observations["OBS"].observations[0].indices == [0, 2, 4, 6, 8] def test_that_missing_obs_file_raises_exception(tmpdir): diff --git a/tests/ert/unit_tests/storage/snapshots/test_storage_migration/test_that_storage_matches/parameters b/tests/ert/unit_tests/storage/snapshots/test_storage_migration/test_that_storage_matches/parameters index 11c37522e80..74f07f09cf8 100644 --- a/tests/ert/unit_tests/storage/snapshots/test_storage_migration/test_that_storage_matches/parameters +++ b/tests/ert/unit_tests/storage/snapshots/test_storage_migration/test_that_storage_matches/parameters @@ -1 +1 @@ -{'BPR': GenKwConfig(name='BPR', forward_init=False, update=True, template_file='/home/eivind/Projects/ert/test-data/all_data_types/template.txt', output_file='params.txt', transform_function_definitions=[{'name': 'BPR', 'param_name': 'NORMAL', 'values': ['0', '1']}], forward_init_file=None), 'PORO': Field(name='PORO', forward_init=False, update=True, nx=2, ny=3, nz=4, file_format='grdecl', output_transformation=None, input_transformation=None, truncation_min=None, truncation_max=None, forward_init_file='data/poro%d.grdecl', output_file='poro.grdecl', grid_file='/home/eivind/Projects/ert/test-data/all_data_types/refcase/CASE.EGRID', mask_file=''), 'TOP': SurfaceConfig(name='TOP', forward_init=False, update=True, ncol=2, nrow=3, xori=0.0, yori=0.0, xinc=1.0, yinc=1.0, rotation=0.0, yflip=1, forward_init_file='data/surf%d.irap', output_file='surf.irap', base_surface_path='data/basesurf.irap')} +{'BPR': GenKwConfig(name='BPR', forward_init=False, update=True, template_file='/home/eivind/Projects/ert/test-data/all_data_types/template.txt', output_file='params.txt', transform_function_definitions=[{'name': 'BPR', 'param_name': 'NORMAL', 'values': ['0', '1']}], forward_init_file=None), 'PORO': Field(name='PORO', forward_init=False, update=True, nx=2, ny=3, nz=4, file_format=, output_transformation=None, input_transformation=None, truncation_min=None, truncation_max=None, forward_init_file='data/poro%d.grdecl', output_file=PosixPath('poro.grdecl'), grid_file='/home/eivind/Projects/ert/test-data/all_data_types/refcase/CASE.EGRID', mask_file=''), 'TOP': SurfaceConfig(name='TOP', forward_init=False, update=True, ncol=2, nrow=3, xori=0.0, yori=0.0, xinc=1.0, yinc=1.0, rotation=0.0, yflip=1, forward_init_file='data/surf%d.irap', output_file='surf.irap', base_surface_path='data/basesurf.irap')}