ert 19.0.1__py3-none-any.whl → 20.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. ert/__main__.py +94 -63
  2. ert/analysis/_es_update.py +11 -14
  3. ert/cli/main.py +1 -1
  4. ert/config/__init__.py +3 -2
  5. ert/config/_create_observation_dataframes.py +52 -375
  6. ert/config/_observations.py +527 -200
  7. ert/config/_read_summary.py +4 -5
  8. ert/config/ert_config.py +52 -117
  9. ert/config/everest_control.py +40 -39
  10. ert/config/everest_response.py +3 -15
  11. ert/config/field.py +4 -76
  12. ert/config/forward_model_step.py +17 -1
  13. ert/config/gen_data_config.py +14 -17
  14. ert/config/observation_config_migrations.py +821 -0
  15. ert/config/parameter_config.py +18 -28
  16. ert/config/parsing/__init__.py +0 -1
  17. ert/config/parsing/_parse_zonemap.py +45 -0
  18. ert/config/parsing/config_keywords.py +1 -0
  19. ert/config/parsing/config_schema.py +2 -0
  20. ert/config/parsing/observations_parser.py +2 -0
  21. ert/config/response_config.py +5 -23
  22. ert/config/rft_config.py +129 -31
  23. ert/config/summary_config.py +1 -13
  24. ert/config/surface_config.py +0 -57
  25. ert/dark_storage/compute/misfits.py +0 -42
  26. ert/dark_storage/endpoints/__init__.py +0 -2
  27. ert/dark_storage/endpoints/experiments.py +2 -5
  28. ert/dark_storage/json_schema/experiment.py +1 -2
  29. ert/field_utils/__init__.py +0 -2
  30. ert/field_utils/field_utils.py +1 -117
  31. ert/gui/ertwidgets/listeditbox.py +9 -1
  32. ert/gui/ertwidgets/models/ertsummary.py +20 -6
  33. ert/gui/ertwidgets/pathchooser.py +9 -1
  34. ert/gui/ertwidgets/stringbox.py +11 -3
  35. ert/gui/ertwidgets/textbox.py +10 -3
  36. ert/gui/ertwidgets/validationsupport.py +19 -1
  37. ert/gui/main_window.py +11 -6
  38. ert/gui/simulation/experiment_panel.py +1 -1
  39. ert/gui/simulation/run_dialog.py +11 -1
  40. ert/gui/tools/manage_experiments/export_dialog.py +4 -0
  41. ert/gui/tools/manage_experiments/manage_experiments_panel.py +1 -0
  42. ert/gui/tools/manage_experiments/storage_info_widget.py +1 -1
  43. ert/gui/tools/manage_experiments/storage_widget.py +21 -4
  44. ert/gui/tools/plot/data_type_proxy_model.py +1 -1
  45. ert/gui/tools/plot/plot_api.py +35 -27
  46. ert/gui/tools/plot/plot_widget.py +5 -0
  47. ert/gui/tools/plot/plot_window.py +4 -7
  48. ert/run_models/ensemble_experiment.py +2 -9
  49. ert/run_models/ensemble_smoother.py +1 -9
  50. ert/run_models/everest_run_model.py +31 -23
  51. ert/run_models/initial_ensemble_run_model.py +19 -22
  52. ert/run_models/manual_update.py +11 -5
  53. ert/run_models/model_factory.py +7 -7
  54. ert/run_models/multiple_data_assimilation.py +3 -16
  55. ert/sample_prior.py +12 -14
  56. ert/scheduler/job.py +24 -4
  57. ert/services/__init__.py +7 -3
  58. ert/services/_storage_main.py +59 -22
  59. ert/services/ert_server.py +186 -24
  60. ert/shared/version.py +3 -3
  61. ert/storage/local_ensemble.py +50 -116
  62. ert/storage/local_experiment.py +94 -109
  63. ert/storage/local_storage.py +10 -12
  64. ert/storage/migration/to24.py +26 -0
  65. ert/storage/migration/to25.py +91 -0
  66. ert/utils/__init__.py +20 -0
  67. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/METADATA +4 -51
  68. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/RECORD +80 -83
  69. everest/bin/everest_script.py +5 -5
  70. everest/bin/kill_script.py +2 -2
  71. everest/bin/monitor_script.py +2 -2
  72. everest/bin/utils.py +4 -4
  73. everest/detached/everserver.py +6 -6
  74. everest/gui/everest_client.py +0 -6
  75. everest/gui/main_window.py +2 -2
  76. everest/util/__init__.py +1 -19
  77. ert/dark_storage/compute/__init__.py +0 -0
  78. ert/dark_storage/endpoints/compute/__init__.py +0 -0
  79. ert/dark_storage/endpoints/compute/misfits.py +0 -95
  80. ert/services/_base_service.py +0 -387
  81. ert/services/webviz_ert_service.py +0 -20
  82. ert/shared/storage/command.py +0 -38
  83. ert/shared/storage/extraction.py +0 -42
  84. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/WHEEL +0 -0
  85. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/entry_points.txt +0 -0
  86. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/licenses/COPYING +0 -0
  87. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/top_level.txt +0 -0
@@ -1,93 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
3
4
  from collections import defaultdict
4
5
  from collections.abc import Sequence
5
- from datetime import datetime, timedelta
6
- from typing import TYPE_CHECKING, Any, assert_never
6
+ from typing import assert_never
7
7
 
8
- import numpy as np
9
8
  import polars as pl
10
- from resfo_utilities import history_key
11
-
12
- from ert.validation import rangestring_to_list
13
9
 
14
10
  from ._observations import (
15
- ErrorModes,
11
+ BreakthroughObservation,
16
12
  GeneralObservation,
17
- HistoryObservation,
18
13
  Observation,
19
- ObservationDate,
20
- ObservationError,
21
14
  RFTObservation,
22
15
  SummaryObservation,
23
16
  )
24
- from .gen_data_config import GenDataConfig
25
17
  from .parsing import (
26
- ConfigWarning,
27
18
  ErrorInfo,
28
- HistorySource,
29
19
  ObservationConfigError,
30
20
  )
31
- from .refcase import Refcase
32
21
  from .rft_config import RFTConfig
33
22
 
34
- if TYPE_CHECKING:
35
- import numpy.typing as npt
36
-
37
-
38
- DEFAULT_TIME_DELTA = timedelta(seconds=30)
39
23
  DEFAULT_LOCALIZATION_RADIUS = 3000
40
24
 
41
25
 
42
26
  def create_observation_dataframes(
43
27
  observations: Sequence[Observation],
44
- refcase: Refcase | None,
45
- gen_data_config: GenDataConfig | None,
46
28
  rft_config: RFTConfig | None,
47
- time_map: list[datetime] | None,
48
- history: HistorySource,
49
29
  ) -> dict[str, pl.DataFrame]:
50
30
  if not observations:
51
31
  return {}
52
- obs_time_list: list[datetime] = []
53
- if refcase is not None:
54
- obs_time_list = refcase.all_dates
55
- elif time_map is not None:
56
- obs_time_list = time_map
57
32
 
58
- time_len = len(obs_time_list)
59
33
  config_errors: list[ErrorInfo] = []
60
34
  grouped: dict[str, list[pl.DataFrame]] = defaultdict(list)
61
35
  for obs in observations:
62
36
  try:
63
37
  match obs:
64
- case HistoryObservation():
65
- grouped["summary"].append(
66
- _handle_history_observation(
67
- refcase,
68
- obs,
69
- obs.name,
70
- history,
71
- time_len,
72
- )
73
- )
74
38
  case SummaryObservation():
75
39
  grouped["summary"].append(
76
40
  _handle_summary_observation(
77
41
  obs,
78
42
  obs.name,
79
- obs_time_list,
80
- bool(refcase),
81
43
  )
82
44
  )
83
45
  case GeneralObservation():
84
46
  grouped["gen_data"].append(
85
47
  _handle_general_observation(
86
- gen_data_config,
87
48
  obs,
88
49
  obs.name,
89
- obs_time_list,
90
- bool(refcase),
91
50
  )
92
51
  )
93
52
  case RFTObservation():
@@ -97,6 +56,12 @@ def create_observation_dataframes(
97
56
  "rft_config is not None when using RFTObservation"
98
57
  )
99
58
  grouped["rft"].append(_handle_rft_observation(rft_config, obs))
59
+ case BreakthroughObservation():
60
+ grouped["breakthrough"].append(
61
+ _handle_breakthrough_observation(
62
+ obs,
63
+ )
64
+ )
100
65
  case default:
101
66
  assert_never(default)
102
67
  except ObservationConfigError as err:
@@ -118,187 +83,6 @@ def create_observation_dataframes(
118
83
  return datasets
119
84
 
120
85
 
121
- def _handle_error_mode(
122
- values: npt.ArrayLike,
123
- error_dict: ObservationError,
124
- ) -> npt.NDArray[np.double]:
125
- values = np.asarray(values)
126
- error_mode = error_dict.error_mode
127
- error_min = error_dict.error_min
128
- error = error_dict.error
129
- match error_mode:
130
- case ErrorModes.ABS:
131
- return np.full(values.shape, error)
132
- case ErrorModes.REL:
133
- return np.abs(values) * error
134
- case ErrorModes.RELMIN:
135
- return np.maximum(np.abs(values) * error, np.full(values.shape, error_min))
136
- case default:
137
- assert_never(default)
138
-
139
-
140
- def _handle_history_observation(
141
- refcase: Refcase | None,
142
- history_observation: HistoryObservation,
143
- summary_key: str,
144
- history_type: HistorySource,
145
- time_len: int,
146
- ) -> pl.DataFrame:
147
- if refcase is None:
148
- raise ObservationConfigError.with_context(
149
- "REFCASE is required for HISTORY_OBSERVATION", summary_key
150
- )
151
-
152
- if history_type == HistorySource.REFCASE_HISTORY:
153
- local_key = history_key(summary_key)
154
- else:
155
- local_key = summary_key
156
- if local_key not in refcase.keys:
157
- raise ObservationConfigError.with_context(
158
- f"Key {local_key!r} is not present in refcase", summary_key
159
- )
160
- values = refcase.values[refcase.keys.index(local_key)]
161
- std_dev = _handle_error_mode(values, history_observation)
162
- for segment in history_observation.segments:
163
- start = segment.start
164
- stop = segment.stop
165
- if start < 0:
166
- ConfigWarning.warn(
167
- f"Segment {segment.name} out of bounds."
168
- " Truncating start of segment to 0.",
169
- segment.name,
170
- )
171
- start = 0
172
- if stop >= time_len:
173
- ConfigWarning.warn(
174
- f"Segment {segment.name} out of bounds. Truncating"
175
- f" end of segment to {time_len - 1}.",
176
- segment.name,
177
- )
178
- stop = time_len - 1
179
- if start > stop:
180
- ConfigWarning.warn(
181
- f"Segment {segment.name} start after stop. Truncating"
182
- f" end of segment to {start}.",
183
- segment.name,
184
- )
185
- stop = start
186
- if np.size(std_dev[start:stop]) == 0:
187
- ConfigWarning.warn(
188
- f"Segment {segment.name} does not"
189
- " contain any time steps. The interval "
190
- f"[{start}, {stop}) does not intersect with steps in the"
191
- "time map.",
192
- segment.name,
193
- )
194
- std_dev[start:stop] = _handle_error_mode(values[start:stop], segment)
195
- dates_series = pl.Series(refcase.dates).dt.cast_time_unit("ms")
196
- if (std_dev <= 0).any():
197
- raise ObservationConfigError.with_context(
198
- "Observation uncertainty must be strictly > 0", summary_key
199
- ) from None
200
-
201
- return pl.DataFrame(
202
- {
203
- "response_key": summary_key,
204
- "observation_key": summary_key,
205
- "time": dates_series,
206
- "observations": pl.Series(values, dtype=pl.Float32),
207
- "std": pl.Series(std_dev, dtype=pl.Float32),
208
- "east": pl.Series([None] * len(values), dtype=pl.Float32),
209
- "north": pl.Series([None] * len(values), dtype=pl.Float32),
210
- "radius": pl.Series([None] * len(values), dtype=pl.Float32),
211
- }
212
- )
213
-
214
-
215
- def _get_time(
216
- date_dict: ObservationDate, start_time: datetime, context: Any = None
217
- ) -> tuple[datetime, str]:
218
- if date_dict.date is not None:
219
- return _parse_date(date_dict.date), f"DATE={date_dict.date}"
220
- if date_dict.days is not None:
221
- days = date_dict.days
222
- return start_time + timedelta(days=days), f"DAYS={days}"
223
- if date_dict.hours is not None:
224
- hours = date_dict.hours
225
- return start_time + timedelta(hours=hours), f"HOURS={hours}"
226
- raise ObservationConfigError.with_context("Missing time specifier", context=context)
227
-
228
-
229
- def _parse_date(date_str: str) -> datetime:
230
- try:
231
- return datetime.fromisoformat(date_str)
232
- except ValueError:
233
- try:
234
- date = datetime.strptime(date_str, "%d/%m/%Y")
235
- except ValueError as err:
236
- raise ObservationConfigError.with_context(
237
- f"Unsupported date format {date_str}. Please use ISO date format",
238
- date_str,
239
- ) from err
240
- else:
241
- ConfigWarning.warn(
242
- f"Deprecated time format {date_str}."
243
- " Please use ISO date format YYYY-MM-DD",
244
- date_str,
245
- )
246
- return date
247
-
248
-
249
- def _find_nearest(
250
- time_map: list[datetime],
251
- time: datetime,
252
- threshold: timedelta = DEFAULT_TIME_DELTA,
253
- ) -> int:
254
- nearest_index = -1
255
- nearest_diff = None
256
- for i, t in enumerate(time_map):
257
- diff = abs(time - t)
258
- if diff < threshold and (nearest_diff is None or nearest_diff > diff):
259
- nearest_diff = diff
260
- nearest_index = i
261
- if nearest_diff is None:
262
- raise IndexError(f"{time} is not in the time map")
263
- return nearest_index
264
-
265
-
266
- def _get_restart(
267
- date_dict: ObservationDate,
268
- obs_name: str,
269
- time_map: list[datetime],
270
- has_refcase: bool,
271
- ) -> int:
272
- if date_dict.restart is not None:
273
- return date_dict.restart
274
- if not time_map:
275
- raise ObservationConfigError.with_context(
276
- f"Missing REFCASE or TIME_MAP for observations: {obs_name}",
277
- obs_name,
278
- )
279
-
280
- time, date_str = _get_time(date_dict, time_map[0], context=obs_name)
281
-
282
- try:
283
- return _find_nearest(time_map, time)
284
- except IndexError as err:
285
- raise ObservationConfigError.with_context(
286
- f"Could not find {time} ({date_str}) in "
287
- f"the time map for observations {obs_name}. "
288
- + (
289
- "The time map is set from the REFCASE keyword. Either "
290
- "the REFCASE has an incorrect/missing date, or the observation "
291
- "is given an incorrect date.)"
292
- if has_refcase
293
- else "(The time map is set from the TIME_MAP "
294
- "keyword. Either the time map file has an "
295
- "incorrect/missing date, or the observation is given an "
296
- "incorrect date."
297
- ),
298
- obs_name,
299
- ) from err
300
-
301
-
302
86
  def _has_localization(summary_dict: SummaryObservation) -> bool:
303
87
  return summary_dict.east is not None and summary_dict.north is not None
304
88
 
@@ -306,39 +90,11 @@ def _has_localization(summary_dict: SummaryObservation) -> bool:
306
90
  def _handle_summary_observation(
307
91
  summary_dict: SummaryObservation,
308
92
  obs_key: str,
309
- time_map: list[datetime],
310
- has_refcase: bool,
311
93
  ) -> pl.DataFrame:
312
94
  summary_key = summary_dict.key
313
95
  value = summary_dict.value
314
- std_dev = float(_handle_error_mode(np.array(value), summary_dict))
315
-
316
- if summary_dict.restart and not (time_map or has_refcase):
317
- raise ObservationConfigError.with_context(
318
- "Keyword 'RESTART' requires either TIME_MAP or REFCASE", context=obs_key
319
- )
320
-
321
- if summary_dict.date is not None and not time_map:
322
- # We special case when the user has provided date in SUMMARY_OBS
323
- # and not REFCASE or time_map so that we don't change current behavior.
324
- date = _parse_date(summary_dict.date)
325
- restart = None
326
- else:
327
- restart = _get_restart(summary_dict, obs_key, time_map, has_refcase)
328
- date = time_map[restart]
329
-
330
- if restart == 0:
331
- raise ObservationConfigError.with_context(
332
- "It is unfortunately not possible to use summary "
333
- "observations from the start of the simulation. "
334
- f"Problem with observation {obs_key}"
335
- f"{' at ' + str(_get_time(summary_dict, time_map[0], obs_key)) if summary_dict.restart is None else ''}", # noqa: E501
336
- obs_key,
337
- )
338
- if std_dev <= 0:
339
- raise ObservationConfigError.with_context(
340
- "Observation uncertainty must be strictly > 0", summary_key
341
- ) from None
96
+ std_dev = summary_dict.error
97
+ date = datetime.datetime.fromisoformat(summary_dict.date)
342
98
 
343
99
  localization_radius = (
344
100
  summary_dict.radius or DEFAULT_LOCALIZATION_RADIUS
@@ -361,135 +117,28 @@ def _handle_summary_observation(
361
117
 
362
118
 
363
119
  def _handle_general_observation(
364
- gen_data_config: GenDataConfig | None,
365
120
  general_observation: GeneralObservation,
366
121
  obs_key: str,
367
- time_map: list[datetime],
368
- has_refcase: bool,
369
122
  ) -> pl.DataFrame:
370
123
  response_key = general_observation.data
124
+ restart = general_observation.restart
371
125
 
372
- if all(
373
- getattr(general_observation, key) is None
374
- for key in ["restart", "date", "days", "hours"]
375
- ):
376
- # The user has not provided RESTART or DATE, this is legal
377
- # for GEN_DATA, so we default it to None
378
- restart = None
379
- else:
380
- restart = _get_restart(general_observation, obs_key, time_map, has_refcase)
381
-
382
- if gen_data_config is None or response_key not in gen_data_config.keys:
383
- raise ObservationConfigError.with_context(
384
- f"Problem with GENERAL_OBSERVATION {obs_key}:"
385
- f" No GEN_DATA with name {response_key!r} found",
386
- response_key,
387
- )
388
- assert isinstance(gen_data_config, GenDataConfig)
389
-
390
- _, report_steps = gen_data_config.get_args_for_key(response_key)
391
-
392
- response_report_steps = [] if report_steps is None else report_steps
393
- if (restart is None and response_report_steps) or (
394
- restart is not None and restart not in response_report_steps
395
- ):
396
- raise ObservationConfigError.with_context(
397
- f"The GEN_DATA node:{response_key} is not configured to load from"
398
- f" report step:{restart} for the observation:{obs_key}",
399
- response_key,
400
- )
401
-
402
- restart = 0 if restart is None else restart
403
-
404
- if (
405
- general_observation.value is None
406
- and general_observation.error is None
407
- and general_observation.obs_file is None
408
- ):
409
- raise ObservationConfigError.with_context(
410
- "GENERAL_OBSERVATION must contain either VALUE and ERROR or OBS_FILE",
411
- context=obs_key,
412
- )
413
-
414
- if (
415
- general_observation.value is not None
416
- and general_observation.error is not None
417
- and general_observation.obs_file is not None
418
- ):
419
- raise ObservationConfigError.with_context(
420
- "GENERAL_OBSERVATION cannot contain both VALUE/ERROR and OBS_FILE",
421
- context=general_observation.obs_file,
422
- )
423
-
424
- if general_observation.obs_file is not None:
425
- try:
426
- file_values = np.loadtxt(
427
- general_observation.obs_file, delimiter=None
428
- ).ravel()
429
- except ValueError as err:
430
- raise ObservationConfigError.with_context(
431
- f"Failed to read OBS_FILE {general_observation.obs_file}: {err}",
432
- general_observation.obs_file,
433
- ) from err
434
- if len(file_values) % 2 != 0:
435
- raise ObservationConfigError.with_context(
436
- "Expected even number of values in GENERAL_OBSERVATION",
437
- general_observation.obs_file,
438
- )
439
- values = file_values[::2]
440
- stds = file_values[1::2]
441
-
442
- else:
443
- assert general_observation.value is not None
444
- assert general_observation.error is not None
445
- values = np.array([general_observation.value])
446
- stds = np.array([general_observation.error])
447
-
448
- index_list = general_observation.index_list
449
- index_file = general_observation.index_file
450
- if index_list is not None and index_file is not None:
451
- raise ObservationConfigError.with_context(
452
- f"GENERAL_OBSERVATION {obs_key} has both INDEX_FILE and INDEX_LIST.",
453
- obs_key,
454
- )
455
- if index_file is not None:
456
- indices = np.loadtxt(index_file, delimiter=None, dtype=np.int32).ravel()
457
- elif index_list is not None:
458
- indices = np.array(sorted(rangestring_to_list(index_list)), dtype=np.int32)
459
- else:
460
- indices = np.arange(len(values), dtype=np.int32)
461
-
462
- if len({len(stds), len(values), len(indices)}) != 1:
463
- raise ObservationConfigError.with_context(
464
- f"Values ({values}), error ({stds}) and "
465
- f"index list ({indices}) must be of equal length",
466
- (
467
- general_observation.obs_file
468
- if general_observation.obs_file is not None
469
- else ""
470
- ),
471
- )
472
-
473
- if np.any(stds <= 0):
126
+ if general_observation.error <= 0:
474
127
  raise ObservationConfigError.with_context(
475
128
  "Observation uncertainty must be strictly > 0", obs_key
476
129
  )
130
+
477
131
  return pl.DataFrame(
478
132
  {
479
- "response_key": response_key,
480
- "observation_key": obs_key,
481
- "report_step": pl.Series(
482
- np.full(len(indices), restart),
483
- dtype=pl.UInt16,
484
- ),
485
- "index": pl.Series(indices, dtype=pl.UInt16),
486
- "observations": pl.Series(values, dtype=pl.Float32),
487
- "std": pl.Series(stds, dtype=pl.Float32),
488
- # Location attributes will always be None for general observations, but are
489
- # necessary to concatenate with other observation dataframes.
490
- "east": pl.Series([None] * len(values), dtype=pl.Float32),
491
- "north": pl.Series([None] * len(values), dtype=pl.Float32),
492
- "radius": pl.Series([None] * len(values), dtype=pl.Float32),
133
+ "response_key": [response_key],
134
+ "observation_key": [general_observation.name],
135
+ "report_step": pl.Series([restart], dtype=pl.UInt16),
136
+ "index": pl.Series([general_observation.index], dtype=pl.UInt16),
137
+ "observations": pl.Series([general_observation.value], dtype=pl.Float32),
138
+ "std": pl.Series([general_observation.error], dtype=pl.Float32),
139
+ "east": pl.Series([general_observation.east], dtype=pl.Float32),
140
+ "north": pl.Series([general_observation.north], dtype=pl.Float32),
141
+ "radius": pl.Series([general_observation.radius], dtype=pl.Float32),
493
142
  }
494
143
  )
495
144
 
@@ -499,8 +148,17 @@ def _handle_rft_observation(
499
148
  rft_observation: RFTObservation,
500
149
  ) -> pl.DataFrame:
501
150
  location = (rft_observation.east, rft_observation.north, rft_observation.tvd)
151
+ zones = {zone for zones in rft_config.zonemap.values() for zone in zones}
502
152
  if location not in rft_config.locations:
503
- rft_config.locations.append(location)
153
+ if (zone := rft_observation.zone) is not None:
154
+ if zone not in zones:
155
+ raise ObservationConfigError(
156
+ f"The RFT_OBSERVATION {rft_observation.name} was given "
157
+ f"zone {zone} but no such zone exists in the ZONEMAP."
158
+ )
159
+ rft_config.locations.append((location, zone))
160
+ else:
161
+ rft_config.locations.append(location)
504
162
 
505
163
  data_to_read = rft_config.data_to_read
506
164
  if rft_observation.well not in data_to_read:
@@ -530,8 +188,27 @@ def _handle_rft_observation(
530
188
  "east": pl.Series([location[0]], dtype=pl.Float32),
531
189
  "north": pl.Series([location[1]], dtype=pl.Float32),
532
190
  "tvd": pl.Series([location[2]], dtype=pl.Float32),
191
+ "zone": pl.Series([rft_observation.zone], dtype=pl.String),
533
192
  "observations": pl.Series([rft_observation.value], dtype=pl.Float32),
534
193
  "std": pl.Series([rft_observation.error], dtype=pl.Float32),
535
194
  "radius": pl.Series([None], dtype=pl.Float32),
536
195
  }
537
196
  )
197
+
198
+
199
+ def _handle_breakthrough_observation(
200
+ obs_config: BreakthroughObservation,
201
+ ) -> pl.DataFrame:
202
+ return pl.DataFrame(
203
+ {
204
+ "observation_key": obs_config.name,
205
+ "response_key": (
206
+ f"BREAKTHROUGH:{obs_config.response_key}:{obs_config.threshold}"
207
+ ),
208
+ "observations": obs_config.date.isoformat(),
209
+ "std": pl.Series([obs_config.error], dtype=pl.Float32),
210
+ "east": pl.Series([obs_config.east], dtype=pl.Float32),
211
+ "north": pl.Series([obs_config.north], dtype=pl.Float32),
212
+ "radius": pl.Series([obs_config.radius], dtype=pl.Float32),
213
+ }
214
+ )