Skip to content

Commit 3fb9cab

Browse files
authored
Extend gather summary obs with breakthrough
This change will generalize existing methods working with summary observations to also fit breakthrough observations. The breakthrough observation will only be printed to terminal along with the SUMMARY bulk config inside the WELL section, and not written to the CSV file.
1 parent a139c58 commit 3fb9cab

3 files changed

Lines changed: 146 additions & 60 deletions

File tree

src/ert/gather_summary_observations.py

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import anyio
1717
import httpx
18+
from httpx import AsyncClient
1819
from resfo_utilities import SummaryKeyType
1920

2021
from ert.cli.main import ErtCliError
@@ -122,28 +123,21 @@ async def path_exists() -> None:
122123
return proc, config_path
123124

124125

125-
async def extract_summary_observations(
126-
client: httpx.AsyncClient,
127-
experiment: str,
128-
experiments: list[Any],
129-
) -> dict[str, list[Any]]:
130-
experiment_match = next(filter(lambda x: x["id"] == experiment, experiments), None)
131-
if experiment_match is None:
132-
raise ErtCliError(f"Provided experiment id {experiment} not found in storage")
133-
if "summary" not in experiment_match["observations"]:
134-
raise ErtCliError(
135-
f"No summary observations found for experiment '{experiment}'"
136-
)
137-
summary_observations = experiment_match["observations"]["summary"]
138-
response_key = {v: k for k, vs in summary_observations.items() for v in vs}
126+
async def extract_observations(
127+
obs_type: str, experiment: dict[str, Any], experiment_id: str, client: AsyncClient
128+
) -> dict[str, Any]:
129+
if obs_type not in experiment["observations"]:
130+
return {}
131+
observations = experiment["observations"][obs_type]
132+
obs_name_to_summary_key = {
133+
obs_name: summary_key
134+
for summary_key, obs_names in observations.items()
135+
for obs_name in obs_names
136+
}
139137
result: dict[str, list[Any]] = defaultdict(list)
140-
for obs in await get_observations(experiment, client):
141-
if obs["name"] in response_key:
142-
result[response_key[obs["name"]]].append(obs)
143-
if len(result) == 0:
144-
raise ErtCliError(
145-
f"No summary observations found for experiment '{experiment}'"
146-
)
138+
for obs in await get_observations(experiment_id, client):
139+
if obs["name"] in obs_name_to_summary_key:
140+
result[obs_name_to_summary_key[obs["name"]]].append(obs)
147141
return result
148142

149143

@@ -157,9 +151,10 @@ def non_empty_fields(skds: list[SummaryKeyData]) -> list[str]:
157151
]
158152

159153

160-
def localization_to_string(
161-
east: float | None, north: float | None, radius: float | None
162-
) -> str | None:
154+
def obs_to_localization_str(obs: list[Any]) -> str | None:
155+
east = get_first_loc_value("east", obs)
156+
north = get_first_loc_value("north", obs)
157+
radius = get_first_loc_value("radius", obs)
163158
if east is None or north is None:
164159
return None
165160
lines = [
@@ -170,34 +165,41 @@ def localization_to_string(
170165
if radius is not None:
171166
lines.append(f"{INDENT6}RADIUS={radius};")
172167
lines.append(f"{INDENT4}}};")
173-
return "\n".join(lines)
168+
return "\n".join(lines) + "\n"
174169

175170

176-
def get_first_loc_value(loc_key: str, summary_obs: list[Any]) -> float | None:
177-
for obs in summary_obs:
178-
key_value = obs.get(loc_key, [None])[0]
171+
def get_first_loc_value(loc_key: str, obs: list[dict[str, Any]]) -> float | int | None:
172+
for o in obs:
173+
key_value = o.get(loc_key, [None])[0]
179174
if key_value is not None:
180175
return key_value
181176
return None
182177

183178

179+
def breakthrough_to_string(obss: list[dict[str, Any]], key: str) -> str:
180+
obs = obss[0]
181+
lines = [
182+
f"{INDENT4}BREAKTHROUGH {{",
183+
f"{INDENT6}THRESHOLD={obs['values'][0]};",
184+
f"{INDENT6}DATE={obs['x_axis'][0]};",
185+
f"{INDENT6}ERROR={obs['errors'][0]};",
186+
f"{INDENT6}KEY={key};",
187+
f"{INDENT4}}};",
188+
]
189+
return "\n".join(lines) + "\n"
190+
191+
184192
def convert_summary_observations(
185-
summary_observations: dict[str, list[Any]], csv_file_name: str
193+
summary_observations: dict[str, list[Any]],
194+
breakthrough_observations: dict[str, list[Any]],
195+
csv_file_name: str,
186196
) -> None:
187-
skds = []
188-
189-
localization = {}
190-
for key in summary_observations: # noqa: PLC0206
197+
summary_keys = []
198+
for key in summary_observations:
191199
summary_key = make_summary_key_data(key)
192-
skds.append(summary_key)
193-
east = get_first_loc_value("east", summary_observations[key])
194-
north = get_first_loc_value("north", summary_observations[key])
195-
radius = get_first_loc_value("radius", summary_observations[key])
196-
loc_string = localization_to_string(east, north, radius)
197-
if loc_string is not None and (well := summary_key.well) is not None:
198-
localization[well] = loc_string
200+
summary_keys.append(summary_key)
199201

200-
header_fields = non_empty_fields(skds)
202+
header_fields = non_empty_fields(summary_keys)
201203
with Path(csv_file_name).open(mode="w", encoding="utf-8") as fout:
202204
fout.write(", ".join([*header_fields, "value", "error", "date"]))
203205
fout.write("\n")
@@ -215,10 +217,27 @@ def convert_summary_observations(
215217
fout.write(f"{observation['errors'][0]:.3g}, ")
216218
fout.write(f"{date.isoformat()}\n")
217219

220+
breakthrough = {}
221+
for brt_key, obs in breakthrough_observations.items():
222+
key = brt_key.removeprefix("BREAKTHROUGH:")
223+
summary_key = make_summary_key_data(key)
224+
if (well := summary_key.well) is not None:
225+
breakthrough[well] = breakthrough_to_string(obs, summary_key.keyword)
226+
227+
localization = {}
228+
for key in summary_observations | breakthrough_observations:
229+
summary_key = make_summary_key_data(key.removeprefix("BREAKTHROUGH:"))
230+
loc_string = obs_to_localization_str(
231+
summary_observations.get(key, []) + breakthrough_observations.get(key, [])
232+
)
233+
if loc_string is not None and (well := summary_key.well) is not None:
234+
localization[well] = loc_string
235+
218236
print(f"SUMMARY {{\n VALUES = {csv_file_name};")
219-
for well, loc in localization.items():
237+
for well in sorted(localization.keys() | breakthrough.keys()):
220238
print(f"{INDENT2}WELL {well} {{")
221-
print(loc)
239+
print(localization.get(well, ""), end="")
240+
print(breakthrough.get(well, ""), end="")
222241
print(f"{INDENT2}}};")
223242
print("};")
224243

@@ -312,11 +331,11 @@ async def _run_with_client(
312331

313332
assert isinstance(all_experiments, list)
314333
if args.experiment is not None:
315-
experiment = args.experiment
334+
experiment_id = args.experiment
316335
experiment_ids = [ex["id"] for ex in all_experiments]
317-
if experiment not in experiment_ids:
336+
if experiment_id not in experiment_ids:
318337
raise ErtCliError(
319-
f"An experiment with id '{experiment}' does not exist.\n"
338+
f"An experiment with id '{experiment_id}' does not exist.\n"
320339
f"Available experiments:\n "
321340
+ "\n ".join(
322341
[
@@ -326,13 +345,29 @@ async def _run_with_client(
326345
)
327346
)
328347
else:
329-
experiment = query_user_for_experiment(all_experiments)
348+
experiment_id = query_user_for_experiment(all_experiments)
330349

331-
summary_observations = await extract_summary_observations(
332-
client, experiment, all_experiments
350+
experiment = next(
351+
filter(lambda x: x["id"] == experiment_id, all_experiments), None
352+
)
353+
if experiment is None:
354+
raise ErtCliError(
355+
f"Provided experiment id {experiment_id} not found in storage"
356+
)
357+
if "summary" not in experiment["observations"]:
358+
raise ErtCliError(
359+
f"No summary observations found for experiment '{experiment_id}'"
360+
)
361+
summary_observations = await extract_observations(
362+
"summary", experiment, experiment_id, client
363+
)
364+
breakthrough_observations = await extract_observations(
365+
"breakthrough", experiment, experiment_id, client
333366
)
334367

335-
convert_summary_observations(summary_observations, args.output_csv_file)
368+
convert_summary_observations(
369+
summary_observations, breakthrough_observations, args.output_csv_file
370+
)
336371

337372

338373
async def _async_main(args: Namespace) -> None:

tests/ert/ui_tests/cli/test_gather_summary_observations.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from argparse import Namespace
33
from pathlib import Path
44
from subprocess import CalledProcessError
5+
from textwrap import dedent
56

7+
import polars as pl
68
import pytest
79

810
from ert.gather_summary_observations import main
@@ -32,9 +34,7 @@ def test_that_cli_command_without_feature_flag_raises_called_process_error() ->
3234
"copy_snake_oil_case_storage", "use_tmpdir", "use_feature_flag"
3335
)
3436
@pytest.mark.skip_mac_ci # Ert api is too slow to start for mac tests
35-
def test_that_gather_summary_observations_outputs_csv_containing_observation_data(
36-
monkeypatch,
37-
):
37+
def test_that_gather_summary_observations_outputs_csv_containing_observation_data():
3838
config_path = "test_data/snake_oil.ert"
3939
storage_path = "test_data/storage/"
4040
experiment_path = "snake_oil/ensemble/experiments/"
@@ -92,3 +92,52 @@ def test_that_single_experiment_in_storage_is_automatically_selected(capsys):
9292
"WOPR, OP1, 0.3, 0.075, 2012-12-15",
9393
]
9494
assert all(line in csv_content for line in expected_csv_content)
95+
96+
97+
@pytest.mark.usefixtures(
98+
"copy_snake_oil_case_storage", "use_tmpdir", "use_feature_flag"
99+
)
100+
def test_that_gather_summary_obs_can_gather_well_localization_from_breakthrough(capsys):
101+
config_path = "test_data/snake_oil.ert"
102+
storage_path = "test_data/storage/"
103+
experiment_path = "snake_oil/ensemble/experiments/"
104+
experiment = next(iter(Path(storage_path + experiment_path).iterdir()))
105+
obs_folder = Path(experiment / "observations")
106+
breakthrough_obs = pl.DataFrame(
107+
{
108+
"response_key": ["BREAKTHROUGH:WWCT:OP1"],
109+
"observation_key": ["BRT_OP1"],
110+
"time": ["2012-10-10"],
111+
"threshold": pl.Series([0.2], dtype=pl.Float64),
112+
"observations": pl.Series([0.0], dtype=pl.Float32),
113+
"std": pl.Series([5.0], dtype=pl.Float32),
114+
"east": pl.Series([100.0], dtype=pl.Float32),
115+
"north": pl.Series([200.0], dtype=pl.Float32),
116+
"radius": pl.Series([2500.0], dtype=pl.Float32),
117+
}
118+
)
119+
breakthrough_obs.write_parquet(Path(obs_folder / "breakthrough"))
120+
args = Namespace(
121+
config=config_path,
122+
experiment=experiment.name,
123+
output_csv_file="summary_observations.csv",
124+
)
125+
main(args)
126+
expected_stdout = dedent("""\
127+
SUMMARY {
128+
VALUES = summary_observations.csv;
129+
WELL OP1 {
130+
LOCALIZATION {
131+
EAST=100.0;
132+
NORTH=200.0;
133+
RADIUS=2500.0;
134+
};
135+
BREAKTHROUGH {
136+
THRESHOLD=0.2;
137+
DATE=2012-10-10T00:00:00;
138+
ERROR=5.0;
139+
KEY=WWCT;
140+
};
141+
};
142+
};""")
143+
assert expected_stdout in capsys.readouterr().out

tests/ert/unit_tests/cli/test_gather_summary_observations.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,26 @@ def test_that_convert_summary_observations_extracts_localization_information(cap
4949
],
5050
}
5151
convert_summary_observations(
52-
summary_observations=summary_obs, csv_file_name="foo.csv"
52+
summary_observations=summary_obs,
53+
breakthrough_observations={},
54+
csv_file_name="foo.csv",
5355
)
5456
expected_print_with_localization = dedent("""\
5557
SUMMARY {
5658
VALUES = foo.csv;
59+
WELL WELL_WITHOUT_RADIUS {
60+
LOCALIZATION {
61+
EAST=40;
62+
NORTH=50;
63+
};
64+
};
5765
WELL WELL_WITH_LOCALIZATION {
5866
LOCALIZATION {
5967
EAST=10;
6068
NORTH=20;
6169
RADIUS=2500;
6270
};
6371
};
64-
WELL WELL_WITHOUT_RADIUS {
65-
LOCALIZATION {
66-
EAST=40;
67-
NORTH=50;
68-
};
69-
};
7072
};""")
7173

7274
assert expected_print_with_localization in capsys.readouterr().out

0 commit comments

Comments
 (0)