Skip to content

Commit acbd2fb

Browse files
committed
fixup! Add RFT QC tool
1 parent a7a5d0d commit acbd2fb

2 files changed

Lines changed: 246 additions & 68 deletions

File tree

src/ert/gui/tools/manage_experiments/rft_qc_widget.py

Lines changed: 125 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import html
4+
import math
45
from collections.abc import Callable, Mapping, Sequence
56
from dataclasses import dataclass, field
67
from enum import StrEnum
@@ -708,8 +709,66 @@ def _facet_counts(self, col: str, dataframes: list[pl.DataFrame]) -> dict[str, i
708709
return counts
709710

710711

712+
class DisplayPoints:
713+
def __init__(self) -> None:
714+
self._use_utm: bool = False
715+
self._df: pl.DataFrame = pl.DataFrame(schema=self.schema())
716+
717+
@staticmethod
718+
def schema() -> dict[str, Any]:
719+
return {
720+
"well_connection_cell": pl.Array(pl.Int64, 3),
721+
"east": pl.Float32,
722+
"north": pl.Float32,
723+
"tvd": pl.Float32,
724+
"status": pl.String,
725+
}
726+
727+
def update_display_points(
728+
self, display_points: pl.DataFrame, use_utm: bool
729+
) -> None:
730+
self._use_utm = use_utm
731+
self._df = display_points.select(self.schema().keys())
732+
733+
def get_point(self, index: int | None) -> dict[str, Any] | None:
734+
if index is not None and 0 <= index < len(self._df):
735+
return self._df.row(index, named=True)
736+
return None
737+
738+
def get_point_coordinates(
739+
self, index: int | None
740+
) -> tuple[float, float, float] | None:
741+
point = self.get_point(index)
742+
if point is None:
743+
return None
744+
if self._use_utm:
745+
return (
746+
point["east"],
747+
point["north"],
748+
point["tvd"],
749+
)
750+
return tuple(point["well_connection_cell"])
751+
752+
def get_points_to_plot(self) -> tuple[list[float], list[float], list[float]]:
753+
if self._use_utm:
754+
return (
755+
self._df["east"].to_list(),
756+
self._df["north"].to_list(),
757+
self._df["tvd"].to_list(),
758+
)
759+
return (
760+
self._df["well_connection_cell"].arr.get(0).to_list(),
761+
self._df["well_connection_cell"].arr.get(1).to_list(),
762+
self._df["well_connection_cell"].arr.get(2).to_list(),
763+
)
764+
765+
711766
class RftPlot:
712-
def __init__(self, show_details: Callable[[dict[str, Any]], None]) -> None:
767+
def __init__(
768+
self,
769+
show_details: Callable[[dict[str, Any]], None],
770+
clear_details: Callable[[], None],
771+
) -> None:
713772
figure = Figure()
714773
self._canvas: FigureCanvas = FigureCanvas(figure)
715774
self._canvas.setSizePolicy(
@@ -723,16 +782,14 @@ def __init__(self, show_details: Callable[[dict[str, Any]], None]) -> None:
723782
self._autoscaled_limits: tuple[Any, Any, Any] | None = None
724783

725784
self._point_artist: PathCollection | None = None
726-
self._point_coords: list[tuple[float, float, float]] = []
727-
self._display_points: pl.DataFrame = pl.DataFrame(
728-
schema=self._display_point_schema()
729-
)
785+
self._display_points: DisplayPoints = DisplayPoints()
730786
self._selection_artist: Any = None
731787
self._hover_artist: Any = None
732788
self._selected_index: int | None = None
733789
self._hover_index: int | None = None
734790

735791
self._show_details = show_details
792+
self._clear_details = clear_details
736793

737794
@property
738795
def canvas(self) -> FigureCanvas:
@@ -759,20 +816,21 @@ def redraw(
759816
self._ax.get_ylim(),
760817
self._ax.get_zlim(),
761818
)
819+
previous_selected_point = self._display_points.get_point(self._selected_index)
762820
self._ax.cla()
763821
self._ax.invert_zaxis()
764822
self._point_artist = None
765823
self._selection_artist = None
766824
self._hover_artist = None
767-
self._point_coords = []
768825
self._selected_index = None
826+
self._clear_details()
769827
self._hover_index = None
770828

771829
if obs_df.is_empty() and response_df.is_empty() and file_rft_df.is_empty():
772830
self._canvas.draw()
773831
return
774832

775-
point_columns = self._display_point_schema().keys()
833+
point_columns = self._display_points.schema().keys()
776834

777835
points = pl.concat(
778836
[
@@ -876,11 +934,6 @@ def _get_observation_cell_center_overlay(
876934
)
877935
self._ax.add_collection3d(lc)
878936

879-
xs, ys, zs = (
880-
points["east"].to_list(),
881-
points["north"].to_list(),
882-
points["tvd"].to_list(),
883-
)
884937
else:
885938
self._ax.set_xlabel("i", labelpad=6)
886939
self._ax.set_ylabel("j", labelpad=6)
@@ -897,11 +950,8 @@ def _get_observation_cell_center_overlay(
897950
_point_style(statuses), statuses, coords
898951
)
899952

900-
xs, ys, zs = (
901-
points["well_connection_cell"].arr.get(0).to_list(),
902-
points["well_connection_cell"].arr.get(1).to_list(),
903-
points["well_connection_cell"].arr.get(2).to_list(),
904-
)
953+
self._display_points.update_display_points(points, use_utm)
954+
xs, ys, zs = self._display_points.get_points_to_plot()
905955
self._point_artist = self._ax.scatter(
906956
xs,
907957
ys,
@@ -910,10 +960,8 @@ def _get_observation_cell_center_overlay(
910960
picker=5,
911961
depthshade=False,
912962
)
913-
self._point_coords = list(zip(xs, ys, zs, strict=True))
914-
self._display_points = points
915963

916-
displayed_statuses = self._display_points["status"].unique().to_list()
964+
displayed_statuses = points["status"].unique().to_list()
917965
for status, style in _POINT_STYLE.items():
918966
if status in displayed_statuses:
919967
self._ax.scatter(
@@ -926,6 +974,39 @@ def _get_observation_cell_center_overlay(
926974

927975
self._ax.legend(loc="upper left", fontsize="x-small")
928976
self._create_overlay_artists()
977+
978+
if previous_selected_point is not None:
979+
# Try to restore the previously selected point
980+
981+
fallback_index = None
982+
fallback_point = None
983+
for i, point in enumerate(points.rows(named=True)):
984+
if (
985+
point["well_connection_cell"]
986+
== previous_selected_point["well_connection_cell"]
987+
):
988+
fallback_index = i
989+
fallback_point = point
990+
if (
991+
math.isclose(
992+
point["east"], previous_selected_point["east"], rel_tol=1e-5
993+
)
994+
and math.isclose(
995+
point["north"],
996+
previous_selected_point["north"],
997+
rel_tol=1e-5,
998+
)
999+
and math.isclose(
1000+
point["tvd"], previous_selected_point["tvd"], rel_tol=1e-5
1001+
)
1002+
):
1003+
self._update_selected_point(i, point)
1004+
break
1005+
# If we didn't find an exact match, but did find a point with the same cell
1006+
# center, use that as a fallback.
1007+
if fallback_index is not None and fallback_point is not None:
1008+
self._update_selected_point(fallback_index, fallback_point)
1009+
9291010
self._canvas.draw()
9301011

9311012
# Store the autoscale limits after the redraw, so we can restore them later if
@@ -943,16 +1024,6 @@ def _get_observation_cell_center_overlay(
9431024
self._ax.set_zlim(*zlim)
9441025
self._canvas.draw_idle()
9451026

946-
@staticmethod
947-
def _display_point_schema() -> dict[str, Any]:
948-
return {
949-
"well_connection_cell": pl.Array(pl.Int64, 3),
950-
"east": pl.Float32,
951-
"north": pl.Float32,
952-
"tvd": pl.Float32,
953-
"status": pl.String,
954-
}
955-
9561027
def _fit_view_to_displayed_points(self) -> None:
9571028
if self._autoscaled_limits is None:
9581029
return
@@ -963,11 +1034,12 @@ def _fit_view_to_displayed_points(self) -> None:
9631034
self._canvas.draw_idle()
9641035

9651036
def _center_on_selected(self) -> None:
966-
if self._selected_index is None or not (
967-
0 <= self._selected_index < len(self._point_coords)
968-
):
1037+
if self._selected_index is None:
1038+
return
1039+
coordinates = self._display_points.get_point_coordinates(self._selected_index)
1040+
if coordinates is None:
9691041
return
970-
cx, cy, cz = self._point_coords[self._selected_index]
1042+
cx, cy, cz = coordinates
9711043
for getter, setter, center in (
9721044
(self._ax.get_xlim, self._ax.set_xlim, cx),
9731045
(self._ax.get_ylim, self._ax.set_ylim, cy),
@@ -1011,9 +1083,10 @@ def _refresh_overlays(self) -> None:
10111083
def _coords_for(
10121084
idx: int | None,
10131085
) -> tuple[list[float], list[float], list[float]]:
1014-
if idx is None or not (0 <= idx < len(self._point_coords)):
1086+
coordinates = self._display_points.get_point_coordinates(idx)
1087+
if coordinates is None:
10151088
return ([], [], [])
1016-
x, y, z = self._point_coords[idx]
1089+
x, y, z = coordinates
10171090
return ([x], [y], [z])
10181091

10191092
hover_idx = (
@@ -1033,11 +1106,15 @@ def _on_pick(self, event: PickEvent) -> None:
10331106
if not hasattr(event, "ind") or len(event.ind) == 0:
10341107
return
10351108
idx = int(event.ind[0])
1036-
if not (0 <= idx < len(self._point_coords)):
1037-
return
1038-
self._selected_index = idx
1039-
if 0 <= idx < len(self._display_points):
1040-
self._show_details(self._display_points.row(idx, named=True))
1109+
selected_point = self._display_points.get_point(idx)
1110+
if selected_point is not None:
1111+
self._update_selected_point(idx, selected_point)
1112+
1113+
def _update_selected_point(
1114+
self, index: int, selected_point: dict[str, Any]
1115+
) -> None:
1116+
self._selected_index = index
1117+
self._show_details(selected_point)
10411118
self._refresh_overlays()
10421119

10431120
def _on_hover(self, event: MouseEvent) -> None:
@@ -1077,12 +1154,9 @@ def _on_scroll(self, event: MouseEvent) -> None:
10771154
if event.inaxes is not self._ax:
10781155
return
10791156
scale = 0.8 if event.button == "up" else 1.25
1080-
if self._selected_index is not None and 0 <= self._selected_index < len(
1081-
self._point_coords
1082-
):
1083-
cx, cy, cz = self._point_coords[self._selected_index]
1084-
else:
1085-
cx, cy, cz = None, None, None
1157+
coordinates = self._display_points.get_point_coordinates(self._selected_index)
1158+
(cx, cy, cz) = (None, None, None) if coordinates is None else coordinates
1159+
10861160
# Assert to reassure mypy that self._ax is indeed an Axes3D instance:
10871161
assert isinstance(self._ax, Axes3D)
10881162
for getter, setter, center in (
@@ -1141,7 +1215,7 @@ def __init__(self, ert_config: ErtConfig | None = None) -> None:
11411215
self._load_rft_file: bool = False
11421216

11431217
self._use_utm = False
1144-
self._plot = RftPlot(self._show_details)
1218+
self._plot = RftPlot(self._show_details, self._clear_details)
11451219
self._filter_panel = FilterPanel(
11461220
self._apply_filter_and_redraw,
11471221
self._plot._fit_view_to_displayed_points,
@@ -1697,3 +1771,6 @@ def _obs_coords(row: dict[str, Any]) -> str | None:
16971771
{file_point_details_html}
16981772
</table>
16991773
""")
1774+
1775+
def _clear_details(self) -> None:
1776+
self._details.clear()

0 commit comments

Comments
 (0)