Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ert/gui/plotting/ert_plots/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from .cesp import CrossEnsembleStatisticsPlot
from .distribution import DistributionPlot
from .histogram import HistogramPlot
from .misfit_map import MisfitMapPlot
from .misfits import MisfitsPlot
from .observations_map import ObservationsMapPlot
from .statistics import StatisticsPlot
from .std_dev import StdDevPlot

__all__ = [
"CrossEnsembleStatisticsPlot",
"DistributionPlot",
"HistogramPlot",
"MisfitMapPlot",
"MisfitsPlot",
"ObservationsMapPlot",
"StatisticsPlot",
"StdDevPlot",
]
97 changes: 97 additions & 0 deletions src/ert/gui/plotting/ert_plots/misfit_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import numpy.typing as npt
import pandas as pd
import polars as pl
from matplotlib.figure import Figure

from ert.gui.plotting.ert_plots.misfits import MisfitsPlot

if TYPE_CHECKING:
from ert.gui.plotting.plot_api import EnsembleObject, PlotApiKeyDefinition
from ert.gui.plotting.utils import PlotContext
from ert.gui.plotting.utils.plot_types import ObservationPlotLocations


class MisfitMapPlot:
def __init__(self) -> None:
self.dimensionality = 2
self.requires_observations = True

@staticmethod
def _show_no_data(figure: Figure, message: str) -> None:
axes = figure.add_subplot(111)
axes.text(0.5, 0.5, message, ha="center", va="center")
axes.set_axis_off()

def plot(
self,
figure: Figure,
plot_context: PlotContext,
ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
observation_data: pd.DataFrame,
std_dev_images: dict[str, npt.NDArray[np.float32]],
obs_loc: ObservationPlotLocations | None,
key_def: PlotApiKeyDefinition | None = None,
) -> None:

if not ensemble_to_data_map:
self._show_no_data(figure, "No ensemble data available")
return

if len(ensemble_to_data_map) > 1:
self._show_no_data(
figure, "Multiple ensembles selected; misfit map supports one at a time"
)
return

ensemble, ensemble_data = next(iter(ensemble_to_data_map.items()))
misfits_by_realization = MisfitsPlot._wide_pandas_to_long_polars_with_misfits(
{(ensemble.name, ensemble.id): ensemble_data},
observation_data,
"seismic",
)[ensemble.name, ensemble.id]

if misfits_by_realization.is_empty():
self._show_no_data(figure, "No misfit data available")
return

mean_misfits = misfits_by_realization.group_by(["EAST", "NORTH"]).agg(
pl.col("misfit").mean()
)
east = mean_misfits["EAST"].to_numpy()
north = mean_misfits["NORTH"].to_numpy()
misfit_values = mean_misfits["misfit"].to_numpy()
axes_misfit = figure.add_subplot(111)

misfit_tripcolor = axes_misfit.tripcolor(
east, north, misfit_values, shading="flat", cmap="viridis"
)

cbar = figure.colorbar(
misfit_tripcolor,
ax=axes_misfit,
label="Mean signed χ²",
orientation="vertical",
pad=0.15,
aspect=40,
)

cbar.ax.set_visible(plot_context.plotConfig().is_legend_enabled())
cbar.ax.ticklabel_format(useOffset=False, style="plain")
config = plot_context.plotConfig()
axes_misfit.spines["top"].set_visible(False)
axes_misfit.spines["right"].set_visible(False)
axes_misfit.spines["left"].set_visible(False)
axes_misfit.spines["bottom"].set_visible(False)
axes_misfit.set_title(config.title())
axes_misfit.ticklabel_format(useOffset=False, style="plain")
axes_misfit.set_aspect("equal")
axes_misfit.set_xlabel(config.x_label() or "east coordinate")
axes_misfit.set_ylabel(config.y_label() or "north coordinate")
axes_misfit.grid(config.is_grid_enabled())
axes_misfit.set_xlim(east.min(), east.max())
axes_misfit.set_ylim(north.min(), north.max())
78 changes: 78 additions & 0 deletions src/ert/gui/plotting/ert_plots/observations_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import numpy.typing as npt
import pandas as pd
import polars as pl
from matplotlib.figure import Figure

if TYPE_CHECKING:
from ert.gui.plotting.plot_api import EnsembleObject, PlotApiKeyDefinition
from ert.gui.plotting.utils import PlotContext
from ert.gui.plotting.utils.plot_types import ObservationPlotLocations


class ObservationsMapPlot:
def __init__(self) -> None:
self.dimensionality = 2
self.requires_observations = True

@staticmethod
def _show_no_data(figure: Figure, message: str) -> None:
axes = figure.add_subplot(111)
axes.text(0.5, 0.5, message, ha="center", va="center")
axes.set_axis_off()

def plot(
self,
figure: Figure,
plot_context: PlotContext,
ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
observation_data: pd.DataFrame,
std_dev_images: dict[str, npt.NDArray[np.float32]],
obs_loc: ObservationPlotLocations | None,
key_def: PlotApiKeyDefinition | None = None,
) -> None:
if observation_data.empty:
self._show_no_data(figure, "No observation data available")
return

observation = pl.from_pandas(observation_data.T).rename(
{"EAST": "EAST", "NORTH": "NORTH", "OBS": "OBS"}
)

east = observation.get_column("EAST").to_numpy()
north = observation.get_column("NORTH").to_numpy()
observation_values = observation.get_column("OBS").to_numpy()

axes = figure.add_subplot(111)
observation_tripcolor = axes.tripcolor(
east, north, observation_values, shading="flat", cmap="viridis"
)

cbar = figure.colorbar(
observation_tripcolor,
ax=axes,
label="Observation value",
orientation="vertical",
pad=0.15,
aspect=40,
)

cbar.ax.set_visible(plot_context.plotConfig().is_legend_enabled())
cbar.ax.ticklabel_format(useOffset=False, style="plain")
config = plot_context.plotConfig()
axes.spines["top"].set_visible(False)
axes.spines["right"].set_visible(False)
axes.spines["left"].set_visible(False)
axes.spines["bottom"].set_visible(False)
axes.set_title(config.title())
axes.ticklabel_format(useOffset=False, style="plain")
axes.set_aspect("equal")
axes.set_xlabel(config.x_label() or "east coordinate")
axes.set_ylabel(config.y_label() or "north coordinate")
axes.set_xlim(east.min(), east.max())
axes.grid(config.is_grid_enabled())
axes.set_ylim(north.min(), north.max())
4 changes: 4 additions & 0 deletions src/ert/gui/plotting/plot_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ def observations_for_key(self, ensemble_ids: list[str], key: str) -> pd.DataFram
"STD": obs["errors"],
"OBS": obs["values"],
"key_index": key_index,
"EAST": obs.get("east")
or [float("nan")] * len(obs["values"]),
"NORTH": obs.get("north")
or [float("nan")] * len(obs["values"]),
}
)
)
Expand Down
45 changes: 35 additions & 10 deletions src/ert/gui/plotting/plot_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
EVEREST_PLOT_MAP,
GAUSSIAN_KDE,
HISTOGRAM,
MISFIT_MAP,
MISFITS,
OBSERVATIONS_MAP,
SHARED_PLOT_MAP,
STATISTICS,
STD_DEV,
Expand Down Expand Up @@ -385,10 +387,30 @@ def _show_no_data_message(self) -> None:
def get_plot_api_version(self) -> str:
return self._api.api_version

def _apply_ensemble_selection_policy_for_tab(self, tab_name: str) -> None:
if tab_name in {MISFIT_MAP, OBSERVATIONS_MAP}:
self._ensemble_selection_widget.set_maximum_ensemble_limit(1)
if len(self._ensemble_selection_widget.get_selected_ensembles()) > 1:
self._ensemble_selection_widget.clear_ensemble_selection()
else:
self._ensemble_selection_widget.reset_maximum_ensemble_limit_to_default()
self._update_ensemble_group_title()

def _update_ensemble_group_title(self) -> None:
max_selected = self._ensemble_selection_widget.get_maximum_ensemble_limit()
str_num_of_ens = f" up to {max_selected}" if self.is_everest else ""
self._ensemble_group.set_title(
f"Select{str_num_of_ens} batches"
if self.is_everest
else f"Select up to {max_selected} ensemble(s)"
)

@Slot(int)
def current_tab_changed(self, index: int) -> None:
tab_name = self._central_tab.tabText(index)
self._apply_ensemble_selection_policy_for_tab(tab_name)
self.update_plot()
self.log_plot_tab_usage(self._central_tab.tabText(index))
self.log_plot_tab_usage(tab_name)

def log_plot_tab_usage(self, tab_name: str, *, default: bool = False) -> None:
msg = f"Plotwindow tab used: {tab_name}" + (" (default tab)" if default else "")
Expand Down Expand Up @@ -592,7 +614,10 @@ def fetch_data(
plot_context,
history_data_available=history_data_available,
has_observations=key_def.observations,
show_observations=key_def.observations and selected_tab != MISFITS,
show_observations=key_def.observations
and selected_tab not in {MISFITS, MISFIT_MAP, OBSERVATIONS_MAP},
show_color_palette=key_def.observations
and selected_tab not in {MISFIT_MAP, OBSERVATIONS_MAP},
log_scale_available=log_scale_valid_values
and selected_tab in {HISTOGRAM, DISTRIBUTION, GAUSSIAN_KDE},
)
Expand Down Expand Up @@ -759,13 +784,7 @@ def keySelected(self) -> None:
else:
self._ensemble_selection_widget.reset_maximum_and_minimum_ensemble_limits_to_default()

max_selected = self._ensemble_selection_widget.get_maximum_ensemble_limit()
str_num_of_ens = f" up to {max_selected}" if self.is_everest else ""
self._ensemble_group.set_title(
f"Select{str_num_of_ens} batches"
if self.is_everest
else f"Select up to {max_selected} ensembles"
)
self._update_ensemble_group_title()

is_observed_seismic = (
key_def.observations
Expand All @@ -778,7 +797,10 @@ def keySelected(self) -> None:
if widget._plotter.dimensionality == key_def.dimensionality
and (key_def.observations or not widget._plotter.requires_observations)
and not is_everest_specific_widget
and (not is_observed_seismic or widget.name == MISFITS)
and (
not is_observed_seismic
or widget.name in {MISFITS, MISFIT_MAP, OBSERVATIONS_MAP}
)
]

def everest_data_origin_check(origin: list[str]) -> bool:
Expand Down Expand Up @@ -834,6 +856,9 @@ def everest_available_widget_selection(
current_widget = available_widgets[0]

self._central_tab.setCurrentWidget(current_widget)
self._apply_ensemble_selection_policy_for_tab(
self._central_tab.tabText(self._central_tab.currentIndex())
)
self._central_tab.currentChanged.connect(self.current_tab_changed)
self._prev_key_dimensionality = key_def.dimensionality
self._prev_key = key_def.key
Expand Down
6 changes: 6 additions & 0 deletions src/ert/gui/plotting/utils/plot_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
CrossEnsembleStatisticsPlot,
DistributionPlot,
HistogramPlot,
MisfitMapPlot,
MisfitsPlot,
StatisticsPlot,
StdDevPlot,
)
from ert.gui.plotting.ert_plots.observations_map import ObservationsMapPlot
from ert.gui.plotting.everest_plots import (
EverestBatchObjectiveFunctionPlot,
EverestConstraintsPlot,
Expand All @@ -23,6 +25,8 @@
GAUSSIAN_KDE = "Gaussian KDE"
ENSEMBLE = "Ensemble"
HISTOGRAM = "Histogram"
MISFIT_MAP = "Misfit map"
OBSERVATIONS_MAP = "Observations map"
STATISTICS = "Statistics"
STD_DEV = "Std dev"
MISFITS = "Misfits"
Expand All @@ -35,6 +39,8 @@
ERT_PLOT_MAP: dict[str, Callable[[], Plotter]] = {
STATISTICS: StatisticsPlot,
MISFITS: MisfitsPlot,
MISFIT_MAP: MisfitMapPlot,
OBSERVATIONS_MAP: ObservationsMapPlot,
HISTOGRAM: HistogramPlot,
DISTRIBUTION: DistributionPlot,
CROSS_ENSEMBLE_STATISTICS: CrossEnsembleStatisticsPlot,
Expand Down
8 changes: 5 additions & 3 deletions src/ert/gui/plotting/widgets/plot_controls/general_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ def create_edit_button(
]
)

palette_container = QWidget()
palette_layout = QVBoxLayout(palette_container)
self._palette_container = QWidget()
palette_layout = QVBoxLayout(self._palette_container)
palette_layout.setContentsMargins(0, 0, 0, 0)
palette_layout.setSpacing(2)
palette_layout.addWidget(QLabel("Selected color palette:"))
Expand All @@ -109,7 +109,7 @@ def create_edit_button(
palette_layout.addWidget(self._color_cycle_selector)
palette_layout.addWidget(self._color_cycle_selector.get_custom_palette_button())

widgets.extend([palette_container, edit_buttons])
widgets.extend([self._palette_container, edit_buttons])

self._general_options = CollapsibleSection(
"General options",
Expand Down Expand Up @@ -167,6 +167,7 @@ def update_plot_context(
history_data_available: bool,
has_observations: bool,
show_observations: bool,
show_color_palette: bool = True,
log_scale_available: bool,
) -> None:
plot_config = plot_context.plotConfig()
Expand All @@ -185,6 +186,7 @@ def update_plot_context(
self._observations_color_edit.setVisible(
show_observations and self.observations_checkbox_state
)
self._palette_container.setVisible(show_color_palette)
plot_config.set_history_enabled(
self.history_checkbox_state and history_data_available
)
Expand Down
Loading
Loading