Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/ert/gui/plotting/ert_plots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .distribution import DistributionPlot
from .gaussian_kde import GaussianKDEPlot
from .histogram import HistogramPlot
from .misfit_map import MisfitMapPlot
from .misfits import MisfitsPlot
from .statistics import StatisticsPlot
from .std_dev import StdDevPlot
Expand All @@ -11,6 +12,7 @@
"DistributionPlot",
"GaussianKDEPlot",
"HistogramPlot",
"MisfitMapPlot",
"MisfitsPlot",
"StatisticsPlot",
"StdDevPlot",
Expand Down
102 changes: 102 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,102 @@
from __future__ import annotations

import math
from typing import TYPE_CHECKING

import matplotlib.colors as mcolors
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
self._misfit_range_cache: dict[str, float] = {}

@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

first_ensemble, first_data = next(iter(ensemble_to_data_map.items()))
misfits_by_realization = MisfitsPlot._wide_pandas_to_long_polars_with_misfits(
{(first_ensemble.name, first_ensemble.id): first_data},
observation_data,
"seismic",
)[first_ensemble.name, first_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)
key = key_def.key if key_def is not None else ""
current_vabs = (
float(np.max(np.abs(misfit_values))) if misfit_values.size else 0.0
)
vabs = max(self._misfit_range_cache.get(key, 0.0), current_vabs)
if math.isclose(vabs, 0.0):
vabs = 1.0
self._misfit_range_cache[key] = vabs

norm = mcolors.Normalize(vmin=-vabs, vmax=vabs)
misfit_tripcolor = axes_misfit.tripcolor(
east, north, misfit_values, shading="flat", cmap="viridis", norm=norm
)

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())
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() or "Misfit map")
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())
77 changes: 77 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,77 @@
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())
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() or "Observation map")
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 @@ -532,6 +532,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
40 changes: 30 additions & 10 deletions src/ert/gui/plotting/plot_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
EVEREST_PLOT_MAP,
GAUSSIAN_KDE,
HISTOGRAM,
MISFIT_MAP,
MISFITS,
SHARED_PLOT_MAP,
STATISTICS,
Expand Down Expand Up @@ -382,10 +383,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 == MISFIT_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 @@ -590,7 +611,9 @@ 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},
show_color_palette=key_def.observations and selected_tab != MISFIT_MAP,
log_scale_available=log_scale_valid_values
and selected_tab in {HISTOGRAM, DISTRIBUTION, GAUSSIAN_KDE},
)
Expand Down Expand Up @@ -756,13 +779,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 @@ -775,7 +792,7 @@ 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})
]

def everest_data_origin_check(origin: list[str]) -> bool:
Expand Down Expand Up @@ -831,6 +848,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
3 changes: 3 additions & 0 deletions src/ert/gui/plotting/utils/plot_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DistributionPlot,
GaussianKDEPlot,
HistogramPlot,
MisfitMapPlot,
MisfitsPlot,
StatisticsPlot,
StdDevPlot,
Expand All @@ -24,6 +25,7 @@
GAUSSIAN_KDE = "Gaussian KDE"
ENSEMBLE = "Ensemble"
HISTOGRAM = "Histogram"
MISFIT_MAP = "Misfit map"
STATISTICS = "Statistics"
STD_DEV = "Std dev"
MISFITS = "Misfits"
Expand All @@ -36,6 +38,7 @@
ERT_PLOT_MAP: dict[str, Callable[[], Plotter]] = {
STATISTICS: StatisticsPlot,
MISFITS: MisfitsPlot,
MISFIT_MAP: MisfitMapPlot,
HISTOGRAM: HistogramPlot,
GAUSSIAN_KDE: GaussianKDEPlot,
DISTRIBUTION: DistributionPlot,
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