ert 19.0.0rc0__py3-none-any.whl → 19.0.0rc1__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 (54) hide show
  1. ert/__main__.py +94 -63
  2. ert/analysis/_es_update.py +30 -20
  3. ert/config/_create_observation_dataframes.py +23 -262
  4. ert/config/_observations.py +181 -170
  5. ert/config/_read_summary.py +4 -5
  6. ert/config/ert_config.py +1 -48
  7. ert/config/observation_config_migrations.py +793 -0
  8. ert/config/parsing/config_schema_deprecations.py +0 -11
  9. ert/config/parsing/observations_parser.py +6 -0
  10. ert/config/rft_config.py +1 -1
  11. ert/dark_storage/compute/misfits.py +0 -42
  12. ert/dark_storage/endpoints/__init__.py +0 -2
  13. ert/dark_storage/endpoints/experiments.py +0 -3
  14. ert/dark_storage/endpoints/observations.py +10 -2
  15. ert/dark_storage/json_schema/experiment.py +0 -1
  16. ert/gui/main.py +4 -4
  17. ert/gui/main_window.py +2 -0
  18. ert/gui/simulation/experiment_panel.py +4 -0
  19. ert/gui/summarypanel.py +19 -0
  20. ert/gui/tools/manage_experiments/export_dialog.py +4 -0
  21. ert/gui/tools/manage_experiments/storage_info_widget.py +20 -20
  22. ert/gui/tools/plot/plot_api.py +10 -10
  23. ert/gui/tools/plot/plot_widget.py +5 -0
  24. ert/gui/tools/plot/plot_window.py +1 -1
  25. ert/run_models/run_model.py +1 -21
  26. ert/services/__init__.py +7 -3
  27. ert/services/_storage_main.py +59 -22
  28. ert/services/ert_server.py +186 -24
  29. ert/shared/version.py +3 -3
  30. ert/storage/local_ensemble.py +2 -95
  31. ert/storage/local_experiment.py +0 -16
  32. ert/storage/local_storage.py +3 -1
  33. ert/storage/migration/to22.py +18 -0
  34. ert/utils/__init__.py +20 -0
  35. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/METADATA +2 -2
  36. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/RECORD +47 -52
  37. everest/bin/everest_script.py +5 -5
  38. everest/bin/kill_script.py +2 -2
  39. everest/bin/monitor_script.py +2 -2
  40. everest/bin/utils.py +4 -4
  41. everest/detached/everserver.py +6 -6
  42. everest/gui/main_window.py +2 -2
  43. everest/util/__init__.py +1 -19
  44. ert/dark_storage/compute/__init__.py +0 -0
  45. ert/dark_storage/endpoints/compute/__init__.py +0 -0
  46. ert/dark_storage/endpoints/compute/misfits.py +0 -95
  47. ert/services/_base_service.py +0 -387
  48. ert/services/webviz_ert_service.py +0 -20
  49. ert/shared/storage/command.py +0 -38
  50. ert/shared/storage/extraction.py +0 -42
  51. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/WHEEL +0 -0
  52. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/entry_points.txt +0 -0
  53. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/licenses/COPYING +0 -0
  54. {ert-19.0.0rc0.dist-info → ert-19.0.0rc1.dist-info}/top_level.txt +0 -0
ert/__main__.py CHANGED
@@ -8,10 +8,12 @@ import multiprocessing
8
8
  import os
9
9
  import re
10
10
  import resource
11
+ import shutil
11
12
  import sys
12
13
  import warnings
13
14
  from argparse import ArgumentParser, ArgumentTypeError
14
15
  from collections.abc import Sequence
16
+ from datetime import datetime
15
17
  from pathlib import Path
16
18
  from typing import Any
17
19
  from uuid import UUID
@@ -24,6 +26,9 @@ from _ert.threading import set_signal_handler
24
26
  from ert.base_model_context import use_runtime_plugins
25
27
  from ert.cli.main import ErtCliError, run_cli
26
28
  from ert.config import ConfigValidationError, ErtConfig, lint_file
29
+ from ert.config.observation_config_migrations import (
30
+ remove_refcase_and_time_map_dependence_from_obs_config,
31
+ )
27
32
  from ert.logging import LOGGING_CONFIG
28
33
  from ert.mode_definitions import (
29
34
  ENIF_MODE,
@@ -36,9 +41,9 @@ from ert.mode_definitions import (
36
41
  from ert.namespace import Namespace
37
42
  from ert.plugins import ErtRuntimePlugins, get_site_plugins, setup_site_logging
38
43
  from ert.run_models.multiple_data_assimilation import MultipleDataAssimilationConfig
39
- from ert.services import ErtServer, WebvizErt
44
+ from ert.services import ErtServer
45
+ from ert.services._storage_main import add_parser_options as ert_api_add_parser_options
40
46
  from ert.shared.status.utils import get_ert_memory_usage
41
- from ert.shared.storage.command import add_parser_options as ert_api_add_parser_options
42
47
  from ert.storage import ErtStorageException, ErtStoragePermissionError
43
48
  from ert.trace import trace, tracer
44
49
  from ert.validation import (
@@ -53,6 +58,66 @@ from ert.validation import (
53
58
  logger = logging.getLogger(__name__)
54
59
 
55
60
 
61
+ def run_convert_observations(
62
+ args: Namespace, _: ErtRuntimePlugins | None = None
63
+ ) -> None:
64
+ changes = remove_refcase_and_time_map_dependence_from_obs_config(args.config)
65
+
66
+ if changes is None or changes.is_empty():
67
+ logger.info("convert_observations did not make any changes")
68
+ print(
69
+ "No observations dependent on TIME_MAP / REFCASE found, you can "
70
+ "safely remove TIME_MAP / REFCASE and the "
71
+ "corresponding files from ERT config."
72
+ )
73
+ return
74
+
75
+ obs_config_to_edit_path = changes.obs_config_path + ".updated"
76
+ print(
77
+ f"Making copy of obs config "
78
+ f"@ {changes.obs_config_path} -> {obs_config_to_edit_path}"
79
+ )
80
+
81
+ shutil.copy(changes.obs_config_path, obs_config_to_edit_path)
82
+ print(f"Applying change to obs config @ {obs_config_to_edit_path}...")
83
+ changes.apply_to_file(Path(obs_config_to_edit_path))
84
+ convert_observations_trace = ""
85
+ for history_change in changes.history_changes:
86
+ convert_observations_trace += (
87
+ f"History obs {history_change.source_observation.name} "
88
+ f"-> {len(history_change.summary_obs_declarations)} summary observations\n"
89
+ )
90
+
91
+ for gen_obs_change in changes.general_obs_changes:
92
+ convert_observations_trace += (
93
+ f"General obs {gen_obs_change.source_observation.name}, changing "
94
+ f"DATE {gen_obs_change.source_observation.date} "
95
+ f"to RESTART={gen_obs_change.restart}\n"
96
+ )
97
+ for summary_change in changes.summary_obs_changes:
98
+ convert_observations_trace += (
99
+ f"Summary obs {summary_change.source_observation.name}, changing "
100
+ f"RESTART {summary_change.source_observation.restart} "
101
+ f"to DATE={summary_change.date}\n"
102
+ )
103
+
104
+ logger.info(f"convert_observations trace: \n {convert_observations_trace}")
105
+ print(convert_observations_trace)
106
+
107
+ os.rename(
108
+ changes.obs_config_path,
109
+ f"{changes.obs_config_path}-{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.old",
110
+ )
111
+ os.rename(obs_config_to_edit_path, changes.obs_config_path)
112
+ msg = (
113
+ f"Observation changes applied to {changes.obs_config_path}. The old "
114
+ f"observations file is now at {changes.obs_config_path}.old and can be "
115
+ f"safely deleted if the new one works."
116
+ )
117
+ print(msg)
118
+ logger.info(msg)
119
+
120
+
56
121
  def run_ert_storage(args: Namespace, _: ErtRuntimePlugins | None = None) -> None:
57
122
  with ErtServer.start_server(
58
123
  verbose=True,
@@ -63,67 +128,24 @@ def run_ert_storage(args: Namespace, _: ErtRuntimePlugins | None = None) -> None
63
128
 
64
129
 
65
130
  def run_webviz_ert(args: Namespace, _: ErtRuntimePlugins | None = None) -> None:
66
- try:
67
- import webviz_ert # type: ignore # noqa
68
- except ImportError as err:
69
- raise ValueError(
70
- "Running `ert vis` requires that webviz_ert is installed"
71
- ) from err
72
-
73
- kwargs: dict[str, Any] = {"verbose": args.verbose}
74
- ert_config = ErtConfig.with_plugins(get_site_plugins()).from_file(args.config)
75
-
76
- os.chdir(ert_config.config_path)
77
- ens_path = ert_config.ens_path
78
-
79
- # Changing current working directory means we need to
80
- # only use the base name of the config file path
81
- kwargs["ert_config"] = os.path.basename(args.config)
82
- kwargs["project"] = os.path.abspath(ens_path)
83
-
84
131
  yellow = "\x1b[33m"
85
132
  green = "\x1b[32m"
86
133
  bold = "\x1b[1m"
87
134
  reset = "\x1b[0m"
88
135
 
89
- try:
90
- with ErtServer.init_service(project=Path(ens_path).absolute()) as storage:
91
- storage.wait_until_ready()
92
- print(
93
- f"""
136
+ print(
137
+ f"""
94
138
  ---------------------------------------------------------------
95
139
 
96
- {yellow}{bold}Webviz-ERT is deprecated and will be removed in the near future{reset}
140
+ {yellow}{bold}Webviz-ERT is removed.
97
141
 
98
142
  {green}{bold}Plotting capabilities provided by Webviz-ERT are now available
99
143
  using the ERT plotter{reset}
100
144
 
101
- ---------------------------------------------------------------
102
-
103
- Starting up Webviz-ERT. This might take more than a minute.
104
-
105
145
  ---------------------------------------------------------------
106
146
  """
107
- )
108
- logger.info("Show Webviz-ert deprecation warning")
109
- webviz_kwargs = {
110
- "experimental_mode": args.experimental_mode,
111
- "verbose": args.verbose,
112
- "title": kwargs.get("ert_config", "ERT - Visualization tool"),
113
- "project": kwargs.get("project", os.getcwd()),
114
- }
115
- with WebvizErt.start_server(**webviz_kwargs) as webviz_ert_server:
116
- webviz_ert_server.wait()
117
- except PermissionError as pe:
118
- print(f"Error: {pe}", file=sys.stderr)
119
- print(
120
- "Cannot start or connect to storage service due to permission issues.",
121
- file=sys.stderr,
122
- )
123
- print(
124
- "This is most likely due to another user starting ERT using this storage",
125
- file=sys.stderr,
126
- )
147
+ )
148
+ logger.info("Show Webviz-ert removal warning")
127
149
 
128
150
 
129
151
  def strip_error_message_and_raise_exception(validated: ValidationStatus) -> None:
@@ -317,19 +339,6 @@ def get_ert_parser(parser: ArgumentParser | None = None) -> ArgumentParser:
317
339
  ert_api_parser.set_defaults(func=run_ert_storage)
318
340
  ert_api_add_parser_options(ert_api_parser)
319
341
 
320
- ert_vis_parser = subparsers.add_parser(
321
- "vis",
322
- description="Launch webviz-driven visualization tool.",
323
- )
324
- ert_vis_parser.set_defaults(func=run_webviz_ert)
325
- ert_vis_parser.add_argument("--name", "-n", type=str, default="Webviz-ERT")
326
- ert_vis_parser.add_argument(
327
- "--experimental-mode",
328
- action="store_true",
329
- help="Feature flag for enabling experimental plugins",
330
- )
331
- ert_api_add_parser_options(ert_vis_parser) # ert vis shares args with ert api
332
-
333
342
  # test_run_parser
334
343
  test_run_description = f"Run '{TEST_RUN_MODE}' in cli"
335
344
  test_run_parser = subparsers.add_parser(
@@ -564,6 +573,28 @@ def get_ert_parser(parser: ArgumentParser | None = None) -> ArgumentParser:
564
573
  "--ensemble", help="Which ensemble to use", default=None
565
574
  )
566
575
 
576
+ # convert_observations_parser
577
+ convert_obs_parser = subparsers.add_parser(
578
+ "convert_observations",
579
+ help=(
580
+ "Convert HISTORY_OBSERVATION to SUMMARY_OBSERVATION and "
581
+ "remove REFCASE and TIME_MAP from ERT config."
582
+ ),
583
+ description=(
584
+ "Convert HISTORY_OBSERVATION to SUMMARY_OBSERVATION, "
585
+ "and embed REFCASE and TIME_MAP into observations"
586
+ ),
587
+ )
588
+ convert_obs_parser.set_defaults(func=run_convert_observations)
589
+ convert_obs_parser.add_argument(
590
+ "config",
591
+ type=valid_file,
592
+ help="Path to ERT config file",
593
+ )
594
+ convert_obs_parser.add_argument(
595
+ "--verbose", action="store_true", help="Show verbose output.", default=False
596
+ )
597
+
567
598
  # Common arguments/defaults for all non-gui modes
568
599
  for cli_parser in [
569
600
  test_run_parser,
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import functools
4
4
  import logging
5
+ import re
5
6
  import time
6
7
  import warnings
7
8
  from collections.abc import Callable, Iterable, Sequence
@@ -322,16 +323,13 @@ def analysis_ES(
322
323
  logger.info(log_msg)
323
324
  progress_callback(AnalysisStatusEvent(msg=log_msg))
324
325
 
325
- log_msg = f"There are {num_obs} responses and {ensemble_size} realizations."
326
- logger.info(log_msg)
327
- progress_callback(AnalysisStatusEvent(msg=log_msg))
328
-
329
- log_msg = (
330
- f"There are {(~non_zero_variance_mask).sum()} parameters with 0 variance "
331
- f"that will not be updated."
332
- )
333
- logger.info(log_msg)
334
- progress_callback(AnalysisStatusEvent(msg=log_msg))
326
+ if (param_count := (~non_zero_variance_mask).sum()) > 0:
327
+ log_msg = (
328
+ f"There are {param_count} parameters with 0 variance "
329
+ f"that will not be updated."
330
+ )
331
+ logger.info(log_msg)
332
+ progress_callback(AnalysisStatusEvent(msg=log_msg))
335
333
 
336
334
  if module.localization:
337
335
  config_node = source_ensemble.experiment.parameter_configuration[
@@ -381,16 +379,16 @@ def analysis_ES(
381
379
  )
382
380
 
383
381
  else:
382
+ log_msg = f"There are {num_obs} responses and {ensemble_size} realizations."
383
+ logger.info(log_msg)
384
+ progress_callback(AnalysisStatusEvent(msg=log_msg))
385
+
384
386
  # In-place multiplication is not yet supported, therefore avoiding @=
385
387
  param_ensemble_array[non_zero_variance_mask] = param_ensemble_array[ # noqa: PLR6104
386
388
  non_zero_variance_mask
387
389
  ] @ T.astype(param_ensemble_array.dtype)
388
390
 
389
- log_msg = f"Storing data for {param_group}.."
390
- logger.info(log_msg)
391
- progress_callback(AnalysisStatusEvent(msg=log_msg))
392
391
  start = time.time()
393
-
394
392
  target_ensemble.save_parameters_numpy(
395
393
  param_ensemble_array, param_group, iens_active_index
396
394
  )
@@ -441,6 +439,12 @@ def smoother_update(
441
439
  with warnings.catch_warnings():
442
440
  original_showwarning = warnings.showwarning
443
441
 
442
+ ILL_CONDITIONED_RE = re.compile(
443
+ r"^LinAlgWarning:.*ill[- ]?conditioned\s+matrix", re.IGNORECASE
444
+ )
445
+ LIMIT_ILL_CONDITIONED_WARNING = 1000
446
+ illconditioned_warn_counter = 0
447
+
444
448
  def log_warning(
445
449
  message: Warning | str,
446
450
  category: type[Warning],
@@ -449,12 +453,18 @@ def smoother_update(
449
453
  file: TextIO | None = None,
450
454
  line: str | None = None,
451
455
  ) -> None:
452
- logger.warning(
453
- f"{category.__name__}: {message} (from {filename}:{lineno})"
454
- )
455
- original_showwarning(
456
- message, category, filename, lineno, file=file, line=line
457
- )
456
+ nonlocal illconditioned_warn_counter
457
+
458
+ if ILL_CONDITIONED_RE.search(str(message)):
459
+ illconditioned_warn_counter += 1
460
+
461
+ if illconditioned_warn_counter < LIMIT_ILL_CONDITIONED_WARNING:
462
+ logger.warning(
463
+ f"{category.__name__}: {message} (from {filename}:{lineno})"
464
+ )
465
+ original_showwarning(
466
+ message, category, filename, lineno, file=file, line=line
467
+ )
458
468
 
459
469
  warnings.showwarning = log_warning
460
470
  analysis_ES(
@@ -2,21 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
4
  from collections.abc import Sequence
5
- from datetime import datetime, timedelta
6
- from typing import TYPE_CHECKING, Any, assert_never
5
+ from datetime import datetime
6
+ from typing import assert_never
7
7
 
8
8
  import numpy as np
9
9
  import polars as pl
10
- from resfo_utilities import history_key
10
+ from numpy import typing as npt
11
11
 
12
12
  from ert.validation import rangestring_to_list
13
13
 
14
14
  from ._observations import (
15
15
  ErrorModes,
16
16
  GeneralObservation,
17
- HistoryObservation,
18
17
  Observation,
19
- ObservationDate,
20
18
  ObservationError,
21
19
  RFTObservation,
22
20
  SummaryObservation,
@@ -25,59 +23,31 @@ from .gen_data_config import GenDataConfig
25
23
  from .parsing import (
26
24
  ConfigWarning,
27
25
  ErrorInfo,
28
- HistorySource,
29
26
  ObservationConfigError,
30
27
  )
31
- from .refcase import Refcase
32
28
  from .rft_config import RFTConfig
33
29
 
34
- if TYPE_CHECKING:
35
- import numpy.typing as npt
36
-
37
-
38
- DEFAULT_TIME_DELTA = timedelta(seconds=30)
39
30
  DEFAULT_LOCATION_RANGE_M = 3000
40
31
 
41
32
 
42
33
  def create_observation_dataframes(
43
34
  observations: Sequence[Observation],
44
- refcase: Refcase | None,
45
35
  gen_data_config: GenDataConfig | None,
46
36
  rft_config: RFTConfig | None,
47
- time_map: list[datetime] | None,
48
- history: HistorySource,
49
37
  ) -> dict[str, pl.DataFrame]:
50
38
  if not observations:
51
39
  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
40
 
58
- time_len = len(obs_time_list)
59
41
  config_errors: list[ErrorInfo] = []
60
42
  grouped: dict[str, list[pl.DataFrame]] = defaultdict(list)
61
43
  for obs in observations:
62
44
  try:
63
45
  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
46
  case SummaryObservation():
75
47
  grouped["summary"].append(
76
48
  _handle_summary_observation(
77
49
  obs,
78
50
  obs.name,
79
- obs_time_list,
80
- bool(refcase),
81
51
  )
82
52
  )
83
53
  case GeneralObservation():
@@ -86,8 +56,6 @@ def create_observation_dataframes(
86
56
  gen_data_config,
87
57
  obs,
88
58
  obs.name,
89
- obs_time_list,
90
- bool(refcase),
91
59
  )
92
60
  )
93
61
  case RFTObservation():
@@ -137,92 +105,6 @@ def _handle_error_mode(
137
105
  assert_never(default)
138
106
 
139
107
 
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
- }
209
- )
210
-
211
-
212
- def _get_time(
213
- date_dict: ObservationDate, start_time: datetime, context: Any = None
214
- ) -> tuple[datetime, str]:
215
- if date_dict.date is not None:
216
- return _parse_date(date_dict.date), f"DATE={date_dict.date}"
217
- if date_dict.days is not None:
218
- days = date_dict.days
219
- return start_time + timedelta(days=days), f"DAYS={days}"
220
- if date_dict.hours is not None:
221
- hours = date_dict.hours
222
- return start_time + timedelta(hours=hours), f"HOURS={hours}"
223
- raise ObservationConfigError.with_context("Missing time specifier", context=context)
224
-
225
-
226
108
  def _parse_date(date_str: str) -> datetime:
227
109
  try:
228
110
  return datetime.fromisoformat(date_str)
@@ -243,171 +125,50 @@ def _parse_date(date_str: str) -> datetime:
243
125
  return date
244
126
 
245
127
 
246
- def _find_nearest(
247
- time_map: list[datetime],
248
- time: datetime,
249
- threshold: timedelta = DEFAULT_TIME_DELTA,
250
- ) -> int:
251
- nearest_index = -1
252
- nearest_diff = None
253
- for i, t in enumerate(time_map):
254
- diff = abs(time - t)
255
- if diff < threshold and (nearest_diff is None or nearest_diff > diff):
256
- nearest_diff = diff
257
- nearest_index = i
258
- if nearest_diff is None:
259
- raise IndexError(f"{time} is not in the time map")
260
- return nearest_index
261
-
262
-
263
- def _get_restart(
264
- date_dict: ObservationDate,
265
- obs_name: str,
266
- time_map: list[datetime],
267
- has_refcase: bool,
268
- ) -> int:
269
- if date_dict.restart is not None:
270
- return date_dict.restart
271
- if not time_map:
272
- raise ObservationConfigError.with_context(
273
- f"Missing REFCASE or TIME_MAP for observations: {obs_name}",
274
- obs_name,
275
- )
276
-
277
- time, date_str = _get_time(date_dict, time_map[0], context=obs_name)
278
-
279
- try:
280
- return _find_nearest(time_map, time)
281
- except IndexError as err:
282
- raise ObservationConfigError.with_context(
283
- f"Could not find {time} ({date_str}) in "
284
- f"the time map for observations {obs_name}. "
285
- + (
286
- "The time map is set from the REFCASE keyword. Either "
287
- "the REFCASE has an incorrect/missing date, or the observation "
288
- "is given an incorrect date.)"
289
- if has_refcase
290
- else "(The time map is set from the TIME_MAP "
291
- "keyword. Either the time map file has an "
292
- "incorrect/missing date, or the observation is given an "
293
- "incorrect date."
294
- ),
295
- obs_name,
296
- ) from err
297
-
298
-
299
128
  def _has_localization(summary_dict: SummaryObservation) -> bool:
300
- return any(
301
- [
302
- summary_dict.location_x is not None,
303
- summary_dict.location_y is not None,
304
- summary_dict.location_range is not None,
305
- ]
306
- )
307
-
308
-
309
- def _validate_localization_values(summary_dict: SummaryObservation) -> None:
310
- """The user must provide LOCATION_X and LOCATION_Y to use localization, while
311
- unprovided LOCATION_RANGE should default to some value.
312
-
313
- This method assumes the summary dict contains at least one LOCATION key.
314
- """
315
- if summary_dict.location_x is None or summary_dict.location_y is None:
316
- loc_values = {
317
- "LOCATION_X": summary_dict.location_x,
318
- "LOCATION_Y": summary_dict.location_y,
319
- "LOCATION_RANGE": summary_dict.location_range,
320
- }
321
- provided_loc_values = {k: v for k, v in loc_values.items() if v is not None}
322
-
323
- provided_loc_values_string = ", ".join(
324
- key.upper() for key in provided_loc_values
325
- )
326
- raise ObservationConfigError.with_context(
327
- f"Localization for observation {summary_dict.name} is misconfigured.\n"
328
- f"Only {provided_loc_values_string} were provided. To enable "
329
- f"localization for an observation, ensure that both LOCATION_X and "
330
- f"LOCATION_Y are defined - or remove LOCATION keywords to disable "
331
- f"localization.",
332
- summary_dict,
333
- )
129
+ return summary_dict.location_x is not None and summary_dict.location_y is not None
334
130
 
335
131
 
336
132
  def _handle_summary_observation(
337
133
  summary_dict: SummaryObservation,
338
134
  obs_key: str,
339
- time_map: list[datetime],
340
- has_refcase: bool,
341
135
  ) -> pl.DataFrame:
342
136
  summary_key = summary_dict.key
343
137
  value = summary_dict.value
344
138
  std_dev = float(_handle_error_mode(np.array(value), summary_dict))
139
+ date = _parse_date(summary_dict.date)
345
140
 
346
- if summary_dict.restart and not (time_map or has_refcase):
347
- raise ObservationConfigError.with_context(
348
- "Keyword 'RESTART' requires either TIME_MAP or REFCASE", context=obs_key
349
- )
350
-
351
- if summary_dict.date is not None and not time_map:
352
- # We special case when the user has provided date in SUMMARY_OBS
353
- # and not REFCASE or time_map so that we don't change current behavior.
354
- date = _parse_date(summary_dict.date)
355
- restart = None
356
- else:
357
- restart = _get_restart(summary_dict, obs_key, time_map, has_refcase)
358
- date = time_map[restart]
359
-
360
- if restart == 0:
361
- raise ObservationConfigError.with_context(
362
- "It is unfortunately not possible to use summary "
363
- "observations from the start of the simulation. "
364
- f"Problem with observation {obs_key}"
365
- f"{' at ' + str(_get_time(summary_dict, time_map[0], obs_key)) if summary_dict.restart is None else ''}", # noqa: E501
366
- obs_key,
367
- )
368
141
  if std_dev <= 0:
369
142
  raise ObservationConfigError.with_context(
370
143
  "Observation uncertainty must be strictly > 0", summary_key
371
144
  ) from None
372
145
 
373
- data_dict = {
374
- "response_key": [summary_key],
375
- "observation_key": [obs_key],
376
- "time": pl.Series([date]).dt.cast_time_unit("ms"),
377
- "observations": pl.Series([value], dtype=pl.Float32),
378
- "std": pl.Series([std_dev], dtype=pl.Float32),
379
- }
380
-
381
- if _has_localization(summary_dict):
382
- _validate_localization_values(summary_dict)
383
- data_dict["location_x"] = summary_dict.location_x
384
- data_dict["location_y"] = summary_dict.location_y
385
- data_dict["location_range"] = (
386
- summary_dict.location_range or DEFAULT_LOCATION_RANGE_M
387
- )
146
+ location_range = (
147
+ summary_dict.location_range or DEFAULT_LOCATION_RANGE_M
148
+ if _has_localization(summary_dict)
149
+ else None
150
+ )
388
151
 
389
- return pl.DataFrame(data_dict)
152
+ return pl.DataFrame(
153
+ {
154
+ "response_key": [summary_key],
155
+ "observation_key": [obs_key],
156
+ "time": pl.Series([date]).dt.cast_time_unit("ms"),
157
+ "observations": pl.Series([value], dtype=pl.Float32),
158
+ "std": pl.Series([std_dev], dtype=pl.Float32),
159
+ "location_x": pl.Series([summary_dict.location_x], dtype=pl.Float32),
160
+ "location_y": pl.Series([summary_dict.location_y], dtype=pl.Float32),
161
+ "location_range": pl.Series([location_range], dtype=pl.Float32),
162
+ }
163
+ )
390
164
 
391
165
 
392
166
  def _handle_general_observation(
393
167
  gen_data_config: GenDataConfig | None,
394
168
  general_observation: GeneralObservation,
395
169
  obs_key: str,
396
- time_map: list[datetime],
397
- has_refcase: bool,
398
170
  ) -> pl.DataFrame:
399
171
  response_key = general_observation.data
400
-
401
- if all(
402
- getattr(general_observation, key) is None
403
- for key in ["restart", "date", "days", "hours"]
404
- ):
405
- # The user has not provided RESTART or DATE, this is legal
406
- # for GEN_DATA, so we default it to None
407
- restart = None
408
- else:
409
- restart = _get_restart(general_observation, obs_key, time_map, has_refcase)
410
-
411
172
  if gen_data_config is None or response_key not in gen_data_config.keys:
412
173
  raise ObservationConfigError.with_context(
413
174
  f"Problem with GENERAL_OBSERVATION {obs_key}:"
@@ -415,7 +176,7 @@ def _handle_general_observation(
415
176
  response_key,
416
177
  )
417
178
  assert isinstance(gen_data_config, GenDataConfig)
418
-
179
+ restart = general_observation.restart
419
180
  _, report_steps = gen_data_config.get_args_for_key(response_key)
420
181
 
421
182
  response_report_steps = [] if report_steps is None else report_steps