ert 19.0.0rc1__py3-none-any.whl → 19.0.0rc3__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.
- ert/__main__.py +63 -94
- ert/analysis/_es_update.py +14 -11
- ert/config/_create_observation_dataframes.py +262 -23
- ert/config/_observations.py +153 -181
- ert/config/_read_summary.py +5 -4
- ert/config/ert_config.py +56 -1
- ert/config/parsing/observations_parser.py +0 -6
- ert/config/rft_config.py +1 -1
- ert/dark_storage/compute/__init__.py +0 -0
- ert/dark_storage/compute/misfits.py +42 -0
- ert/dark_storage/endpoints/__init__.py +2 -0
- ert/dark_storage/endpoints/compute/__init__.py +0 -0
- ert/dark_storage/endpoints/compute/misfits.py +95 -0
- ert/dark_storage/endpoints/experiments.py +3 -0
- ert/dark_storage/json_schema/experiment.py +1 -0
- ert/gui/main_window.py +0 -2
- ert/gui/tools/manage_experiments/export_dialog.py +0 -4
- ert/gui/tools/manage_experiments/storage_info_widget.py +5 -1
- ert/gui/tools/plot/plot_api.py +10 -10
- ert/gui/tools/plot/plot_widget.py +0 -5
- ert/gui/tools/plot/plot_window.py +1 -1
- ert/services/__init__.py +3 -7
- ert/services/_base_service.py +387 -0
- ert/services/_storage_main.py +22 -59
- ert/services/ert_server.py +24 -186
- ert/services/webviz_ert_service.py +20 -0
- ert/shared/storage/command.py +38 -0
- ert/shared/storage/extraction.py +42 -0
- ert/shared/version.py +3 -3
- ert/storage/local_ensemble.py +95 -2
- ert/storage/local_experiment.py +16 -0
- ert/storage/local_storage.py +1 -3
- ert/utils/__init__.py +0 -20
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/METADATA +2 -2
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/RECORD +46 -41
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/WHEEL +1 -1
- everest/bin/everest_script.py +5 -5
- everest/bin/kill_script.py +2 -2
- everest/bin/monitor_script.py +2 -2
- everest/bin/utils.py +4 -4
- everest/detached/everserver.py +6 -6
- everest/gui/main_window.py +2 -2
- everest/util/__init__.py +19 -1
- ert/config/observation_config_migrations.py +0 -793
- ert/storage/migration/to22.py +0 -18
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/entry_points.txt +0 -0
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/licenses/COPYING +0 -0
- {ert-19.0.0rc1.dist-info → ert-19.0.0rc3.dist-info}/top_level.txt +0 -0
ert/__main__.py
CHANGED
|
@@ -8,12 +8,10 @@ import multiprocessing
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
10
|
import resource
|
|
11
|
-
import shutil
|
|
12
11
|
import sys
|
|
13
12
|
import warnings
|
|
14
13
|
from argparse import ArgumentParser, ArgumentTypeError
|
|
15
14
|
from collections.abc import Sequence
|
|
16
|
-
from datetime import datetime
|
|
17
15
|
from pathlib import Path
|
|
18
16
|
from typing import Any
|
|
19
17
|
from uuid import UUID
|
|
@@ -26,9 +24,6 @@ from _ert.threading import set_signal_handler
|
|
|
26
24
|
from ert.base_model_context import use_runtime_plugins
|
|
27
25
|
from ert.cli.main import ErtCliError, run_cli
|
|
28
26
|
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
|
-
)
|
|
32
27
|
from ert.logging import LOGGING_CONFIG
|
|
33
28
|
from ert.mode_definitions import (
|
|
34
29
|
ENIF_MODE,
|
|
@@ -41,9 +36,9 @@ from ert.mode_definitions import (
|
|
|
41
36
|
from ert.namespace import Namespace
|
|
42
37
|
from ert.plugins import ErtRuntimePlugins, get_site_plugins, setup_site_logging
|
|
43
38
|
from ert.run_models.multiple_data_assimilation import MultipleDataAssimilationConfig
|
|
44
|
-
from ert.services import ErtServer
|
|
45
|
-
from ert.services._storage_main import add_parser_options as ert_api_add_parser_options
|
|
39
|
+
from ert.services import ErtServer, WebvizErt
|
|
46
40
|
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
|
|
47
42
|
from ert.storage import ErtStorageException, ErtStoragePermissionError
|
|
48
43
|
from ert.trace import trace, tracer
|
|
49
44
|
from ert.validation import (
|
|
@@ -58,66 +53,6 @@ from ert.validation import (
|
|
|
58
53
|
logger = logging.getLogger(__name__)
|
|
59
54
|
|
|
60
55
|
|
|
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
|
-
|
|
121
56
|
def run_ert_storage(args: Namespace, _: ErtRuntimePlugins | None = None) -> None:
|
|
122
57
|
with ErtServer.start_server(
|
|
123
58
|
verbose=True,
|
|
@@ -128,24 +63,67 @@ def run_ert_storage(args: Namespace, _: ErtRuntimePlugins | None = None) -> None
|
|
|
128
63
|
|
|
129
64
|
|
|
130
65
|
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
|
+
|
|
131
84
|
yellow = "\x1b[33m"
|
|
132
85
|
green = "\x1b[32m"
|
|
133
86
|
bold = "\x1b[1m"
|
|
134
87
|
reset = "\x1b[0m"
|
|
135
88
|
|
|
136
|
-
|
|
137
|
-
|
|
89
|
+
try:
|
|
90
|
+
with ErtServer.init_service(project=Path(ens_path).absolute()) as storage:
|
|
91
|
+
storage.wait_until_ready()
|
|
92
|
+
print(
|
|
93
|
+
f"""
|
|
138
94
|
---------------------------------------------------------------
|
|
139
95
|
|
|
140
|
-
{yellow}{bold}Webviz-ERT is removed
|
|
96
|
+
{yellow}{bold}Webviz-ERT is deprecated and will be removed in the near future{reset}
|
|
141
97
|
|
|
142
98
|
{green}{bold}Plotting capabilities provided by Webviz-ERT are now available
|
|
143
99
|
using the ERT plotter{reset}
|
|
144
100
|
|
|
101
|
+
---------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
Starting up Webviz-ERT. This might take more than a minute.
|
|
104
|
+
|
|
145
105
|
---------------------------------------------------------------
|
|
146
106
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
)
|
|
149
127
|
|
|
150
128
|
|
|
151
129
|
def strip_error_message_and_raise_exception(validated: ValidationStatus) -> None:
|
|
@@ -339,6 +317,19 @@ def get_ert_parser(parser: ArgumentParser | None = None) -> ArgumentParser:
|
|
|
339
317
|
ert_api_parser.set_defaults(func=run_ert_storage)
|
|
340
318
|
ert_api_add_parser_options(ert_api_parser)
|
|
341
319
|
|
|
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
|
+
|
|
342
333
|
# test_run_parser
|
|
343
334
|
test_run_description = f"Run '{TEST_RUN_MODE}' in cli"
|
|
344
335
|
test_run_parser = subparsers.add_parser(
|
|
@@ -573,28 +564,6 @@ def get_ert_parser(parser: ArgumentParser | None = None) -> ArgumentParser:
|
|
|
573
564
|
"--ensemble", help="Which ensemble to use", default=None
|
|
574
565
|
)
|
|
575
566
|
|
|
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
|
-
|
|
598
567
|
# Common arguments/defaults for all non-gui modes
|
|
599
568
|
for cli_parser in [
|
|
600
569
|
test_run_parser,
|
ert/analysis/_es_update.py
CHANGED
|
@@ -323,13 +323,16 @@ def analysis_ES(
|
|
|
323
323
|
logger.info(log_msg)
|
|
324
324
|
progress_callback(AnalysisStatusEvent(msg=log_msg))
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
326
|
+
log_msg = f"There are {num_obs} responses and {ensemble_size} realizations."
|
|
327
|
+
logger.info(log_msg)
|
|
328
|
+
progress_callback(AnalysisStatusEvent(msg=log_msg))
|
|
329
|
+
|
|
330
|
+
log_msg = (
|
|
331
|
+
f"There are {(~non_zero_variance_mask).sum()} parameters with 0 variance "
|
|
332
|
+
f"that will not be updated."
|
|
333
|
+
)
|
|
334
|
+
logger.info(log_msg)
|
|
335
|
+
progress_callback(AnalysisStatusEvent(msg=log_msg))
|
|
333
336
|
|
|
334
337
|
if module.localization:
|
|
335
338
|
config_node = source_ensemble.experiment.parameter_configuration[
|
|
@@ -379,16 +382,16 @@ def analysis_ES(
|
|
|
379
382
|
)
|
|
380
383
|
|
|
381
384
|
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
|
-
|
|
386
385
|
# In-place multiplication is not yet supported, therefore avoiding @=
|
|
387
386
|
param_ensemble_array[non_zero_variance_mask] = param_ensemble_array[ # noqa: PLR6104
|
|
388
387
|
non_zero_variance_mask
|
|
389
388
|
] @ T.astype(param_ensemble_array.dtype)
|
|
390
389
|
|
|
390
|
+
log_msg = f"Storing data for {param_group}.."
|
|
391
|
+
logger.info(log_msg)
|
|
392
|
+
progress_callback(AnalysisStatusEvent(msg=log_msg))
|
|
391
393
|
start = time.time()
|
|
394
|
+
|
|
392
395
|
target_ensemble.save_parameters_numpy(
|
|
393
396
|
param_ensemble_array, param_group, iens_active_index
|
|
394
397
|
)
|
|
@@ -2,19 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Sequence
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import assert_never
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import TYPE_CHECKING, Any, assert_never
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import polars as pl
|
|
10
|
-
from
|
|
10
|
+
from resfo_utilities import history_key
|
|
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,
|
|
17
18
|
Observation,
|
|
19
|
+
ObservationDate,
|
|
18
20
|
ObservationError,
|
|
19
21
|
RFTObservation,
|
|
20
22
|
SummaryObservation,
|
|
@@ -23,31 +25,59 @@ from .gen_data_config import GenDataConfig
|
|
|
23
25
|
from .parsing import (
|
|
24
26
|
ConfigWarning,
|
|
25
27
|
ErrorInfo,
|
|
28
|
+
HistorySource,
|
|
26
29
|
ObservationConfigError,
|
|
27
30
|
)
|
|
31
|
+
from .refcase import Refcase
|
|
28
32
|
from .rft_config import RFTConfig
|
|
29
33
|
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import numpy.typing as npt
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DEFAULT_TIME_DELTA = timedelta(seconds=30)
|
|
30
39
|
DEFAULT_LOCATION_RANGE_M = 3000
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
def create_observation_dataframes(
|
|
34
43
|
observations: Sequence[Observation],
|
|
44
|
+
refcase: Refcase | None,
|
|
35
45
|
gen_data_config: GenDataConfig | None,
|
|
36
46
|
rft_config: RFTConfig | None,
|
|
47
|
+
time_map: list[datetime] | None,
|
|
48
|
+
history: HistorySource,
|
|
37
49
|
) -> dict[str, pl.DataFrame]:
|
|
38
50
|
if not observations:
|
|
39
51
|
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
|
|
40
57
|
|
|
58
|
+
time_len = len(obs_time_list)
|
|
41
59
|
config_errors: list[ErrorInfo] = []
|
|
42
60
|
grouped: dict[str, list[pl.DataFrame]] = defaultdict(list)
|
|
43
61
|
for obs in observations:
|
|
44
62
|
try:
|
|
45
63
|
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
|
+
)
|
|
46
74
|
case SummaryObservation():
|
|
47
75
|
grouped["summary"].append(
|
|
48
76
|
_handle_summary_observation(
|
|
49
77
|
obs,
|
|
50
78
|
obs.name,
|
|
79
|
+
obs_time_list,
|
|
80
|
+
bool(refcase),
|
|
51
81
|
)
|
|
52
82
|
)
|
|
53
83
|
case GeneralObservation():
|
|
@@ -56,6 +86,8 @@ def create_observation_dataframes(
|
|
|
56
86
|
gen_data_config,
|
|
57
87
|
obs,
|
|
58
88
|
obs.name,
|
|
89
|
+
obs_time_list,
|
|
90
|
+
bool(refcase),
|
|
59
91
|
)
|
|
60
92
|
)
|
|
61
93
|
case RFTObservation():
|
|
@@ -105,6 +137,92 @@ def _handle_error_mode(
|
|
|
105
137
|
assert_never(default)
|
|
106
138
|
|
|
107
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
|
+
}
|
|
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
|
+
|
|
108
226
|
def _parse_date(date_str: str) -> datetime:
|
|
109
227
|
try:
|
|
110
228
|
return datetime.fromisoformat(date_str)
|
|
@@ -125,50 +243,171 @@ def _parse_date(date_str: str) -> datetime:
|
|
|
125
243
|
return date
|
|
126
244
|
|
|
127
245
|
|
|
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
|
+
|
|
128
299
|
def _has_localization(summary_dict: SummaryObservation) -> bool:
|
|
129
|
-
return
|
|
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
|
+
)
|
|
130
334
|
|
|
131
335
|
|
|
132
336
|
def _handle_summary_observation(
|
|
133
337
|
summary_dict: SummaryObservation,
|
|
134
338
|
obs_key: str,
|
|
339
|
+
time_map: list[datetime],
|
|
340
|
+
has_refcase: bool,
|
|
135
341
|
) -> pl.DataFrame:
|
|
136
342
|
summary_key = summary_dict.key
|
|
137
343
|
value = summary_dict.value
|
|
138
344
|
std_dev = float(_handle_error_mode(np.array(value), summary_dict))
|
|
139
|
-
date = _parse_date(summary_dict.date)
|
|
140
345
|
|
|
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
|
+
)
|
|
141
368
|
if std_dev <= 0:
|
|
142
369
|
raise ObservationConfigError.with_context(
|
|
143
370
|
"Observation uncertainty must be strictly > 0", summary_key
|
|
144
371
|
) from None
|
|
145
372
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
)
|
|
151
388
|
|
|
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
|
-
)
|
|
389
|
+
return pl.DataFrame(data_dict)
|
|
164
390
|
|
|
165
391
|
|
|
166
392
|
def _handle_general_observation(
|
|
167
393
|
gen_data_config: GenDataConfig | None,
|
|
168
394
|
general_observation: GeneralObservation,
|
|
169
395
|
obs_key: str,
|
|
396
|
+
time_map: list[datetime],
|
|
397
|
+
has_refcase: bool,
|
|
170
398
|
) -> pl.DataFrame:
|
|
171
399
|
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
|
+
|
|
172
411
|
if gen_data_config is None or response_key not in gen_data_config.keys:
|
|
173
412
|
raise ObservationConfigError.with_context(
|
|
174
413
|
f"Problem with GENERAL_OBSERVATION {obs_key}:"
|
|
@@ -176,7 +415,7 @@ def _handle_general_observation(
|
|
|
176
415
|
response_key,
|
|
177
416
|
)
|
|
178
417
|
assert isinstance(gen_data_config, GenDataConfig)
|
|
179
|
-
|
|
418
|
+
|
|
180
419
|
_, report_steps = gen_data_config.get_args_for_key(response_key)
|
|
181
420
|
|
|
182
421
|
response_report_steps = [] if report_steps is None else report_steps
|