ert 16.0.9__py3-none-any.whl → 19.0.0rc2__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/events.py +19 -2
- _ert/forward_model_runner/client.py +6 -2
- _ert/forward_model_runner/fm_dispatch.py +9 -6
- _ert/forward_model_runner/reporting/event.py +1 -0
- _ert/forward_model_runner/runner.py +1 -2
- _ert/utils.py +12 -0
- ert/__main__.py +58 -38
- ert/analysis/_enif_update.py +8 -4
- ert/analysis/_es_update.py +19 -6
- ert/analysis/_update_commons.py +16 -6
- ert/base_model_context.py +1 -1
- ert/cli/main.py +17 -12
- ert/cli/monitor.py +7 -0
- ert/config/__init__.py +17 -6
- ert/config/_create_observation_dataframes.py +118 -21
- ert/config/_get_num_cpu.py +1 -1
- ert/config/_observations.py +91 -2
- ert/config/_read_summary.py +74 -328
- ert/config/design_matrix.py +62 -23
- ert/config/distribution.py +1 -1
- ert/config/ensemble_config.py +9 -17
- ert/config/ert_config.py +155 -58
- ert/config/everest_control.py +234 -0
- ert/config/{everest_constraints_config.py → everest_response.py} +27 -15
- ert/config/field.py +99 -90
- ert/config/forward_model_step.py +122 -17
- ert/config/gen_data_config.py +5 -10
- ert/config/gen_kw_config.py +11 -41
- ert/config/known_response_types.py +14 -0
- ert/config/parameter_config.py +1 -33
- ert/config/parsing/_option_dict.py +10 -2
- ert/config/parsing/config_errors.py +1 -1
- ert/config/parsing/config_keywords.py +2 -1
- ert/config/parsing/config_schema.py +23 -11
- ert/config/parsing/config_schema_deprecations.py +3 -3
- ert/config/parsing/config_schema_item.py +26 -11
- ert/config/parsing/context_values.py +3 -3
- ert/config/parsing/file_context_token.py +1 -1
- ert/config/parsing/observations_parser.py +6 -2
- ert/config/parsing/queue_system.py +9 -0
- ert/config/parsing/schema_item_type.py +1 -0
- ert/config/queue_config.py +42 -50
- ert/config/response_config.py +0 -8
- ert/config/rft_config.py +275 -0
- ert/config/summary_config.py +3 -8
- ert/config/surface_config.py +73 -26
- ert/config/workflow_fixtures.py +2 -1
- ert/config/workflow_job.py +135 -54
- ert/dark_storage/client/__init__.py +2 -2
- ert/dark_storage/client/_session.py +4 -4
- ert/dark_storage/client/client.py +2 -2
- ert/dark_storage/common.py +12 -3
- ert/dark_storage/compute/misfits.py +11 -7
- ert/dark_storage/endpoints/compute/misfits.py +6 -4
- ert/dark_storage/endpoints/ensembles.py +4 -0
- ert/dark_storage/endpoints/experiment_server.py +30 -24
- ert/dark_storage/endpoints/experiments.py +2 -2
- ert/dark_storage/endpoints/observations.py +8 -6
- ert/dark_storage/endpoints/parameters.py +4 -12
- ert/dark_storage/endpoints/responses.py +24 -5
- ert/dark_storage/json_schema/ensemble.py +3 -0
- ert/dark_storage/json_schema/experiment.py +1 -1
- ert/data/_measured_data.py +6 -5
- ert/ensemble_evaluator/__init__.py +8 -1
- ert/ensemble_evaluator/config.py +2 -1
- ert/ensemble_evaluator/evaluator.py +81 -29
- ert/ensemble_evaluator/event.py +6 -0
- ert/ensemble_evaluator/snapshot.py +3 -1
- ert/ensemble_evaluator/state.py +1 -0
- ert/field_utils/__init__.py +8 -0
- ert/field_utils/field_utils.py +228 -15
- ert/field_utils/grdecl_io.py +1 -1
- ert/field_utils/roff_io.py +1 -1
- ert/gui/__init__.py +5 -2
- ert/gui/ertnotifier.py +1 -1
- ert/gui/ertwidgets/__init__.py +23 -16
- ert/gui/ertwidgets/analysismoduleedit.py +2 -2
- ert/gui/ertwidgets/checklist.py +1 -1
- ert/gui/ertwidgets/closabledialog.py +2 -0
- ert/gui/ertwidgets/copyablelabel.py +2 -0
- ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
- ert/gui/ertwidgets/ensembleselector.py +2 -2
- ert/gui/ertwidgets/listeditbox.py +2 -0
- ert/gui/ertwidgets/models/__init__.py +2 -0
- ert/gui/ertwidgets/models/activerealizationsmodel.py +5 -1
- ert/gui/ertwidgets/models/path_model.py +1 -1
- ert/gui/ertwidgets/models/targetensemblemodel.py +5 -1
- ert/gui/ertwidgets/models/text_model.py +4 -1
- ert/gui/ertwidgets/pathchooser.py +0 -3
- ert/gui/ertwidgets/searchbox.py +17 -4
- ert/gui/ertwidgets/stringbox.py +2 -0
- ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
- ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +63 -30
- ert/gui/main.py +41 -13
- ert/gui/main_window.py +3 -7
- ert/gui/model/fm_step_list.py +3 -0
- ert/gui/model/real_list.py +1 -0
- ert/gui/model/snapshot.py +1 -0
- ert/gui/simulation/combobox_with_description.py +3 -0
- ert/gui/simulation/ensemble_experiment_panel.py +8 -2
- ert/gui/simulation/ensemble_information_filter_panel.py +7 -2
- ert/gui/simulation/ensemble_smoother_panel.py +8 -2
- ert/gui/simulation/evaluate_ensemble_panel.py +17 -7
- ert/gui/simulation/experiment_panel.py +18 -6
- ert/gui/simulation/manual_update_panel.py +35 -10
- ert/gui/simulation/multiple_data_assimilation_panel.py +13 -9
- ert/gui/simulation/run_dialog.py +47 -20
- ert/gui/simulation/single_test_run_panel.py +6 -3
- ert/gui/simulation/view/progress_widget.py +2 -0
- ert/gui/simulation/view/realization.py +5 -1
- ert/gui/simulation/view/update.py +2 -0
- ert/gui/summarypanel.py +20 -1
- ert/gui/tools/event_viewer/panel.py +3 -4
- ert/gui/tools/event_viewer/tool.py +2 -0
- ert/gui/tools/load_results/load_results_panel.py +1 -1
- ert/gui/tools/load_results/load_results_tool.py +2 -0
- ert/gui/tools/manage_experiments/export_dialog.py +136 -0
- ert/gui/tools/manage_experiments/manage_experiments_panel.py +2 -0
- ert/gui/tools/manage_experiments/storage_info_widget.py +121 -16
- ert/gui/tools/manage_experiments/storage_widget.py +4 -3
- ert/gui/tools/plot/customize/color_chooser.py +5 -2
- ert/gui/tools/plot/customize/customize_plot_dialog.py +2 -0
- ert/gui/tools/plot/customize/default_customization_view.py +4 -0
- ert/gui/tools/plot/customize/limits_customization_view.py +3 -0
- ert/gui/tools/plot/customize/statistics_customization_view.py +3 -0
- ert/gui/tools/plot/customize/style_chooser.py +2 -0
- ert/gui/tools/plot/customize/style_customization_view.py +3 -0
- ert/gui/tools/plot/data_type_keys_widget.py +2 -0
- ert/gui/tools/plot/data_type_proxy_model.py +3 -0
- ert/gui/tools/plot/plot_api.py +50 -28
- ert/gui/tools/plot/plot_ensemble_selection_widget.py +17 -10
- ert/gui/tools/plot/plot_widget.py +15 -2
- ert/gui/tools/plot/plot_window.py +41 -19
- ert/gui/tools/plot/plottery/plot_config.py +2 -0
- ert/gui/tools/plot/plottery/plot_context.py +14 -0
- ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
- ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
- ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
- ert/gui/tools/plot/plottery/plots/ensemble.py +13 -5
- ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
- ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
- ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
- ert/gui/tools/plot/plottery/plots/observations.py +18 -4
- ert/gui/tools/plot/plottery/plots/statistics.py +62 -20
- ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
- ert/gui/tools/plot/widgets/clearable_line_edit.py +9 -0
- ert/gui/tools/plot/widgets/filter_popup.py +2 -0
- ert/gui/tools/plot/widgets/filterable_kw_list_model.py +3 -0
- ert/gui/tools/plugins/plugin.py +1 -1
- ert/gui/tools/plugins/plugins_tool.py +2 -0
- ert/gui/tools/plugins/process_job_dialog.py +3 -0
- ert/gui/tools/workflows/workflow_dialog.py +2 -0
- ert/gui/tools/workflows/workflows_tool.py +2 -0
- ert/libres_facade.py +5 -7
- ert/logging/__init__.py +4 -1
- ert/mode_definitions.py +2 -0
- ert/plugins/__init__.py +4 -6
- ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
- ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
- ert/plugins/hook_specifications/__init__.py +0 -10
- ert/plugins/hook_specifications/jobs.py +0 -9
- ert/plugins/plugin_manager.py +53 -124
- ert/resources/forward_models/run_reservoirsimulator.py +8 -4
- ert/resources/forward_models/template_render.py +10 -10
- ert/resources/shell_scripts/delete_directory.py +2 -2
- ert/run_models/__init__.py +24 -6
- ert/run_models/_create_run_path.py +133 -38
- ert/run_models/ensemble_experiment.py +10 -4
- ert/run_models/ensemble_information_filter.py +8 -1
- ert/run_models/ensemble_smoother.py +9 -3
- ert/run_models/evaluate_ensemble.py +8 -6
- ert/run_models/event.py +7 -3
- ert/run_models/everest_run_model.py +337 -113
- ert/run_models/initial_ensemble_run_model.py +25 -24
- ert/run_models/manual_update.py +6 -3
- ert/run_models/manual_update_enif.py +37 -0
- ert/run_models/model_factory.py +78 -18
- ert/run_models/multiple_data_assimilation.py +22 -11
- ert/run_models/run_model.py +72 -73
- ert/run_models/single_test_run.py +7 -4
- ert/run_models/update_run_model.py +4 -2
- ert/runpaths.py +5 -6
- ert/sample_prior.py +9 -4
- ert/scheduler/__init__.py +10 -5
- ert/scheduler/driver.py +40 -0
- ert/scheduler/event.py +3 -1
- ert/scheduler/job.py +23 -13
- ert/scheduler/lsf_driver.py +15 -5
- ert/scheduler/openpbs_driver.py +10 -4
- ert/scheduler/scheduler.py +5 -0
- ert/scheduler/slurm_driver.py +20 -5
- ert/services/__init__.py +2 -2
- ert/services/_base_service.py +37 -20
- ert/services/_storage_main.py +20 -18
- ert/services/ert_server.py +317 -0
- ert/shared/_doc_utils/__init__.py +4 -2
- ert/shared/_doc_utils/ert_jobs.py +1 -4
- ert/shared/net_utils.py +43 -18
- ert/shared/storage/connection.py +3 -3
- ert/shared/version.py +3 -3
- ert/storage/__init__.py +14 -1
- ert/storage/local_ensemble.py +44 -13
- ert/storage/local_experiment.py +54 -34
- ert/storage/local_storage.py +90 -58
- ert/storage/migration/to10.py +3 -2
- ert/storage/migration/to11.py +9 -10
- ert/storage/migration/to12.py +19 -20
- ert/storage/migration/to13.py +28 -27
- ert/storage/migration/to14.py +3 -3
- ert/storage/migration/to15.py +25 -0
- ert/storage/migration/to16.py +38 -0
- ert/storage/migration/to17.py +42 -0
- ert/storage/migration/to18.py +11 -0
- ert/storage/migration/to19.py +34 -0
- ert/storage/migration/to20.py +23 -0
- ert/storage/migration/to21.py +25 -0
- ert/storage/migration/to6.py +3 -2
- ert/storage/migration/to7.py +12 -13
- ert/storage/migration/to8.py +9 -11
- ert/storage/migration/to9.py +5 -4
- ert/storage/realization_storage_state.py +7 -7
- ert/substitutions.py +12 -28
- ert/validation/active_range.py +7 -7
- ert/validation/ensemble_realizations_argument.py +4 -2
- ert/validation/rangestring.py +16 -16
- ert/workflow_runner.py +6 -3
- {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/METADATA +21 -15
- ert-19.0.0rc2.dist-info/RECORD +524 -0
- {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/WHEEL +1 -1
- everest/api/everest_data_api.py +14 -1
- everest/assets/everest_logo.svg +406 -0
- everest/bin/config_branch_script.py +30 -14
- everest/bin/everconfigdump_script.py +2 -10
- everest/bin/everest_script.py +53 -33
- everest/bin/everlint_script.py +3 -5
- everest/bin/kill_script.py +7 -5
- everest/bin/main.py +11 -24
- everest/bin/monitor_script.py +64 -35
- everest/bin/utils.py +58 -43
- everest/bin/visualization_script.py +23 -13
- everest/config/__init__.py +4 -1
- everest/config/control_config.py +81 -6
- everest/config/control_variable_config.py +4 -3
- everest/config/everest_config.py +102 -79
- everest/config/forward_model_config.py +5 -3
- everest/config/install_data_config.py +7 -5
- everest/config/install_job_config.py +45 -3
- everest/config/install_template_config.py +3 -3
- everest/config/optimization_config.py +19 -6
- everest/config/output_constraint_config.py +8 -2
- everest/config/server_config.py +6 -55
- everest/config/simulator_config.py +62 -17
- everest/config/utils.py +25 -105
- everest/config/validation_utils.py +34 -15
- everest/config_file_loader.py +30 -21
- everest/detached/__init__.py +0 -6
- everest/detached/client.py +7 -52
- everest/detached/everserver.py +19 -45
- everest/everest_storage.py +24 -40
- everest/gui/everest_client.py +2 -3
- everest/gui/main_window.py +2 -2
- everest/optimizer/everest2ropt.py +68 -42
- everest/optimizer/opt_model_transforms.py +15 -20
- everest/optimizer/utils.py +0 -29
- everest/plugins/hook_specs.py +0 -24
- everest/strings.py +1 -6
- everest/util/__init__.py +3 -1
- ert/config/everest_objective_config.py +0 -95
- ert/config/ext_param_config.py +0 -107
- ert/gui/tools/export/__init__.py +0 -3
- ert/gui/tools/export/export_panel.py +0 -83
- ert/gui/tools/export/export_tool.py +0 -67
- ert/gui/tools/export/exporter.py +0 -36
- ert/plugins/hook_specifications/ecl_config.py +0 -29
- ert/services/storage_service.py +0 -127
- ert/summary_key_type.py +0 -234
- ert-16.0.9.dist-info/RECORD +0 -521
- everest/bin/everexport_script.py +0 -53
- everest/config/sampler_config.py +0 -103
- everest/simulator/__init__.py +0 -88
- everest/simulator/everest_to_ert.py +0 -252
- /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
- /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
- {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/entry_points.txt +0 -0
- {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/licenses/COPYING +0 -0
- {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -6,7 +6,7 @@ import numpy as np
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from scipy.stats import gaussian_kde
|
|
8
8
|
|
|
9
|
-
from ert.gui.tools.plot.plot_api import EnsembleObject
|
|
9
|
+
from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
|
|
10
10
|
|
|
11
11
|
from .plot_tools import ConditionalAxisFormatter, PlotTools
|
|
12
12
|
|
|
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
class GaussianKDEPlot:
|
|
22
22
|
def __init__(self) -> None:
|
|
23
23
|
self.dimensionality = 1
|
|
24
|
+
self.requires_observations = False
|
|
24
25
|
|
|
25
26
|
@staticmethod
|
|
26
27
|
def plot(
|
|
@@ -29,10 +30,16 @@ class GaussianKDEPlot:
|
|
|
29
30
|
ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
|
|
30
31
|
observation_data: pd.DataFrame,
|
|
31
32
|
std_dev_images: dict[str, npt.NDArray[np.float32]],
|
|
33
|
+
key_def: PlotApiKeyDefinition | None = None,
|
|
32
34
|
) -> None:
|
|
33
35
|
plotGaussianKDE(figure, plot_context, ensemble_to_data_map, observation_data)
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
def _array_is_constant(data: pd.Series | pd.DataFrame) -> bool:
|
|
39
|
+
array = data.to_numpy()
|
|
40
|
+
return array.shape[0] == 0 or (array[0] == array).all()
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
def plotGaussianKDE(
|
|
37
44
|
figure: Figure,
|
|
38
45
|
plot_context: PlotContext,
|
|
@@ -55,11 +62,14 @@ def plotGaussianKDE(
|
|
|
55
62
|
if data.empty:
|
|
56
63
|
continue
|
|
57
64
|
data = data[0]
|
|
58
|
-
if data
|
|
65
|
+
if not _array_is_constant(data):
|
|
59
66
|
_plotGaussianKDE(
|
|
60
67
|
axes, config, data, f"{ensemble.experiment_name} : {ensemble.name}"
|
|
61
68
|
)
|
|
62
69
|
|
|
70
|
+
if plot_context.log_scale:
|
|
71
|
+
axes.set_xscale("log")
|
|
72
|
+
|
|
63
73
|
PlotTools.finalizePlot(
|
|
64
74
|
plot_context, figure, axes, default_x_label="Value", default_y_label="Density"
|
|
65
75
|
)
|
|
@@ -8,7 +8,7 @@ import numpy as np
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
from matplotlib.patches import Rectangle
|
|
10
10
|
|
|
11
|
-
from ert.gui.tools.plot.plot_api import EnsembleObject
|
|
11
|
+
from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
|
|
12
12
|
from ert.shared.status.utils import convert_to_numeric
|
|
13
13
|
|
|
14
14
|
from .plot_tools import ConditionalAxisFormatter, PlotTools
|
|
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
|
|
24
24
|
class HistogramPlot:
|
|
25
25
|
def __init__(self) -> None:
|
|
26
26
|
self.dimensionality = 1
|
|
27
|
+
self.requires_observations = False
|
|
27
28
|
|
|
28
29
|
@staticmethod
|
|
29
30
|
def plot(
|
|
@@ -32,6 +33,7 @@ class HistogramPlot:
|
|
|
32
33
|
ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
|
|
33
34
|
observation_data: pd.DataFrame,
|
|
34
35
|
std_dev_images: dict[str, npt.NDArray[np.float32]],
|
|
36
|
+
key_def: PlotApiKeyDefinition | None = None,
|
|
35
37
|
) -> None:
|
|
36
38
|
plotHistogram(figure, plot_context, ensemble_to_data_map)
|
|
37
39
|
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
4
|
+
|
|
5
|
+
import matplotlib.dates as mdates
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
import seaborn as sns
|
|
10
|
+
from matplotlib import pyplot as plt
|
|
11
|
+
from matplotlib.lines import Line2D
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
|
|
17
|
+
from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
|
|
18
|
+
from ert.gui.tools.plot.plottery import PlotContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MisfitsPlot:
|
|
22
|
+
"""
|
|
23
|
+
Visualize signed chi-squared misfits between simulated responses and observations.
|
|
24
|
+
|
|
25
|
+
Layout:
|
|
26
|
+
- X-axis: index for gen data, time for summary
|
|
27
|
+
- Y-axis: signed chi-squared misfits
|
|
28
|
+
- Glyphs: One boxplot per time step / gendata index
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.dimensionality = 2
|
|
33
|
+
self.requires_observations = True
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _fade_axis_ticklabels(ax: plt.Axes) -> None:
|
|
37
|
+
"""Apply the same alpha to all tick labels on an axis."""
|
|
38
|
+
for label in (*ax.get_xticklabels(), *ax.get_yticklabels()):
|
|
39
|
+
label.set_alpha(0.4)
|
|
40
|
+
|
|
41
|
+
ax.xaxis.label.set_alpha(0.4)
|
|
42
|
+
ax.yaxis.label.set_alpha(0.4)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _draw_legend(
|
|
46
|
+
figure: Figure,
|
|
47
|
+
ensemble_colors: dict[tuple[str, str], str],
|
|
48
|
+
sorted_ensemble_keys: list[tuple[str, str]],
|
|
49
|
+
) -> None:
|
|
50
|
+
legend_handles = [
|
|
51
|
+
Line2D(
|
|
52
|
+
[],
|
|
53
|
+
[],
|
|
54
|
+
marker="s",
|
|
55
|
+
linestyle="None",
|
|
56
|
+
color=ensemble_colors[key],
|
|
57
|
+
label=key[0],
|
|
58
|
+
)
|
|
59
|
+
for key in sorted_ensemble_keys
|
|
60
|
+
]
|
|
61
|
+
figure.legend(
|
|
62
|
+
handles=legend_handles,
|
|
63
|
+
loc="upper center",
|
|
64
|
+
bbox_to_anchor=(0.5, 0.93),
|
|
65
|
+
ncol=min(len(legend_handles), 4),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _make_ensemble_colors(
|
|
70
|
+
sorted_ensemble_keys: list[tuple[str, str]],
|
|
71
|
+
) -> dict[tuple[str, str], str]:
|
|
72
|
+
"""
|
|
73
|
+
Build a consistent color map and figure-level legend
|
|
74
|
+
used by all misfit plots
|
|
75
|
+
"""
|
|
76
|
+
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
77
|
+
return {
|
|
78
|
+
key: colors[i % len(colors)] for i, key in enumerate(sorted_ensemble_keys)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _compute_misfits_padded_minmax(
|
|
83
|
+
misfits_df: pl.DataFrame, relative_pad_y_axis: float
|
|
84
|
+
) -> tuple[float, float]:
|
|
85
|
+
y_min, y_max = (
|
|
86
|
+
cast(float, misfits_df["misfit"].min()),
|
|
87
|
+
cast(float, misfits_df["misfit"].max()),
|
|
88
|
+
)
|
|
89
|
+
abs_pad_y = (y_max - y_min) * relative_pad_y_axis
|
|
90
|
+
y_min -= abs_pad_y
|
|
91
|
+
y_max += abs_pad_y
|
|
92
|
+
|
|
93
|
+
return y_min, y_max
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _wide_pandas_to_long_polars_with_misfits(
|
|
97
|
+
ensemble_to_data_map: dict[tuple[str, str], pd.DataFrame],
|
|
98
|
+
observation_data: pd.DataFrame,
|
|
99
|
+
response_type: Literal["summary", "gen_data"],
|
|
100
|
+
) -> dict[tuple[str, str], pl.DataFrame]:
|
|
101
|
+
if response_type == "summary":
|
|
102
|
+
key_index_with_correct_dtype = pl.col("key_index").str.to_datetime(
|
|
103
|
+
strict=False
|
|
104
|
+
)
|
|
105
|
+
elif response_type == "gen_data":
|
|
106
|
+
key_index_with_correct_dtype = (
|
|
107
|
+
pl.col("key_index").cast(pl.Float32).cast(pl.UInt16)
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(f"Unsupported response_type: {response_type}")
|
|
111
|
+
|
|
112
|
+
obs_df = (
|
|
113
|
+
pl.from_pandas(observation_data.T)
|
|
114
|
+
.rename({"OBS": "observation", "STD": "error"})
|
|
115
|
+
.with_columns(pl.col("key_index").cast(pl.String))
|
|
116
|
+
.with_columns(key_index_with_correct_dtype)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ens_key: (
|
|
121
|
+
pl.from_pandas(df, include_index=True)
|
|
122
|
+
.unpivot(
|
|
123
|
+
index=df.index.name,
|
|
124
|
+
variable_name="key_index",
|
|
125
|
+
value_name="response",
|
|
126
|
+
)
|
|
127
|
+
.with_columns(key_index_with_correct_dtype)
|
|
128
|
+
.join(obs_df, on="key_index", how="inner")
|
|
129
|
+
.with_columns(
|
|
130
|
+
(pl.col("response") - pl.col("observation")).alias("residual")
|
|
131
|
+
)
|
|
132
|
+
.with_columns(
|
|
133
|
+
(
|
|
134
|
+
pl.col("residual").sign()
|
|
135
|
+
* (pl.col("residual") / pl.col("error")).pow(2)
|
|
136
|
+
).alias("misfit")
|
|
137
|
+
)
|
|
138
|
+
.drop("residual")
|
|
139
|
+
)
|
|
140
|
+
for ens_key, df in ensemble_to_data_map.items()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def plot(
|
|
144
|
+
self,
|
|
145
|
+
figure: Figure,
|
|
146
|
+
plot_context: PlotContext,
|
|
147
|
+
ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
|
|
148
|
+
observation_data: pd.DataFrame,
|
|
149
|
+
std_dev_images: dict[str, npt.NDArray[np.float32]],
|
|
150
|
+
key_def: PlotApiKeyDefinition | None = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
assert key_def is not None
|
|
153
|
+
response_type = key_def.metadata["data_origin"]
|
|
154
|
+
data_with_misfits = self._wide_pandas_to_long_polars_with_misfits(
|
|
155
|
+
{(eo.name, eo.id): df for eo, df in ensemble_to_data_map.items()},
|
|
156
|
+
observation_data,
|
|
157
|
+
response_type,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if response_type == "summary":
|
|
161
|
+
self._plot_summary_misfits_boxplots(
|
|
162
|
+
figure,
|
|
163
|
+
data_with_misfits,
|
|
164
|
+
plot_context,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
elif response_type == "gen_data":
|
|
168
|
+
self._plot_gendata_misfits(
|
|
169
|
+
figure,
|
|
170
|
+
data_with_misfits,
|
|
171
|
+
plot_context,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _plot_gendata_misfits(
|
|
175
|
+
self,
|
|
176
|
+
figure: Figure,
|
|
177
|
+
data_with_misfits: dict[tuple[str, str], pl.DataFrame],
|
|
178
|
+
plot_context: PlotContext,
|
|
179
|
+
) -> None:
|
|
180
|
+
# Only plot ensembles with data (i.e., they pertain to an experiment
|
|
181
|
+
# with observations, and there are responses towards those in the ens)
|
|
182
|
+
ensemble_to_misfit_df = {
|
|
183
|
+
k: v for k, v in data_with_misfits.items() if not v.is_empty()
|
|
184
|
+
}
|
|
185
|
+
sorted_ensemble_keys = sorted(ensemble_to_misfit_df.keys())
|
|
186
|
+
|
|
187
|
+
all_misfits = pl.concat(
|
|
188
|
+
[
|
|
189
|
+
df.with_columns(pl.lit(key).alias("ensemble_key"))
|
|
190
|
+
for key, df in ensemble_to_misfit_df.items()
|
|
191
|
+
]
|
|
192
|
+
).select(["realization", "key_index", "misfit", "ensemble_key"])
|
|
193
|
+
|
|
194
|
+
distinct_gendata_index = all_misfits["key_index"].unique().sort().to_list()
|
|
195
|
+
num_gendata_index = len(distinct_gendata_index)
|
|
196
|
+
|
|
197
|
+
color_map = self._make_ensemble_colors(sorted_ensemble_keys)
|
|
198
|
+
self._draw_legend(
|
|
199
|
+
figure=figure,
|
|
200
|
+
ensemble_colors=color_map,
|
|
201
|
+
sorted_ensemble_keys=sorted_ensemble_keys,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
y_min, y_max = self._compute_misfits_padded_minmax(all_misfits, 0.05)
|
|
205
|
+
|
|
206
|
+
# Create subplot grid (2 rows, N columns)
|
|
207
|
+
axes = figure.subplots(
|
|
208
|
+
nrows=2, ncols=num_gendata_index, sharex="col", sharey=True
|
|
209
|
+
)
|
|
210
|
+
axes = (
|
|
211
|
+
axes.reshape(2, num_gendata_index)
|
|
212
|
+
if num_gendata_index > 1
|
|
213
|
+
else np.array([[axes[0]], [axes[1]]])
|
|
214
|
+
)
|
|
215
|
+
axes_top, axes_bottom = axes[0, :], axes[1, :]
|
|
216
|
+
|
|
217
|
+
x_positions = np.arange(len(sorted_ensemble_keys))
|
|
218
|
+
box_width_relative = 0.6
|
|
219
|
+
|
|
220
|
+
for col_idx, key_index in enumerate(distinct_gendata_index):
|
|
221
|
+
ax_top, ax_bottom = axes_top[col_idx], axes_bottom[col_idx]
|
|
222
|
+
|
|
223
|
+
for ens_idx, ens_key in enumerate(sorted_ensemble_keys):
|
|
224
|
+
color = color_map.get(ens_key, "C0")
|
|
225
|
+
|
|
226
|
+
# Filter for the specific key and ensemble
|
|
227
|
+
mis_vals = all_misfits.filter(
|
|
228
|
+
(pl.col("key_index") == key_index)
|
|
229
|
+
& (pl.col("ensemble_key") == ens_key)
|
|
230
|
+
)["misfit"].to_numpy()
|
|
231
|
+
|
|
232
|
+
if mis_vals.size == 0:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
x_center = x_positions[ens_idx]
|
|
236
|
+
|
|
237
|
+
# Top: Boxplot
|
|
238
|
+
ax_top.boxplot(
|
|
239
|
+
mis_vals,
|
|
240
|
+
positions=[x_center],
|
|
241
|
+
widths=box_width_relative,
|
|
242
|
+
patch_artist=True,
|
|
243
|
+
showfliers=False,
|
|
244
|
+
boxprops={"facecolor": color, "alpha": 0.35},
|
|
245
|
+
whiskerprops={"color": color, "alpha": 0.8},
|
|
246
|
+
capprops={"color": color, "alpha": 0.8},
|
|
247
|
+
medianprops={"color": color, "alpha": 0.8},
|
|
248
|
+
)
|
|
249
|
+
ax_top.plot(x_center, np.mean(mis_vals), "o", markersize=4, color=color)
|
|
250
|
+
|
|
251
|
+
# Bottom: Strip plot with dynamic marker size
|
|
252
|
+
num_points = len(mis_vals)
|
|
253
|
+
|
|
254
|
+
if num_points >= 200:
|
|
255
|
+
marker_size = 2
|
|
256
|
+
elif num_points >= 100:
|
|
257
|
+
marker_size = 3
|
|
258
|
+
else:
|
|
259
|
+
marker_size = 4
|
|
260
|
+
|
|
261
|
+
# Use stripplot as a robust alternative for dense data
|
|
262
|
+
sns.stripplot(
|
|
263
|
+
x=[x_center] * num_points, # Plot all points at the same x-center
|
|
264
|
+
y=mis_vals,
|
|
265
|
+
ax=ax_bottom,
|
|
266
|
+
color=color,
|
|
267
|
+
size=marker_size,
|
|
268
|
+
alpha=0.35,
|
|
269
|
+
jitter=True, # Explicitly add jitter
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Axis/spine styling
|
|
273
|
+
(n_rows, n_cols) = axes.shape
|
|
274
|
+
for r_idx in range(n_rows):
|
|
275
|
+
for c_idx in range(n_cols):
|
|
276
|
+
ax = axes[r_idx, c_idx]
|
|
277
|
+
is_first_col = c_idx == 0
|
|
278
|
+
is_bottom_row = r_idx == (n_rows - 1)
|
|
279
|
+
|
|
280
|
+
# Common styling
|
|
281
|
+
ax.set(ylim=(y_min, y_max))
|
|
282
|
+
ax.axhline(0.0, color="black", linewidth=0.5, alpha=0.5)
|
|
283
|
+
ax.grid(True, axis="y", linestyle=":", alpha=0.4)
|
|
284
|
+
self._fade_axis_ticklabels(ax)
|
|
285
|
+
|
|
286
|
+
# Spines
|
|
287
|
+
ax.spines["top"].set_visible(False)
|
|
288
|
+
ax.spines["right"].set_visible(False)
|
|
289
|
+
ax.spines["left"].set_visible(is_first_col)
|
|
290
|
+
ax.spines["bottom"].set_visible(is_bottom_row)
|
|
291
|
+
|
|
292
|
+
# Ticks
|
|
293
|
+
ax.set_xticks(x_positions, labels=[])
|
|
294
|
+
if not is_bottom_row:
|
|
295
|
+
ax.tick_params(axis="x", which="both", bottom=False)
|
|
296
|
+
if not is_first_col:
|
|
297
|
+
ax.tick_params(axis="y", which="both", left=False)
|
|
298
|
+
|
|
299
|
+
for ax, key_val in zip(axes_bottom, distinct_gendata_index, strict=True):
|
|
300
|
+
ax.set_xlabel(f"index={int(key_val)}", rotation=25, ha="right")
|
|
301
|
+
|
|
302
|
+
figure.suptitle(
|
|
303
|
+
f"{plot_context.key()} (Signed Chi-squared misfits per index)",
|
|
304
|
+
fontsize=14,
|
|
305
|
+
y=0.98,
|
|
306
|
+
)
|
|
307
|
+
figure.tight_layout(rect=(0.02, 0.02, 0.98, 0.88))
|
|
308
|
+
|
|
309
|
+
def _plot_summary_misfits_boxplots(
|
|
310
|
+
self,
|
|
311
|
+
figure: Figure,
|
|
312
|
+
data_with_misfits: dict[tuple[str, str], pl.DataFrame],
|
|
313
|
+
plot_context: PlotContext,
|
|
314
|
+
) -> None:
|
|
315
|
+
# Calculate shared y-axis limits from all misfits
|
|
316
|
+
all_misfits = pl.concat(
|
|
317
|
+
[df.select("misfit") for df in data_with_misfits.values()]
|
|
318
|
+
)
|
|
319
|
+
y_min, y_max = self._compute_misfits_padded_minmax(all_misfits, 0.05)
|
|
320
|
+
|
|
321
|
+
# Prepare ensemble colors and draw the legend
|
|
322
|
+
sorted_ensemble_keys = sorted(data_with_misfits.keys())
|
|
323
|
+
color_map = self._make_ensemble_colors(sorted_ensemble_keys)
|
|
324
|
+
self._draw_legend(
|
|
325
|
+
figure=figure,
|
|
326
|
+
ensemble_colors=color_map,
|
|
327
|
+
sorted_ensemble_keys=sorted_ensemble_keys,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Create all subplots at once with shared axes
|
|
331
|
+
n_ens = len(sorted_ensemble_keys)
|
|
332
|
+
axes = figure.subplots(nrows=n_ens, ncols=1, sharex=True, sharey=True)
|
|
333
|
+
axes = [axes] if n_ens == 1 else axes.tolist()
|
|
334
|
+
axes[0].set_ylim(y_min, y_max)
|
|
335
|
+
|
|
336
|
+
for ax, ensemble_key in zip(axes, sorted_ensemble_keys, strict=True):
|
|
337
|
+
df = data_with_misfits[ensemble_key]
|
|
338
|
+
if df.is_empty():
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
df = df.select(["key_index", "misfit"]).sort("key_index")
|
|
342
|
+
times_py = df["key_index"].unique(maintain_order=True).to_list()
|
|
343
|
+
positions = mdates.date2num(times_py) # type: ignore[no-untyped-call]
|
|
344
|
+
|
|
345
|
+
# Calculate dynamic box width based on time spacing
|
|
346
|
+
min_dt = np.min(np.diff(positions)) if len(positions) > 1 else 1.0
|
|
347
|
+
|
|
348
|
+
# multiplier to downsize outlier sizes etc
|
|
349
|
+
# (without this, outliers, whiskers etc are sized way
|
|
350
|
+
# out of proportion when there are many tiny boxplots)
|
|
351
|
+
many_boxes_factor = min(1, len(times_py) / 50)
|
|
352
|
+
box_width = min_dt * (0.7 - 0.3 * (1 - many_boxes_factor))
|
|
353
|
+
|
|
354
|
+
# One boxplot per time step
|
|
355
|
+
grouped_misfits = df.group_by("key_index", maintain_order=True).agg(
|
|
356
|
+
pl.col("misfit")
|
|
357
|
+
)
|
|
358
|
+
data_for_boxes = [
|
|
359
|
+
s.to_numpy() if s.len() > 0 else np.array([np.nan])
|
|
360
|
+
for s in grouped_misfits["misfit"]
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
color = color_map.get(ensemble_key)
|
|
364
|
+
|
|
365
|
+
# Draw the boxplots with inlined styles
|
|
366
|
+
bp = ax.boxplot(
|
|
367
|
+
data_for_boxes,
|
|
368
|
+
positions=positions,
|
|
369
|
+
widths=box_width,
|
|
370
|
+
whis=(5, 95),
|
|
371
|
+
showfliers=True,
|
|
372
|
+
manage_ticks=False,
|
|
373
|
+
patch_artist=True,
|
|
374
|
+
boxprops={
|
|
375
|
+
"facecolor": color,
|
|
376
|
+
"alpha": 0.18,
|
|
377
|
+
"edgecolor": color,
|
|
378
|
+
"linewidth": 0.7,
|
|
379
|
+
},
|
|
380
|
+
whiskerprops={"color": color, "alpha": 0.6, "linewidth": 0.7},
|
|
381
|
+
capprops={"color": color, "alpha": 0.6, "linewidth": 0.7},
|
|
382
|
+
medianprops={"color": color, "linewidth": 0.6, "alpha": 0.9},
|
|
383
|
+
flierprops={
|
|
384
|
+
"marker": "o",
|
|
385
|
+
"markersize": min(6, box_width * (0.4 - (0.2 * many_boxes_factor))),
|
|
386
|
+
"alpha": 0.7,
|
|
387
|
+
"markeredgewidth": 0.3 + (0.4 * (1 - many_boxes_factor)),
|
|
388
|
+
"markeredgecolor": color,
|
|
389
|
+
"markerfacecolor": "none",
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
plt.setp(bp["fliers"], zorder=1.5) # Put fliers behind other elements
|
|
393
|
+
|
|
394
|
+
# Add ensemble name text and a horizontal line at y=0
|
|
395
|
+
ax.text(
|
|
396
|
+
0.01,
|
|
397
|
+
0.85,
|
|
398
|
+
f"{ensemble_key[0]}",
|
|
399
|
+
transform=ax.transAxes,
|
|
400
|
+
ha="left",
|
|
401
|
+
va="center",
|
|
402
|
+
fontsize=9,
|
|
403
|
+
alpha=0.75,
|
|
404
|
+
)
|
|
405
|
+
ax.axhline(0.0, color="black", linewidth=0.8, alpha=0.4, zorder=0)
|
|
406
|
+
|
|
407
|
+
# Apply common styling to all axes
|
|
408
|
+
for ax in axes:
|
|
409
|
+
ax.spines["top"].set_visible(False)
|
|
410
|
+
ax.spines["right"].set_visible(False)
|
|
411
|
+
ax.spines["left"].set_visible(True)
|
|
412
|
+
ax.tick_params(axis="y", labelsize=10, width=0.5, length=3)
|
|
413
|
+
ax.grid(True, axis="y", linestyle=":", linewidth=0.5, alpha=0.75)
|
|
414
|
+
self._fade_axis_ticklabels(ax)
|
|
415
|
+
|
|
416
|
+
# Hide the x-axis on all but the last plot
|
|
417
|
+
for ax in axes[:-1]:
|
|
418
|
+
ax.spines["bottom"].set_visible(False)
|
|
419
|
+
ax.tick_params(axis="x", which="both", bottom=False, labelbottom=False)
|
|
420
|
+
|
|
421
|
+
# Style the x-axis only on the last plot
|
|
422
|
+
bottom_ax = axes[-1]
|
|
423
|
+
bottom_ax.spines["bottom"].set_visible(True)
|
|
424
|
+
bottom_ax.xaxis.set_major_locator(mdates.AutoDateLocator()) # type: ignore[no-untyped-call]
|
|
425
|
+
bottom_ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d")) # type: ignore[no-untyped-call]
|
|
426
|
+
|
|
427
|
+
bottom_ax.tick_params(axis="x", labelsize=8, width=0.5, length=3)
|
|
428
|
+
plt.setp(bottom_ax.get_xticklabels(), rotation=25, ha="right")
|
|
429
|
+
|
|
430
|
+
figure.suptitle(
|
|
431
|
+
f"{plot_context.key()} (Signed Chi-squared misfits over time)",
|
|
432
|
+
fontsize=14,
|
|
433
|
+
y=0.97,
|
|
434
|
+
alpha=0.9,
|
|
435
|
+
)
|
|
436
|
+
figure.tight_layout(rect=(0.02, 0.02, 0.98, 0.92))
|
|
@@ -27,7 +27,10 @@ def plotObservations(
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _plotObservations(
|
|
30
|
-
axes: Axes,
|
|
30
|
+
axes: Axes,
|
|
31
|
+
plot_config: PlotConfig,
|
|
32
|
+
data: DataFrame,
|
|
33
|
+
value_column: str,
|
|
31
34
|
) -> None:
|
|
32
35
|
"""
|
|
33
36
|
Observations are always plotted on top. z-order set to 1000
|
|
@@ -46,11 +49,22 @@ def _plotObservations(
|
|
|
46
49
|
# line style set to 'off' toggles errorbar visibility
|
|
47
50
|
if not style.line_style:
|
|
48
51
|
style.width = 0
|
|
52
|
+
if plot_config.depth_y_axis:
|
|
53
|
+
errorbar_data = {
|
|
54
|
+
"x": data.loc["OBS"].to_numpy(),
|
|
55
|
+
"y": data.loc["key_index"].to_numpy(),
|
|
56
|
+
"xerr": data.loc["STD"].to_numpy(),
|
|
57
|
+
}
|
|
58
|
+
axes.yaxis.set_inverted(True)
|
|
59
|
+
else:
|
|
60
|
+
errorbar_data = {
|
|
61
|
+
"x": data.loc["key_index"].to_numpy(),
|
|
62
|
+
"y": data.loc["OBS"].to_numpy(),
|
|
63
|
+
"yerr": data.loc["STD"].to_numpy(),
|
|
64
|
+
}
|
|
49
65
|
|
|
50
66
|
axes.errorbar(
|
|
51
|
-
|
|
52
|
-
y=data.loc["OBS"].values,
|
|
53
|
-
yerr=data.loc["STD"].values,
|
|
67
|
+
**errorbar_data,
|
|
54
68
|
fmt=style.line_style,
|
|
55
69
|
ecolor=style.color,
|
|
56
70
|
color=style.color,
|