Skip to content

Commit e1528d0

Browse files
committed
Assure breakthrough time is always cast to ms
Reproduces and fixes #13962
1 parent 47757c5 commit e1528d0

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

src/ert/config/breakthrough_config.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ class BreakthroughConfig(DerivedResponseConfig):
1818
observed_dates: list[datetime] = Field(default_factory=list)
1919
has_finalized_keys: bool = True
2020

21+
@staticmethod
22+
def response_schema() -> dict[str, Any]:
23+
return {
24+
"response_key": pl.String,
25+
"threshold": pl.Float64,
26+
"time": pl.Datetime(time_unit="ms", time_zone=None),
27+
"values": pl.Float32,
28+
}
29+
2130
def derive_from_storage(
2231
self, iter_: int, realization: int, ensemble: Any
2332
) -> pl.DataFrame:
@@ -55,7 +64,9 @@ def derive_from_storage(
5564
time_offset_series = pl.Series(breakthrough_time_offsets, dtype=pl.Float32)
5665
else:
5766
time_offset_series = pl.Series(breakthrough_time_offsets, dtype=Float32)
58-
time_series = pl.Series(breakthrough_times).dt.cast_time_unit("ms")
67+
time_series = pl.Series(breakthrough_times)
68+
69+
time_series = time_series.dt.cast_time_unit("ms")
5970

6071
return pl.DataFrame(
6172
{
@@ -64,7 +75,7 @@ def derive_from_storage(
6475
"time": time_series,
6576
"values": time_offset_series,
6677
}
67-
)
78+
).pipe(self._assert_schema, self.response_schema())
6879

6980
@property
7081
def match_key(self) -> list[str]:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from datetime import datetime
2+
3+
import polars as pl
4+
5+
from ert.config import BreakthroughConfig, SummaryConfig
6+
from ert.storage.local_storage import open_storage
7+
8+
9+
def test_that_derive_from_storage_frames_are_stackable_regardless_of_breakthrough_time(
10+
tmp_path,
11+
):
12+
with open_storage(tmp_path, mode="w") as storage:
13+
response_key = "WWCT:OP1"
14+
time = datetime(2000, 3, 2, 13, 0) # ruff: ignore[call-datetime-without-tzinfo]
15+
16+
breakthrough_config = BreakthroughConfig(
17+
keys=[f"BREAKTHROUGH:{response_key}"],
18+
summary_keys=[response_key],
19+
thresholds=[0.2],
20+
observed_dates=[time],
21+
)
22+
23+
summary_config = SummaryConfig(
24+
keys=[response_key],
25+
input_files=["not_relevant"],
26+
)
27+
28+
experiment = storage.create_experiment(
29+
experiment_config={
30+
"response_configuration": [
31+
summary_config.model_dump(mode="json"),
32+
breakthrough_config.model_dump(mode="json"),
33+
],
34+
}
35+
)
36+
37+
ensemble = storage.create_ensemble(
38+
experiment, ensemble_size=2, iteration=0, name="prior"
39+
)
40+
41+
def create_summary_response_dataframe(
42+
response_key: str, realization: int, value_modifier
43+
) -> pl.DataFrame:
44+
return pl.DataFrame(
45+
{
46+
"realization": [realization] * 5,
47+
"response_key": [response_key] * 5,
48+
"time": [datetime(2000, month, 1, 1, 0) for month in range(1, 6)], # ruff: ignore[call-datetime-without-tzinfo]
49+
"values": [n / value_modifier for n in range(5)],
50+
}
51+
)
52+
53+
value_over_threshold = create_summary_response_dataframe(response_key, 0, 10)
54+
value_under_threshold = create_summary_response_dataframe(response_key, 1, 100)
55+
56+
ensemble.save_response("summary", value_over_threshold, 0)
57+
ensemble.save_response("summary", value_under_threshold, 1)
58+
59+
breakthrough_response0 = breakthrough_config.derive_from_storage(0, 0, ensemble)
60+
breakthrough_response1 = breakthrough_config.derive_from_storage(0, 1, ensemble)
61+
62+
ensemble.save_response("breakthrough", breakthrough_response0, 0)
63+
ensemble.save_response("breakthrough", breakthrough_response1, 1)
64+
65+
responses = ensemble.load_responses("breakthrough", (0, 1))
66+
assert len(responses) == 2

0 commit comments

Comments
 (0)