Skip to content

Commit 3338e50

Browse files
committed
Move GenKw template feature into run_template functionality
This includes adding parameter substition feature into substitute class. GenKwConfig.templates_from_config is reponsible for extracting tmpl and output files. In case an absolute path is provided as an output file it raises ConfigValidationError. To prevent multiple storage migrations this also remove forward_init from gen_kw.
1 parent ed0a7cf commit 3338e50

22 files changed

+137
-385
lines changed

src/ert/config/design_matrix.py

-4
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,6 @@ def merge_with_existing_parameters(
168168
)
169169

170170
design_parameter_group.name = parameter_group.name
171-
design_parameter_group.template_file = parameter_group.template_file
172-
design_parameter_group.output_file = parameter_group.output_file
173171
design_group_added = True
174172
elif set(design_keys) & set(existing_keys):
175173
raise ConfigValidationError(
@@ -249,8 +247,6 @@ def read_and_validate_design_matrix(
249247
parameter_configuration = GenKwConfig(
250248
name=DESIGN_MATRIX_GROUP,
251249
forward_init=False,
252-
template_file=None,
253-
output_file=None,
254250
transform_function_definitions=transform_function_definitions,
255251
update=False,
256252
)

src/ert/config/ensemble_config.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ def __post_init__(self) -> None:
6060
[p for p in self.parameter_configs.values() if isinstance(p, GenKwConfig)]
6161
)
6262

63-
self._check_for_forward_init_in_gen_kw(
64-
[p for p in self.parameter_configs.values() if isinstance(p, GenKwConfig)]
65-
)
66-
6763
self.grid_file = _get_abs_path(self.grid_file)
6864

6965
@staticmethod
@@ -97,11 +93,15 @@ def _check_for_duplicate_gen_kw_param_names(gen_kw_list: list[GenKwConfig]) -> N
9793
f"Found duplicate GEN_KW parameter names: {duplicates_formatted}"
9894
)
9995

96+
@no_type_check
10097
@staticmethod
101-
def _check_for_forward_init_in_gen_kw(gen_kw_list: list[GenKwConfig]) -> None:
102-
for gen_kw in gen_kw_list:
103-
if gen_kw.forward_init_file is not None:
104-
logger.info(f"GEN_KW uses FORWARD_INIT: {gen_kw}")
98+
def get_gen_kw_templates(config_dict: ConfigDict) -> list[tuple[str, str]]:
99+
gen_kw_list = config_dict.get(ConfigKeys.GEN_KW, [])
100+
return [
101+
template
102+
for g in gen_kw_list
103+
if (template := GenKwConfig.templates_from_config(g)) is not None
104+
]
105105

106106
@no_type_check
107107
@classmethod

src/ert/config/ert_config.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@
5858
ObservationConfigError,
5959
SummaryValues,
6060
)
61-
from .parsing.observations_parser import (
62-
parse_content as parse_observations,
63-
)
61+
from .parsing.observations_parser import parse_content as parse_observations
6462
from .queue_config import QueueConfig
6563
from .workflow import Workflow
6664
from .workflow_job import (
@@ -322,6 +320,7 @@ def read_templates(config_dict) -> list[tuple[str, str]]:
322320
"it is synced with your DATA file."
323321
)
324322
templates.append(template)
323+
templates.extend(EnsembleConfig.get_gen_kw_templates(config_dict))
325324
return templates
326325

327326

src/ert/config/gen_kw_config.py

+31-89
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import math
44
import os
5-
import shutil
65
import warnings
76
from collections.abc import Callable
87
from dataclasses import dataclass
@@ -16,8 +15,6 @@
1615
from scipy.stats import norm
1716
from typing_extensions import TypedDict
1817

19-
from ert.substitutions import substitute_runpath_name
20-
2118
from ._str_to_bool import str_to_bool
2219
from .parameter_config import ParameterConfig
2320
from .parsing import ConfigValidationError, ConfigWarning, ErrorInfo
@@ -59,10 +56,7 @@ class TransformFunctionDefinition:
5956

6057
@dataclass
6158
class GenKwConfig(ParameterConfig):
62-
template_file: str | None
63-
output_file: str | None
6459
transform_function_definitions: list[TransformFunctionDefinition]
65-
forward_init_file: str | None = None
6660

6761
def __post_init__(self) -> None:
6862
self.transform_functions: list[TransformFunction] = []
@@ -86,31 +80,19 @@ def __len__(self) -> int:
8680
return len(self.transform_functions)
8781

8882
@classmethod
89-
def from_config_list(cls, gen_kw: list[str | dict[str, str]]) -> Self:
83+
def templates_from_config(
84+
cls, gen_kw: list[str | dict[str, str]]
85+
) -> tuple[str, str] | None:
9086
gen_kw_key = cast(str, gen_kw[0])
91-
92-
options = cast(dict[str, str], gen_kw[-1])
9387
positional_args = cast(list[str], gen_kw[:-1])
94-
forward_init = str_to_bool(options.get("FORWARD_INIT", "FALSE"))
95-
init_file = _get_abs_path(options.get("INIT_FILES"))
96-
update_parameter = str_to_bool(options.get("UPDATE", "TRUE"))
97-
errors = []
9888

99-
if len(positional_args) == 2:
100-
parameter_file_contents = positional_args[1][1]
101-
parameter_file_context = positional_args[1][0]
102-
template_file = None
103-
output_file = None
104-
elif len(positional_args) == 4:
89+
if len(positional_args) == 4:
10590
output_file = positional_args[2]
106-
parameter_file_contents = positional_args[3][1]
10791
parameter_file_context = positional_args[3][0]
10892
template_file = _get_abs_path(positional_args[1][0])
10993
if not os.path.isfile(template_file):
110-
errors.append(
111-
ConfigValidationError.with_context(
112-
f"No such template file: {template_file}", positional_args[1]
113-
)
94+
raise ConfigValidationError.with_context(
95+
f"No such template file: {template_file}", positional_args[1]
11496
)
11597
elif Path(template_file).stat().st_size == 0:
11698
token = getattr(parameter_file_context, "token", parameter_file_context)
@@ -121,31 +103,35 @@ def from_config_list(cls, gen_kw: list[str | dict[str, str]]) -> Self:
121103
f"instead: GEN_KW {gen_kw_key} {token}",
122104
positional_args[1],
123105
)
106+
if output_file.startswith("/"):
107+
raise ConfigValidationError.with_context(
108+
f"Output file cannot have an absolute path {output_file}",
109+
positional_args[2],
110+
)
111+
return template_file, output_file
112+
return None
113+
114+
@classmethod
115+
def from_config_list(cls, gen_kw: list[str | dict[str, str]]) -> Self:
116+
gen_kw_key = cast(str, gen_kw[0])
117+
118+
options = cast(dict[str, str], gen_kw[-1])
119+
positional_args = cast(list[str], gen_kw[:-1])
120+
update_parameter = str_to_bool(options.get("UPDATE", "TRUE"))
121+
errors = []
122+
123+
if len(positional_args) == 2:
124+
parameter_file_contents = positional_args[1][1]
125+
parameter_file_context = positional_args[1][0]
126+
elif len(positional_args) == 4:
127+
parameter_file_contents = positional_args[3][1]
128+
parameter_file_context = positional_args[3][0]
124129

125130
else:
126131
raise ConfigValidationError(
127132
f"Unexpected positional arguments: {positional_args}"
128133
)
129134

130-
if forward_init:
131-
errors.append(
132-
ConfigValidationError.with_context(
133-
"Loading GEN_KW from files created by the forward "
134-
"model is not supported.",
135-
gen_kw,
136-
)
137-
)
138-
139-
if init_file and "%" not in init_file:
140-
errors.append(
141-
ConfigValidationError.with_context(
142-
"Loading GEN_KW from files requires %d in file format", gen_kw
143-
)
144-
)
145-
146-
if errors:
147-
raise ConfigValidationError.from_collected(errors)
148-
149135
transform_function_definitions: list[TransformFunctionDefinition] = []
150136
for line_number, item in enumerate(parameter_file_contents.splitlines()):
151137
item = item.split("--")[0] # remove comments
@@ -187,10 +173,7 @@ def from_config_list(cls, gen_kw: list[str | dict[str, str]]) -> Self:
187173
)
188174
return cls(
189175
name=gen_kw_key,
190-
forward_init=forward_init,
191-
template_file=template_file,
192-
output_file=output_file,
193-
forward_init_file=init_file,
176+
forward_init=False,
194177
transform_function_definitions=transform_function_definitions,
195178
update=update_parameter,
196179
)
@@ -281,9 +264,6 @@ def _check_valid_derrf_parameters(prior: PriorDict) -> None:
281264
def sample_or_load(
282265
self, real_nr: int, random_seed: int, ensemble_size: int
283266
) -> xr.Dataset:
284-
if self.forward_init_file:
285-
return self.read_from_runpath(Path(), real_nr, 0)
286-
287267
keys = [e.name for e in self.transform_functions]
288268
parameter_value = self._sample_value(
289269
self.name,
@@ -306,22 +286,7 @@ def read_from_runpath(
306286
real_nr: int,
307287
iteration: int,
308288
) -> xr.Dataset:
309-
keys = [e.name for e in self.transform_functions]
310-
if not self.forward_init_file:
311-
raise ValueError("loading gen_kw values requires forward_init_file")
312-
313-
parameter_value = self._values_from_file(
314-
substitute_runpath_name(self.forward_init_file, real_nr, iteration),
315-
keys,
316-
)
317-
318-
return xr.Dataset(
319-
{
320-
"values": ("names", parameter_value),
321-
"transformed_values": ("names", self.transform(parameter_value)),
322-
"names": keys,
323-
}
324-
)
289+
return xr.Dataset()
325290

326291
def write_to_runpath(
327292
self,
@@ -362,21 +327,6 @@ def parse_value(value: float | int | str) -> float | int | str:
362327
if tf.use_log
363328
}
364329

365-
if self.template_file is not None and self.output_file is not None:
366-
target_file = substitute_runpath_name(
367-
self.output_file, real_nr, ensemble.iteration
368-
)
369-
target_file = target_file.removeprefix("/")
370-
(run_path / target_file).parent.mkdir(exist_ok=True, parents=True)
371-
template_file_path = (
372-
ensemble.experiment.mount_point / Path(self.template_file).name
373-
)
374-
with open(template_file_path, encoding="utf-8") as f:
375-
template = f.read()
376-
for key, value in data.items():
377-
template = template.replace(f"<{key}>", f"{value:.6g}")
378-
with open(run_path / target_file, "w", encoding="utf-8") as f:
379-
f.write(template)
380330
if log10_data:
381331
return {self.name: data, f"LOG10_{self.name}": log10_data}
382332
else:
@@ -553,14 +503,6 @@ def _parse_transform_function_definition(
553503
calc_func=PRIOR_FUNCTIONS[t.param_name],
554504
)
555505

556-
def save_experiment_data(self, experiment_path: Path) -> None:
557-
if self.template_file:
558-
incoming_template_file_path = Path(self.template_file)
559-
template_file_path = Path(
560-
experiment_path / incoming_template_file_path.name
561-
)
562-
shutil.copyfile(incoming_template_file_path, template_file_path)
563-
564506

565507
@dataclass
566508
class TransformFunction:

src/ert/enkf_main.py

+17-10
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def _generate_parameter_files(
9090
iens: int,
9191
fs: Ensemble,
9292
iteration: int,
93-
) -> None:
93+
) -> dict[str, dict[str, float]]:
9494
"""
9595
Generate parameter files that are placed in each runtime directory for
9696
forward-model jobs to consume.
@@ -119,6 +119,7 @@ def _generate_parameter_files(
119119

120120
_value_export_txt(run_path, export_base_name, exports)
121121
_value_export_json(run_path, export_base_name, exports)
122+
return exports
122123

123124

124125
def _manifest_to_json(ensemble: Ensemble, iens: int, iter: int) -> dict[str, Any]:
@@ -130,6 +131,7 @@ def _manifest_to_json(ensemble: Ensemble, iens: int, iter: int) -> dict[str, Any
130131
ExtParamConfig | GenKwConfig | Field | SurfaceConfig,
131132
)
132133
if param_config.forward_init and ensemble.iteration == 0:
134+
assert not isinstance(param_config, GenKwConfig)
133135
assert param_config.forward_init_file is not None
134136
file_path = substitute_runpath_name(
135137
param_config.forward_init_file, iens, iter
@@ -225,6 +227,15 @@ def create_run_path(
225227
run_path = Path(run_arg.runpath)
226228
if run_arg.active:
227229
run_path.mkdir(parents=True, exist_ok=True)
230+
param_data = _generate_parameter_files(
231+
ensemble.experiment.parameter_configuration.values(),
232+
parameters_file,
233+
run_path,
234+
run_arg.iens,
235+
ensemble,
236+
ensemble.iteration,
237+
)
238+
228239
for (
229240
source_file_content,
230241
target_file,
@@ -237,6 +248,11 @@ def create_run_path(
237248
run_arg.iens,
238249
ensemble.iteration,
239250
)
251+
result = substitutions.substitute_parameters(
252+
result,
253+
param_data,
254+
)
255+
240256
target = run_path / target_file
241257
if not target.parent.exists():
242258
os.makedirs(
@@ -245,15 +261,6 @@ def create_run_path(
245261
)
246262
target.write_text(result)
247263

248-
_generate_parameter_files(
249-
ensemble.experiment.parameter_configuration.values(),
250-
parameters_file,
251-
run_path,
252-
run_arg.iens,
253-
ensemble,
254-
ensemble.iteration,
255-
)
256-
257264
path = run_path / "jobs.json"
258265
_backup_if_existing(path)
259266

src/ert/storage/local_experiment.py

+12-13
Original file line numberDiff line numberDiff line change
@@ -272,24 +272,23 @@ def parameter_info(self) -> dict[str, Any]:
272272

273273
@property
274274
def templates_configuration(self) -> list[tuple[str, str]]:
275+
templates: list[tuple[str, str]] = []
275276
try:
276-
templates: list[tuple[str, str]] = []
277277
with open(self.mount_point / self._templates_file, encoding="utf-8") as f:
278278
templates = json.load(f)
279-
templates_with_content: list[tuple[str, str]] = []
280-
for source_file, target_file in templates:
281-
try:
282-
file_content = (self.mount_point / source_file).read_text("utf-8")
283-
templates_with_content.append((file_content, target_file))
284-
except UnicodeDecodeError as e:
285-
raise ValueError(
286-
f"Unsupported non UTF-8 character found in file: {source_file}"
287-
) from e
288-
return templates_with_content
289279
except (FileNotFoundError, json.JSONDecodeError):
290-
pass
291280
# If the file is missing or broken, we return an empty list
292-
return []
281+
pass
282+
templates_with_content: list[tuple[str, str]] = []
283+
for source_file, target_file in templates:
284+
try:
285+
file_content = (self.mount_point / source_file).read_text("utf-8")
286+
templates_with_content.append((file_content, target_file))
287+
except UnicodeDecodeError as e:
288+
raise ValueError(
289+
f"Unsupported non UTF-8 character found in file: {source_file}"
290+
) from e
291+
return templates_with_content
293292

294293
@property
295294
def response_info(self) -> dict[str, Any]:

0 commit comments

Comments
 (0)