diff --git a/webviz_subsurface/plugins/_swatinit_qc/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/__init__.py
index 262986762..3d966a009 100644
--- a/webviz_subsurface/plugins/_swatinit_qc/__init__.py
+++ b/webviz_subsurface/plugins/_swatinit_qc/__init__.py
@@ -1 +1,2 @@
from ._plugin import SwatinitQC
+from ._swatint import SwatinitQcDataModel
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_error.py b/webviz_subsurface/plugins/_swatinit_qc/_error.py
new file mode 100644
index 000000000..bc998f235
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/_error.py
@@ -0,0 +1,5 @@
+from dash import html
+
+
+def error(error_message: str) -> html.Div:
+ return html.Div(children=error_message, style={"color": "red"})
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_plugin.py b/webviz_subsurface/plugins/_swatinit_qc/_plugin.py
index 8cdc38e0d..9a5b45c2f 100644
--- a/webviz_subsurface/plugins/_swatinit_qc/_plugin.py
+++ b/webviz_subsurface/plugins/_swatinit_qc/_plugin.py
@@ -4,38 +4,13 @@
import webviz_core_components as wcc
from webviz_config import WebvizPluginABC, WebvizSettings
-from ._business_logic import SwatinitQcDataModel
-from ._callbacks import plugin_callbacks
-from ._layout import plugin_main_layout
+from ._error import error
+from ._plugin_ids import PlugInIDs
+from ._swatint import SwatinitQcDataModel
+from .views import OverviewTabLayout, TabMaxPcInfoLayout, TabQqPlotLayout
class SwatinitQC(WebvizPluginABC):
- """This plugin is used to visualize the output from [check_swatinit]\
-(https://fmu-docs.equinor.com/docs/subscript/scripts/check_swatinit.html) which is a QC tool
-for Water Initialization in Eclipse runs when the `SWATINIT` keyword has been used. It is used to
-quantify how much the volume changes from `SWATINIT` to `SWAT` at time zero in the dynamical model,
-and help understand why it changes.
-
----
-* **`csvfile`:** Path to an csvfile from check_swatinit. The path should be relative to the runpath
-if ensemble and realization is given as input, if not the path needs to be absolute.
-* **`ensemble`:** Which ensemble in `shared_settings` to visualize.
-* **`realization`:** Which realization to pick from the ensemble
-* **`faultlines`**: A csv file containing faultpolygons to be visualized together with the map view.
-Export format from [xtgeo.xyz.polygons.dataframe](
-https://xtgeo.readthedocs.io/en/latest/apiref/xtgeo.xyz.polygons.html#xtgeo.xyz.polygons.Polygons.dataframe
-) \
-[(example file)](\
-https://github.com/equinor/webviz-subsurface-testdata/blob/master/01_drogon_ahm/\
-realization-0/iter-0/share/results/polygons/toptherys--gl_faultlines_extract_postprocess.csv).
-
----
-The `csvfile` can be generated by running the [CHECK_SWATINIT](https://fmu-docs.equinor.com/\
-docs/ert/reference/forward_models.html?highlight=swatinit#CHECK_SWATINIT) forward model in ERT,
-or with the "check_swatinit" command line tool.
-
-"""
-
def __init__(
self,
webviz_settings: WebvizSettings,
@@ -44,7 +19,7 @@ def __init__(
realization: Optional[int] = None,
faultlines: Path = None,
) -> None:
- super().__init__()
+ super().__init__(stretch=True)
self._datamodel = SwatinitQcDataModel(
webviz_settings=webviz_settings,
@@ -53,15 +28,65 @@ def __init__(
realization=realization,
faultlines=faultlines,
)
- self.add_webvizstore()
- self.set_callbacks()
+ self.error_message = ""
+
+ # Stores used in Overview tab
+ self.add_store(
+ PlugInIDs.Stores.Overview.BUTTON, WebvizPluginABC.StorageType.SESSION
+ )
+
+ # Stores used in Water tab
+ self.add_store(
+ PlugInIDs.Stores.Water.QC_VIZ, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Water.EQLNUM, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Water.COLOR_BY, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Water.MAX_POINTS, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Water.QC_FLAG, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Water.SATNUM, WebvizPluginABC.StorageType.SESSION
+ )
+
+ # Stores used in Capilaty tab
+ self.add_store(
+ PlugInIDs.Stores.Capilary.SPLIT_TABLE_BY,
+ WebvizPluginABC.StorageType.SESSION,
+ )
+ self.add_store(
+ PlugInIDs.Stores.Capilary.MAX_PC_SCALE, WebvizPluginABC.StorageType.SESSION
+ )
+ self.add_store(
+ PlugInIDs.Stores.Capilary.EQLNUM, WebvizPluginABC.StorageType.SESSION
+ )
+
+ # Adding each tab as a view element
+ self.add_view(
+ OverviewTabLayout(self._datamodel),
+ PlugInIDs.SwatinitViews.OVERVIEW,
+ PlugInIDs.SwatinitViews.GROUP_NAME,
+ )
+ self.add_view(
+ TabQqPlotLayout(self._datamodel),
+ PlugInIDs.SwatinitViews.WATER,
+ PlugInIDs.SwatinitViews.GROUP_NAME,
+ )
+ self.add_view(
+ TabMaxPcInfoLayout(self._datamodel),
+ PlugInIDs.SwatinitViews.WATER,
+ PlugInIDs.SwatinitViews.GROUP_NAME,
+ )
@property
def layout(self) -> wcc.Tabs:
- return plugin_main_layout(self.uuid, self._datamodel)
-
- def set_callbacks(self) -> None:
- plugin_callbacks(self.uuid, self._datamodel)
+ return error(self.error_message)
def add_webvizstore(self) -> List[Tuple[Callable, List[dict]]]:
return self._datamodel.webviz_store
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_plugin_ids.py b/webviz_subsurface/plugins/_swatinit_qc/_plugin_ids.py
new file mode 100644
index 000000000..609773d89
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/_plugin_ids.py
@@ -0,0 +1,53 @@
+# pylint: disable=too-few-public-methods
+class PlugInIDs:
+ class Tabs:
+ QC_PLOTS = "Water Initializaion QC plots"
+ MAX_PC_SCALING = "Capillary pressure scaling"
+ OVERVIEW = "Overview and Information"
+
+ class Stores:
+ class Shared:
+ PICK_VIEW = "pick-view"
+
+ class Overview:
+ BUTTON = "button"
+
+ class Water:
+ QC_VIZ = "qc-viz"
+ EQLNUM = "eqlnum"
+ COLOR_BY = "color_by"
+ MAX_POINTS = "max-points"
+ QC_FLAG = "qc-flag"
+ SATNUM = "satnum"
+
+ class Capilary:
+ SPLIT_TABLE_BY = "split-table-by"
+ MAX_PC_SCALE = "max-pc-scale"
+ EQLNUM = "eqlnum"
+
+ class SharedSettings:
+ PICK_VIEW = "pick-view"
+ FILTERS = "filters"
+
+ class SettingsGroups:
+ WATER_SEELECTORS = "water-selectors"
+ WATER_FILTERS = "water-filters"
+ CAPILAR_SELECTORS = "capilar-selectors"
+ CAPILAR_FILTERS = "capilar-filters"
+
+ class QcFlags:
+ FINE_EQUIL = "FINE_EQUIL"
+ HC_BELOW_FWL = "HC_BELOW_FWL"
+ PC_SCALED = "PC_SCALED"
+ PPCWMAX = "PPCWMAX"
+ SWATINIT_1 = "SWATINIT_1"
+ SWL_TRUNC = "SWL_TRUNC"
+ UNKNOWN = "UNKNOWN"
+ WATER = "WATER"
+
+ class SwatinitViews:
+ GROUP_NAME = "swatinit-group"
+
+ OVERVIEW = "overview"
+ WATER = "water"
+ CAPILAR = "capilar"
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_business_logic.py b/webviz_subsurface/plugins/_swatinit_qc/_swatint.py
similarity index 100%
rename from webviz_subsurface/plugins/_swatinit_qc/_business_logic.py
rename to webviz_subsurface/plugins/_swatinit_qc/_swatint.py
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/__init__.py
new file mode 100644
index 000000000..435f39c52
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/__init__.py
@@ -0,0 +1,9 @@
+from ._capilar_layout import CapilarViewelement
+from ._dash_table import DashTable
+from ._fullscreen import FullScreen
+from ._layout_style import LayoutStyle
+from ._map_figure import MapFigure
+from ._overview_layout import OverviewViewelement
+from ._properties_vs_depth import PropertiesVsDepthSubplots
+from ._water_layout import WaterViewelement
+from ._waterfall_plot import WaterfallPlot
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_capilar_layout.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_capilar_layout.py
new file mode 100644
index 000000000..de3e313df
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_capilar_layout.py
@@ -0,0 +1,152 @@
+from cgi import print_arguments
+from typing import List
+
+import pandas as pd
+import plotly.graph_objects as go
+import webviz_core_components as wcc
+from dash import dash_table, dcc
+from dash.development.base_component import Component
+from webviz_config.webviz_plugin_subclasses import ViewElementABC
+
+from .._swatint import SwatinitQcDataModel
+from ..views.capilar_tab.settings import CapilarFilters
+from ._dash_table import DashTable
+from ._fullscreen import FullScreen
+from ._layout_style import LayoutStyle
+from ._map_figure import MapFigure
+
+# This can be moved into a view_elements folder in views > capilar_tab
+
+
+class CapilarViewelement(ViewElementABC):
+ """All elements visible in the 'Caplillary pressure scaling'-tab
+ gathered in one viewelement"""
+
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ INFO_TEXT = "info-text"
+ MAP = "map"
+ TABLE = "table"
+
+ def __init__(
+ self,
+ datamodel: SwatinitQcDataModel,
+ ) -> None:
+ super().__init__()
+ self.datamodel = datamodel
+
+ self.init_eqlnums = self.datamodel.eqlnums[:1]
+ continous_filters = (
+ [self.datamodel.dframe[col].min(), self.datamodel.dframe[col].max()]
+ for col in self.datamodel.filters_continuous
+ )
+
+ continous_filters_ids = (
+ [
+ {
+ "id": CapilarFilters.IDs.RANGE_FILTERS,
+ "col": col,
+ }
+ ]
+ for col in self.datamodel.filters_continuous
+ )
+
+ print({"EQLNUM": self.init_eqlnums})
+ self.dframe = self.datamodel.get_dataframe(
+ filters={"EQLNUM": self.init_eqlnums},
+ range_filters=zip_filters(continous_filters, continous_filters_ids),
+ )
+
+ df_for_map = datamodel.resample_dataframe(self.dframe, max_points=10000)
+ self.selectors = self.datamodel.SELECTORS
+
+ self.map_figure = MapFigure(
+ dframe=df_for_map,
+ color_by="EQLNUM",
+ faultlinedf=datamodel.faultlines_df,
+ colormap=datamodel.create_colormap("EQLNUM"),
+ ).figure
+
+ def inner_layout(self) -> List[Component]:
+ return [
+ wcc.Header("Maximum capillary pressure scaling", style=LayoutStyle.HEADER),
+ wcc.FlexBox(
+ style={"margin-top": "10px", "height": "40vh"},
+ children=[
+ wcc.FlexColumn(
+ dcc.Markdown(pc_columns_description()),
+ id=self.register_component_unique_id(
+ CapilarViewelement.IDs.INFO_TEXT
+ ),
+ flex=7,
+ style={"margin-right": "40px"},
+ ),
+ wcc.FlexColumn(
+ FullScreen(
+ wcc.Graph(
+ style={"height": "100%", "min-height": "35vh"},
+ figure=self.map_figure,
+ id=self.register_component_unique_id(
+ CapilarViewelement.IDs.MAP
+ ),
+ )
+ ),
+ flex=3,
+ ),
+ ],
+ ),
+ self.max_pc_table,
+ ]
+
+ @property
+ def max_pc_table(self) -> dash_table:
+ return DashTable(
+ id=self.register_component_unique_id(CapilarViewelement.IDs.TABLE),
+ data=self.dframe.to_dict("records"),
+ columns=[
+ {
+ "name": i,
+ "id": i,
+ "type": "numeric" if i not in self.selectors else "text",
+ "format": {"specifier": ".4~r"} if i not in self.selectors else {},
+ }
+ for i in self.dframe.columns
+ ],
+ height="48vh",
+ sort_action="native",
+ fixed_rows={"headers": True},
+ style_cell={
+ "minWidth": LayoutStyle.TABLE_CELL_WIDTH,
+ "maxWidth": LayoutStyle.TABLE_CELL_WIDTH,
+ "width": LayoutStyle.TABLE_CELL_WIDTH,
+ },
+ style_data_conditional=[
+ {
+ "if": {
+ "filter_query": f"{{{self.datamodel.COLNAME_THRESHOLD}}} > 0",
+ },
+ **LayoutStyle.TABLE_HIGHLIGHT,
+ },
+ ],
+ )
+
+
+# pylint: disable=anomalous-backslash-in-string
+def pc_columns_description() -> str:
+ return f"""
+> **Column descriptions**
+> - **PCOW_MAX** - Maximum capillary pressure from the input SWOF/SWFN tables
+> - **PC_SCALING** - Maximum capillary pressure scaling applied
+> - **PPCW** - Maximum capillary pressure after scaling
+> - **{SwatinitQcDataModel.COLNAME_THRESHOLD}** - Column showing how many percent of the pc-scaled dataset that match the user-selected threshold
+*PPCW = PCOW_MAX \* PC_SCALING*
+A threshold for the maximum capillary scaling can be set in the menu.
+The table will show how many percent of the dataset that exceeds this value, and cells above the threshold will be shown in the map ➡️
+"""
+
+
+def zip_filters(filter_values: list, filter_ids: list) -> dict:
+ for values, id_val in zip(filter_values, filter_ids):
+ print("val: ", values)
+ print("id: ", id_val)
+ return {id_val["col"]: values for values, id_val in zip(filter_values, filter_ids)}
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_dash_table.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_dash_table.py
new file mode 100644
index 000000000..ec6e8ba2d
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_dash_table.py
@@ -0,0 +1,20 @@
+from typing import Any, List
+
+from dash import dash_table
+
+from ._layout_style import LayoutStyle
+
+
+class DashTable(dash_table.DataTable):
+ def __init__(
+ self, data: List[dict], columns: List[dict], height: str = "none", **kwargs: Any
+ ) -> None:
+ super().__init__(
+ data=data,
+ columns=columns,
+ style_table={"height": height, **LayoutStyle.TABLE_STYLE},
+ style_as_list_view=True,
+ css=LayoutStyle.TABLE_CSS,
+ style_header=LayoutStyle.TABLE_HEADER,
+ **kwargs,
+ )
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_fullscreen.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_fullscreen.py
new file mode 100644
index 000000000..8ea588122
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_fullscreen.py
@@ -0,0 +1,8 @@
+from typing import Any, List
+
+import webviz_core_components as wcc
+
+
+class FullScreen(wcc.WebvizPluginPlaceholder):
+ def __init__(self, children: List[Any]) -> None:
+ super().__init__(buttons=["expand"], children=children)
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_layout_style.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_layout_style.py
new file mode 100644
index 000000000..9a284c79c
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_layout_style.py
@@ -0,0 +1,19 @@
+class LayoutStyle:
+ MAIN_HEIGHT = "87vh"
+ HEADER = {
+ "font-size": "15px",
+ "color": "black",
+ "text-transform": "uppercase",
+ "border-color": "black",
+ }
+ TABLE_HEADER = {"fontWeight": "bold"}
+ TABLE_STYLE = {"max-height": MAIN_HEIGHT, "overflowY": "auto"}
+ TABLE_CELL_WIDTH = 95
+ TABLE_CELL_HEIGHT = "10px"
+ TABLE_HIGHLIGHT = {"backgroundColor": "rgb(230, 230, 230)", "fontWeight": "bold"}
+ TABLE_CSS = [
+ {
+ "selector": ".dash-spreadsheet tr",
+ "rule": f"height: {TABLE_CELL_HEIGHT};",
+ },
+ ]
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_map_figure.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_map_figure.py
new file mode 100644
index 000000000..7689d5c08
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_map_figure.py
@@ -0,0 +1,78 @@
+from typing import Optional
+
+import numpy as np
+import pandas as pd
+import plotly.graph_objects as go
+
+from webviz_subsurface._figures import create_figure
+
+
+class MapFigure:
+ def __init__(
+ self,
+ dframe: pd.DataFrame,
+ color_by: str,
+ colormap: dict,
+ faultlinedf: Optional[pd.DataFrame] = None,
+ ):
+ self.dframe = dframe
+ self.color_by = color_by
+ self.colormap = colormap
+ self.hover_data = ["I", "J", "K"]
+
+ self._figure = self.create_figure()
+
+ if faultlinedf is not None:
+ self.add_fault_lines(faultlinedf)
+
+ @property
+ def figure(self) -> go.Figure:
+ return self._figure
+
+ @property
+ def axis_layout(self) -> dict:
+ return {
+ "title": None,
+ "showticklabels": False,
+ "showgrid": False,
+ "showline": False,
+ }
+
+ def create_figure(self) -> go.Figure:
+ return (
+ create_figure(
+ plot_type="scatter",
+ data_frame=self.dframe,
+ x="X",
+ y="Y",
+ color=self.color_by
+ if self.color_by != "PERMX"
+ else np.log10(self.dframe[self.color_by]),
+ color_discrete_map=self.colormap,
+ xaxis={"constrain": "domain", **self.axis_layout},
+ yaxis={"scaleanchor": "x", **self.axis_layout},
+ hover_data=[self.color_by] + self.hover_data,
+ color_continuous_scale="Viridis",
+ )
+ .update_traces(marker_size=10, unselected={"marker": {"opacity": 0}})
+ .update_coloraxes(showscale=False)
+ .update_layout(
+ plot_bgcolor="white",
+ margin={"t": 10, "b": 10, "l": 0, "r": 0},
+ showlegend=False,
+ )
+ )
+
+ def add_fault_lines(self, faultlinedf: pd.DataFrame) -> None:
+ for _fault, faultdf in faultlinedf.groupby("POLY_ID"):
+ self._figure.add_trace(
+ {
+ "x": faultdf["X_UTME"],
+ "y": faultdf["Y_UTMN"],
+ "mode": "lines",
+ "type": "scatter",
+ "hoverinfo": "none",
+ "showlegend": False,
+ "line": {"color": "grey", "width": 1},
+ }
+ )
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_overview_layout.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_overview_layout.py
new file mode 100644
index 000000000..fa907e1fb
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_overview_layout.py
@@ -0,0 +1,193 @@
+import webviz_core_components as wcc
+from dash import dcc, html
+from dash.development.base_component import Component
+from webviz_config.webviz_plugin_subclasses import ViewElementABC
+
+from .._swatint import SwatinitQcDataModel
+from ._dash_table import DashTable
+from ._layout_style import LayoutStyle
+
+# This can be moved into a view_elements folder in views > overview_tab
+
+
+class OverviewViewelement(ViewElementABC):
+ """All elements visible in the 'Overview and Information'-tab
+ gathered in one viewelement"""
+
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ LAYOUT = "layout"
+ DESCRIBTION = "describtion"
+ FRAME = "frame"
+ KEY_NUMBERS = "key-numbers"
+ BUTTON = "button"
+ INFO_DIALOG = "info-dialog"
+ TABLE_LABEL = "tabel-label"
+ TABLE = "table"
+
+ def __init__(self, datamodel: SwatinitQcDataModel) -> None:
+ super().__init__()
+ self.datamodel = datamodel
+ max_pc, min_pc = datamodel.pc_scaling_min_max
+ wvol, hcvol = datamodel.vol_diff_total
+ self.number_style = {
+ "font-weight": "bold",
+ "font-size": "17px",
+ "margin-left": "20px",
+ }
+ self.infodata = [
+ ("HC Volume difference:", f"{hcvol:.2f} %"),
+ ("Water Volume difference:", f"{wvol:.2f} %"),
+ ("Maximum Capillary Pressure scaling:", f"{max_pc:.1f}"),
+ ("Minimum Capillary Pressure scaling:", f"{min_pc:.3g}"),
+ ]
+ self.title = "Plugin and 'check_swatinit' information"
+
+ self.tabledata, self.columns = datamodel.table_data_qc_vol_overview()
+ self.label = (
+ "Table showing volume changes from SWATINIT to SWAT at Reservoir conditions"
+ )
+
+ def inner_layout(self) -> html.Div:
+ return html.Div(
+ id=self.register_component_unique_id(OverviewViewelement.IDs.LAYOUT),
+ children=[
+ html.Div(
+ style={"height": "40vh", "overflow-y": "auto"},
+ children=[
+ wcc.FlexBox(
+ children=[
+ wcc.FlexColumn(
+ [
+ self.table,
+ ],
+ flex=7,
+ style={"margin-right": "20px"},
+ ),
+ wcc.FlexColumn(self.infobox, flex=3),
+ ],
+ ),
+ ],
+ ),
+ self.describtion,
+ ],
+ )
+
+ @property
+ def describtion(self) -> html.Div:
+ return html.Div(
+ style={"margin-top": "20px"},
+ id=OverviewViewelement.IDs.DESCRIBTION,
+ children=[
+ wcc.Header("QC_FLAG descriptions", style=LayoutStyle.HEADER),
+ dcc.Markdown(qc_flag_description()),
+ ],
+ )
+
+ @property
+ def infobox(self) -> Component:
+ return wcc.Frame(
+ style={"height": "90%"},
+ id=self.register_component_unique_id(OverviewViewelement.IDs.FRAME),
+ children=[
+ wcc.Header("Information", style=LayoutStyle.HEADER),
+ self.info_dialog,
+ wcc.Header("Key numbers", style=LayoutStyle.HEADER),
+ html.Div(
+ [
+ html.Div([text, html.Span(num, style=self.number_style)])
+ for text, num in self.infodata
+ ],
+ id=self.register_component_unique_id(
+ OverviewViewelement.IDs.KEY_NUMBERS
+ ),
+ ),
+ ],
+ )
+
+ @property
+ def info_dialog(self) -> html.Div:
+ return html.Div(
+ style={"margin-bottom": "30px"},
+ children=[
+ html.Button(
+ "CLICK HERE FOR INFORMATION",
+ style={"width": "100%", "background-color": "white"},
+ id=self.register_component_unique_id(
+ OverviewViewelement.IDs.BUTTON
+ ),
+ ),
+ wcc.Dialog(
+ title=self.title,
+ id=self.register_component_unique_id(
+ OverviewViewelement.IDs.INFO_DIALOG
+ ),
+ max_width="md",
+ open=False,
+ children=dcc.Markdown(check_swatinit_description()),
+ ),
+ ],
+ )
+
+ @property
+ def table(self) -> html.Div:
+ return html.Div(
+ children=[
+ html.Div(
+ html.Label(self.label, className="webviz-underlined-label"),
+ style={"margin-bottom": "20px"},
+ id=self.register_component_unique_id(
+ OverviewViewelement.IDs.TABLE_LABEL
+ ),
+ ),
+ DashTable(
+ id=self.register_component_unique_id(OverviewViewelement.IDs.TABLE),
+ data=self.tabledata,
+ columns=self.columns,
+ style_data_conditional=[
+ {
+ "if": {"row_index": [0, len(self.tabledata) - 1]},
+ **LayoutStyle.TABLE_HIGHLIGHT,
+ },
+ ],
+ ),
+ ],
+ )
+
+
+def qc_flag_description() -> str:
+ return """
+- **PC_SCALED** - Capillary pressure have been scaled and SWATINIT was accepted.
+- **FINE_EQUIL** - If item 9 in EQUIL is nonzero then initialization happens in a vertically
+ refined model. Capillary pressure is still scaled, but water might be added or lost.
+- **SWL_TRUNC** - If SWL is larger than SWATINIT, SWAT will be reset to SWL. Extra water is
+ added and hydrocarbons are lost.
+- **SWATINIT_1** - When SWATINIT is 1 above the contact, Eclipse will ignore SWATINIT and not
+ touch the capillary pressure function which typically results in extra hydrocarbons.
+ This could be ok as long as the porosities and/or permeabilities of these cells are small.
+ If SWU is included, cells where SWATINIT is equal or larger than SWU will
+ also be flagged as SWATINIT_1
+- **HC_BELOW_FWL** - If SWATINIT is less than 1 below the contact provided in EQUIL, Eclipse will
+ ignore it and not scale the capillary pressure function. SWAT will be 1, unless a capillary
+ pressure function with negative values is in SWOF/SWFN. This results in the loss of HC volumes.
+- **PPCWMAX** - If an upper limit of how much capillary pressure scaling is allowed is set, water will be
+ lost if this limit is hit.
+- **WATER** - SWATINIT was 1 in the water zone, and SWAT is set to 1.
+> **Consult the [check_swatinit](https://fmu-docs.equinor.com/docs/subscript/scripts/check_swatinit.html)
+ documentation for more detailed descriptions**
+"""
+
+
+def check_swatinit_description() -> str:
+ return """
+This plugin is used to visualize the output from **check_swatinit** which is a **QC tool for Water Initialization in Eclipse runs
+when the SWATINIT keyword has been used**. It is used to quantify how much the volume changes from **SWATINIT** to **SWAT** at time
+zero in the dynamical model, and help understand why it changes.
+When the keyword SWATINIT has been used as water initialization option in Eclipse, capillary pressure scaling on a cell-by-cell basis will
+occur in order to match SWATINIT from the geomodel.
+This process has some exceptions which can cause volume changes from SWATINIT to SWAT at time zero.
+Each cell in the dynamical model has been flagged according to what has happened during initialization, and information is stored
+in the **QC_FLAG** column.
+> Check the maximum capillary pressure pr SATNUM in each EQLNUM to ensure extreme values were not necessary
+[check_swatinit documentation](https://fmu-docs.equinor.com/docs/subscript/scripts/check_swatinit.html)
+"""
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_properties_vs_depth.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_properties_vs_depth.py
new file mode 100644
index 000000000..1b6053903
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_properties_vs_depth.py
@@ -0,0 +1,166 @@
+import numpy as np
+import pandas as pd
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+
+from webviz_subsurface._utils.colors import hex_to_rgb, rgb_to_str, scale_rgb_lightness
+
+
+def axis_defaults(showgrid: bool = True) -> dict:
+ return {
+ "showline": True,
+ "linewidth": 2,
+ "linecolor": "black",
+ "mirror": True,
+ "showgrid": showgrid,
+ "gridwidth": 1,
+ "gridcolor": "lightgrey",
+ }
+
+
+class PropertiesVsDepthSubplots:
+ def __init__(
+ self,
+ dframe: pd.DataFrame,
+ color_by: str,
+ colormap: dict,
+ discrete_color: bool = True,
+ ) -> None:
+ self.dframe = dframe
+ self.color_by = color_by
+ self.discrete_color = discrete_color
+ self.colormap = colormap
+ self.layout = [(1, 1), (1, 2), (2, 1), (2, 2)]
+ self.responses = ["SWATINIT", "SWAT", "PRESSURE", "PC"]
+ self.hover_data = self.create_hover_data(
+ include_columns=["QC_FLAG", "EQLNUM", "SATNUM", "I", "J", "K"]
+ )
+ self.uirevision = "".join([str(x) for x in self.dframe["EQLNUM"].unique()])
+
+ self._figure = self.create_empty_subplots_figure(rows=2, cols=2)
+ self.add_subplotfigures_to_main_figure()
+ self.add_contacts_to_plot()
+
+ @property
+ def figure(self) -> go.Figure:
+ return self._figure
+
+ @property
+ def hovertemplate(self) -> go.Figure:
+ return (
+ "X: %{x}
Y: %{y}
"
+ + "
".join(
+ [
+ f"{col}:%{{customdata[{idx}]}}"
+ for idx, col in enumerate(self.hover_data)
+ ]
+ )
+ + ""
+ )
+
+ def create_empty_subplots_figure(self, rows: int, cols: int) -> go.Figure:
+ return (
+ make_subplots(
+ rows=rows,
+ cols=cols,
+ subplot_titles=[f"Depth vs {resp}" for resp in self.responses],
+ shared_yaxes=True,
+ vertical_spacing=0.07,
+ horizontal_spacing=0.05,
+ )
+ .update_layout(
+ plot_bgcolor="white",
+ uirevision=self.uirevision,
+ margin={"t": 50, "b": 10, "l": 10, "r": 10},
+ legend={"orientation": "h"},
+ clickmode="event+select",
+ coloraxis={"colorscale": "Viridis", **self.colorbar},
+ )
+ .update_yaxes(autorange="reversed", **axis_defaults())
+ .update_xaxes(axis_defaults())
+ .update_xaxes(row=1, col=2, matches="x")
+ )
+
+ def add_subplotfigures_to_main_figure(self) -> None:
+ # for discrete colors there should be one trace per unique color
+ unique_traces = (
+ self.dframe[self.color_by].unique()
+ if self.discrete_color
+ else [self.color_by]
+ )
+
+ for color in unique_traces:
+ df = self.dframe
+ df = df[df[self.color_by] == color] if self.discrete_color else df
+ customdata = np.stack([df[col] for col in self.hover_data], axis=-1)
+
+ for idx, response in enumerate(self.responses):
+ trace = go.Scattergl(
+ x=df[response],
+ y=df["Z"],
+ mode="markers",
+ name=color,
+ showlegend=idx == 0,
+ marker=self.set_marker_style(color, df),
+ unselected={"marker": self.set_unselected_marker_style(color)},
+ customdata=customdata,
+ hovertemplate=self.hovertemplate,
+ legendgroup=color,
+ ).update(marker_size=10)
+
+ row, col = self.layout[idx]
+ self._figure.add_trace(trace, row=row, col=col)
+
+ @property
+ def colorbar(self) -> dict:
+ if self.color_by != "PERMX":
+ return {}
+ tickvals = list(range(-4, 5, 1))
+ return {
+ "colorbar": {
+ "tickvals": tickvals,
+ "ticktext": [10**val for val in tickvals],
+ }
+ }
+
+ def create_hover_data(self, include_columns: list) -> list:
+ # ensure the colorby is the first entry in the list -> used in customdata in callback
+ hover_data = [self.color_by]
+ for col in include_columns:
+ if col not in hover_data:
+ hover_data.append(col)
+ return hover_data
+
+ def set_marker_style(self, color: str, df: pd.DataFrame) -> dict:
+ if not self.discrete_color:
+ return {
+ "coloraxis": "coloraxis",
+ "color": df[self.color_by]
+ if self.color_by != "PERMX"
+ else np.log10(df[self.color_by]),
+ }
+ return {"color": self.colormap[color], "opacity": 0.5}
+
+ def set_unselected_marker_style(self, color: str) -> dict:
+ if not self.discrete_color:
+ return {"opacity": 0.1}
+ return {
+ "color": rgb_to_str(
+ scale_rgb_lightness(hex_to_rgb(self.colormap[color]), 250)
+ )
+ }
+
+ def add_contacts_to_plot(self) -> None:
+ """Annotate axes with named horizontal lines for contacts."""
+ for contact in ["OWC", "GWC", "GOC"]:
+ if contact in self.dframe and self.dframe["EQLNUM"].nunique() == 1:
+ # contacts are assumed constant in the dataframe
+ value = self.dframe[contact].values[0]
+ # do not include dummy contacts (shallower than the dataset)
+ if value > self.dframe["Z"].min():
+ self._figure.add_hline(
+ value,
+ line={"color": "black", "dash": "dash", "width": 1.5},
+ annotation_text=f"{contact}={value:g}",
+ annotation_position="bottom left",
+ )
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_water_layout.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_water_layout.py
new file mode 100644
index 000000000..cf8d0265c
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_water_layout.py
@@ -0,0 +1,118 @@
+import plotly.graph_objects as go
+import webviz_core_components as wcc
+from dash import dcc, html
+from dash.development.base_component import Component
+from webviz_config.webviz_plugin_subclasses import ViewElementABC
+
+from .._swatint import SwatinitQcDataModel
+from ._fullscreen import FullScreen
+from ._layout_style import LayoutStyle
+
+# This can be moved into a view_elements folder in views > water_tab
+
+
+class WaterViewelement(ViewElementABC):
+ """All elements visible in the 'Water Initialization QC plots'-tab
+ gathered in one viewelement"""
+
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ MAIN_FIGURE = "main-figure"
+ MAP_FIGURE = "map-figure"
+ INFO_BOX_EQLNUMS = "info-box-eqlnums"
+ INFO_BOX_SATNUMS = "infobox-satnums"
+ INFO_BOX_VOL_DIFF = "info-box-vol-diff"
+
+ def __init__(
+ self,
+ datamodel: SwatinitQcDataModel,
+ main_figure: go.Figure,
+ map_figure: go.Figure,
+ qc_volumes: dict,
+ ) -> None:
+ super().__init__()
+ self.datamodel = datamodel
+
+ self.main_figure = main_figure
+ self.map_figure = map_figure
+ self.qc_volumes = qc_volumes
+
+ def inner_layout(self) -> wcc.FlexBox:
+ return wcc.FlexBox(
+ children=[
+ wcc.FlexColumn(
+ flex=4,
+ children=wcc.Graph(
+ style={"height": "85vh"},
+ id=self.register_component_unique_id(
+ WaterViewelement.IDs.MAIN_FIGURE
+ ),
+ figure=self.main_figure,
+ ),
+ ),
+ wcc.FlexColumn(
+ flex=1,
+ children=[
+ FullScreen(
+ wcc.Graph(
+ responsive=True,
+ style={"height": "100%", "min-height": "45vh"},
+ id=self.register_component_unique_id(
+ WaterViewelement.IDs.MAP_FIGURE
+ ),
+ figure=self.map_figure,
+ )
+ ),
+ self.info_box,
+ ],
+ ),
+ ]
+ )
+
+ @property
+ def info_box(self) -> html.Div:
+ qc_vols = self.qc_volumes
+ height = "35vh"
+ return html.Div(
+ [
+ wcc.Header("Information about selection", style=LayoutStyle.HEADER),
+ html.Div(
+ "EQLNUMS:",
+ style={"font-weight": "bold", "font-size": "15px"},
+ ),
+ html.Div(
+ ", ".join([str(x) for x in qc_vols["EQLNUMS"]]),
+ id=self.register_component_unique_id(
+ WaterViewelement.IDs.INFO_BOX_EQLNUMS
+ ),
+ ),
+ html.Div(
+ "SATNUMS:",
+ style={"font-weight": "bold", "font-size": "15px"},
+ ),
+ html.Div(
+ ", ".join([str(x) for x in qc_vols["SATNUMS"]]),
+ id=self.register_component_unique_id(
+ WaterViewelement.IDs.INFO_BOX_SATNUMS
+ ),
+ ),
+ html.Div(
+ html.Span("Reservoir Volume Difference:"),
+ style={"font-weight": "bold", "margin-top": "10px"},
+ ),
+ html.Div(
+ children=[
+ html.Div(line)
+ for line in [
+ f"Water Volume Diff: {qc_vols['WVOL_DIFF']/(10**6):.2f} Mrm3",
+ f"Water Volume Diff (%): {qc_vols['WVOL_DIFF_PERCENT']:.2f}",
+ f"HC Volume Diff (%): {qc_vols['HCVOL_DIFF_PERCENT']:.2f}",
+ ]
+ ],
+ id=self.register_component_unique_id(
+ WaterViewelement.IDs.INFO_BOX_VOL_DIFF
+ ),
+ ),
+ ],
+ style={"height": height, "padding": "10px"},
+ )
diff --git a/webviz_subsurface/plugins/_swatinit_qc/view_elements/_waterfall_plot.py b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_waterfall_plot.py
new file mode 100644
index 000000000..38b5437f3
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/view_elements/_waterfall_plot.py
@@ -0,0 +1,114 @@
+from typing import List
+
+import plotly.graph_objects as go
+
+from .._plugin_ids import PlugInIDs
+
+
+def axis_defaults(showgrid: bool = True) -> dict:
+ return {
+ "showline": True,
+ "linewidth": 2,
+ "linecolor": "black",
+ "mirror": True,
+ "showgrid": showgrid,
+ "gridwidth": 1,
+ "gridcolor": "lightgrey",
+ }
+
+
+class WaterfallPlot:
+ # Ensure fixed order of plot elements:
+ ORDER = [
+ "SWATINIT_WVOL",
+ PlugInIDs.QcFlags.SWL_TRUNC,
+ PlugInIDs.QcFlags.PPCWMAX,
+ PlugInIDs.QcFlags.FINE_EQUIL,
+ PlugInIDs.QcFlags.HC_BELOW_FWL,
+ PlugInIDs.QcFlags.SWATINIT_1,
+ "SWAT_WVOL",
+ ]
+ MEASURES = [
+ "absolute",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ ]
+
+ def __init__(self, qc_vols: dict) -> None:
+ # collect necessary values from input and make volume values more human friendly
+ self.qc_vols = {
+ key: (qc_vols[key] / (10**6))
+ for key in self.ORDER + ["SWATINIT_HCVOL", "SWAT_HCVOL"]
+ }
+ self.qc_vols.update(
+ {key: qc_vols[key] for key in ["WVOL_DIFF_PERCENT", "HCVOL_DIFF_PERCENT"]}
+ )
+
+ @property
+ def range(self) -> list:
+ range_min = min(self.qc_vols["SWATINIT_WVOL"], self.qc_vols["SWAT_WVOL"]) * 0.95
+ range_max = max(self.qc_vols["SWATINIT_WVOL"], self.qc_vols["SWAT_WVOL"]) * 1.05
+ return [range_min, range_max]
+
+ @property
+ def figure(self) -> go.Figure:
+ return (
+ go.Figure(
+ go.Waterfall(
+ orientation="v",
+ measure=self.MEASURES,
+ x=self.ORDER,
+ textposition="outside",
+ text=self.create_bartext(),
+ y=[self.qc_vols[key] for key in self.ORDER],
+ connector={"mode": "spanning"},
+ )
+ )
+ .update_yaxes(
+ title="Water Volume (Mrm3)", range=self.range, **axis_defaults()
+ )
+ .update_xaxes(
+ type="category",
+ tickangle=-45,
+ tickfont_size=17,
+ **axis_defaults(showgrid=False),
+ )
+ .update_layout(
+ plot_bgcolor="white",
+ title="Waterfall chart showing changes from SWATINIT to SWAT",
+ margin={"t": 50, "b": 50, "l": 50, "r": 50},
+ )
+ )
+
+ def create_bartext(self) -> List[str]:
+ """
+ Create bartext for each qc_flag category with Water and HC volume change
+ relative to SWATINIT_WVOL in percent.
+ """
+ text = []
+ for bar_name in self.ORDER:
+ bartext = [f"{self.qc_vols[bar_name]:.2f} Mrm3"]
+ if bar_name != self.ORDER[0]:
+ bartext.append(
+ f"Water {self.get_water_diff_in_percent(bar_name):.1f} %"
+ )
+ bartext.append(f"HC {self.get_hc_diff_in_percent(bar_name):.1f} %")
+
+ text.append("
".join(bartext))
+ return text
+
+ def get_water_diff_in_percent(self, bar_name: str) -> float:
+ if bar_name == self.ORDER[-1]:
+ return self.qc_vols["WVOL_DIFF_PERCENT"]
+ return (self.qc_vols[bar_name] / self.qc_vols["SWATINIT_WVOL"]) * 100
+
+ def get_hc_diff_in_percent(self, bar_name: str) -> float:
+ if bar_name == self.ORDER[-1]:
+ return self.qc_vols["HCVOL_DIFF_PERCENT"]
+ if self.qc_vols["SWATINIT_HCVOL"] > 0:
+ return (-self.qc_vols[bar_name] / self.qc_vols["SWATINIT_HCVOL"]) * 100
+ return 0
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/__init__.py
new file mode 100644
index 000000000..d47bb7016
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/__init__.py
@@ -0,0 +1,3 @@
+from .capilar_tab import CapilarFilters, CapilarSelections, TabMaxPcInfoLayout
+from .overbiew_tab import OverviewTabLayout
+from .water_tab import TabQqPlotLayout, WaterFilters, WaterSelections
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/__init__.py
new file mode 100644
index 000000000..bf186e2d4
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/__init__.py
@@ -0,0 +1,2 @@
+from ._capilary_preassure import TabMaxPcInfoLayout
+from .settings import CapilarFilters, CapilarSelections
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/_capilary_preassure.py b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/_capilary_preassure.py
new file mode 100644
index 000000000..7baa0e435
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/_capilary_preassure.py
@@ -0,0 +1,153 @@
+from typing import Dict, List, Optional
+
+from dash import ALL, Input, Output, State, callback
+from webviz_config.webviz_plugin_subclasses import ViewABC
+
+from ..._plugin_ids import PlugInIDs
+from ..._swatint import SwatinitQcDataModel
+from ...view_elements import CapilarViewelement, MapFigure
+from .settings import CapilarFilters, CapilarSelections
+
+
+class TabMaxPcInfoLayout(ViewABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ CAPILAR_TAB = "capilar-tab"
+ MAIN_CLOUMN = "main-column"
+
+ def __init__(
+ self,
+ datamodel: SwatinitQcDataModel,
+ ) -> None:
+ super().__init__("Capillary pressure scaling")
+ self.datamodel = datamodel
+ self.selectors = self.datamodel.SELECTORS
+
+ main_column = self.add_column(TabMaxPcInfoLayout.IDs.MAIN_CLOUMN)
+ row = main_column.make_row()
+ row.add_view_element(
+ CapilarViewelement(self.datamodel),
+ TabMaxPcInfoLayout.IDs.CAPILAR_TAB,
+ )
+
+ self.add_settings_group(
+ CapilarSelections(self.datamodel),
+ PlugInIDs.SettingsGroups.CAPILAR_SELECTORS,
+ )
+ self.add_settings_group(
+ CapilarFilters(self.datamodel), PlugInIDs.SettingsGroups.CAPILAR_FILTERS
+ )
+
+ def set_callbacks(self) -> None:
+ # update map
+ @callback(
+ Output(
+ self.view_element(TabMaxPcInfoLayout.IDs.CAPILAR_TAB)
+ .component_unique_id(CapilarViewelement.IDs.MAP)
+ .to_string(),
+ "figure",
+ ),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Capilary.EQLNUM), "data"),
+ Input(
+ self.get_store_unique_id(PlugInIDs.Stores.Capilary.MAX_PC_SCALE),
+ "data",
+ ),
+ Input(
+ {
+ "id": self.view_element(TabMaxPcInfoLayout.IDs.CAPILAR_TAB)
+ .component_unique_id(CapilarFilters.IDs.RANGE_FILTERS)
+ .to_string(), # litt usikker på denne...?
+ "col": ALL,
+ },
+ "value",
+ ),
+ State(
+ {
+ "id": self.view_element(TabMaxPcInfoLayout.IDs.CAPILAR_TAB)
+ .component_unique_id(CapilarFilters.IDs.RANGE_FILTERS)
+ .to_string(),
+ "col": ALL,
+ },
+ "id",
+ ),
+ )
+ def _update_map(
+ eqlnums: list,
+ threshold: Optional[int],
+ continous_filters: List[List[str]],
+ continous_filters_ids: List[Dict[str, str]],
+ ) -> MapFigure:
+ df = self.datamodel.get_dataframe(
+ filters={"EQLNUM": eqlnums},
+ range_filters=zip_filters(continous_filters, continous_filters_ids),
+ )
+ df_for_map = df[df["PC_SCALING"] >= threshold]
+ if threshold is None:
+ df_for_map = self.datamodel.resample_dataframe(df, max_points=10000)
+
+ return MapFigure(
+ dframe=df_for_map,
+ color_by="EQLNUM",
+ faultlinedf=self.datamodel.faultlines_df,
+ colormap=self.datamodel.create_colormap("EQLNUM"),
+ ).figure
+
+ # update table
+ @callback(
+ Output(
+ self.view_element(TabMaxPcInfoLayout.IDs.CAPILAR_TAB)
+ .component_unique_id(CapilarViewelement.IDs.TABLE)
+ .to_string(),
+ "columns",
+ ),
+ Input(
+ self.get_store_unique_id(PlugInIDs.Stores.Capilary.MAX_PC_SCALE),
+ "data",
+ ),
+ Input(
+ self.get_store_unique_id(PlugInIDs.Stores.Capilary.SPLIT_TABLE_BY),
+ "data",
+ ),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Capilary.EQLNUM), "data"),
+ Input(
+ {"id": CapilarFilters.IDs.RANGE_FILTERS, "col": ALL},
+ "value",
+ ),
+ State(
+ {"id": CapilarFilters.IDs.RANGE_FILTERS, "col": ALL},
+ "id",
+ ),
+ )
+ def _update_table(
+ threshold: Optional[list],
+ groupby_eqlnum: list,
+ eqlnums: list,
+ continous_filters: List[List[str]],
+ continous_filters_ids: List[Dict[str, str]],
+ ) -> List[dict]:
+ df = self.datamodel.get_dataframe(
+ filters={"EQLNUM": eqlnums},
+ range_filters=zip_filters(continous_filters, continous_filters_ids),
+ )
+ dframe = (
+ self.datamodel.get_max_pc_info_and_percent_for_data_matching_condition(
+ dframe=df,
+ condition=threshold,
+ groupby_eqlnum=groupby_eqlnum == "both",
+ )
+ )
+ text_columns = self.selectors
+ columns = [
+ {
+ "name": i,
+ "id": i,
+ "type": "numeric" if i not in text_columns else "text",
+ "format": {"specifier": ".4~r"} if i not in text_columns else {},
+ }
+ for i in dframe.columns
+ ]
+ return columns
+
+
+def zip_filters(filter_values: list, filter_ids: list) -> dict:
+ return {id_val["col"]: values for values, id_val in zip(filter_values, filter_ids)}
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/__init__.py
new file mode 100644
index 000000000..6866d83b8
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/__init__.py
@@ -0,0 +1 @@
+from ._caplilar_settings import CapilarFilters, CapilarSelections
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/_caplilar_settings.py b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/_caplilar_settings.py
new file mode 100644
index 000000000..bcb19b096
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/capilar_tab/settings/_caplilar_settings.py
@@ -0,0 +1,121 @@
+from logging import captureWarnings
+from typing import List
+
+import webviz_core_components as wcc
+from dash import Input, Output, callback, dcc
+from dash.development.base_component import Component
+from webviz_config.webviz_plugin_subclasses import SettingsGroupABC
+
+from ...._plugin_ids import PlugInIDs
+from ...._swatint import SwatinitQcDataModel
+
+
+class CapilarSelections(SettingsGroupABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ SPLIT_TABLE_BY = "split-table-by"
+ MAX_THRESH = "max-thresh"
+
+ def __init__(self, datamodel: SwatinitQcDataModel) -> None:
+ super().__init__("Selections")
+ self.datamodel = datamodel
+
+ def layout(self) -> List[Component]:
+ return [
+ wcc.RadioItems(
+ label="Split table by:",
+ id=self.register_component_unique_id(
+ CapilarSelections.IDs.SPLIT_TABLE_BY
+ ),
+ options=[
+ {"label": "SATNUM", "value": "SATNUM"},
+ {"label": "SATNUM and EQLNUM", "value": "both"},
+ ],
+ value="SATNUM",
+ ),
+ wcc.Label("Maximum PC_SCALING threshold"),
+ dcc.Input(
+ id=self.register_component_unique_id(CapilarSelections.IDs.MAX_THRESH),
+ type="number",
+ persistence=True,
+ persistence_type="session",
+ ),
+ ]
+
+ def set_callbacks(self) -> None:
+ @callback(
+ Output(
+ self.get_store_unique_id(PlugInIDs.Stores.Capilary.SPLIT_TABLE_BY),
+ "data",
+ ),
+ Input(
+ self.component_unique_id(
+ CapilarSelections.IDs.SPLIT_TABLE_BY
+ ).to_string(),
+ "value",
+ ),
+ )
+ def _set_split(split: str) -> str:
+ return split
+
+ @callback(
+ Output(
+ self.get_store_unique_id(PlugInIDs.Stores.Capilary.MAX_PC_SCALE), "data"
+ ),
+ Input(
+ self.component_unique_id(CapilarSelections.IDs.MAX_THRESH).to_string(),
+ "value",
+ ),
+ )
+ def _set_max_pc(max_pc: int) -> int:
+ return max_pc
+
+
+class CapilarFilters(SettingsGroupABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ EQLNUM = "eqlnum"
+ RANGE_FILTERS = "range_filters"
+
+ def __init__(self, datamodel: SwatinitQcDataModel) -> None:
+ super().__init__("Filter")
+ self.datamodel = datamodel
+ self.range_filters_id = self.register_component_unique_id(
+ CapilarFilters.IDs.RANGE_FILTERS
+ )
+
+ def layout(self) -> List[Component]:
+ return [
+ wcc.SelectWithLabel(
+ label="EQLNUM",
+ id=self.register_component_unique_id(CapilarFilters.IDs.EQLNUM),
+ options=[
+ {"label": ens, "value": ens} for ens in self.datamodel.eqlnums
+ ],
+ value=self.datamodel.eqlnums[:1],
+ size=min(8, len(self.datamodel.eqlnums)),
+ multi=True,
+ ),
+ self.range_filters,
+ ]
+
+ @property
+ def range_filters(self) -> List:
+ dframe = self.datamodel.dframe
+ filters = []
+ for col in self.datamodel.filters_continuous:
+ min_val, max_val = dframe[col].min(), dframe[col].max()
+ filters.append(
+ wcc.RangeSlider(
+ label="Depth range" if col == "Z" else col,
+ id={"id": self.range_filters_id, "col": col},
+ min=min_val,
+ max=max_val,
+ value=[min_val, max_val],
+ marks={
+ str(val): {"label": f"{val:.2f}"} for val in [min_val, max_val]
+ },
+ tooltip={"always_visible": False},
+ )
+ )
+ return filters
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/__init__.py
new file mode 100644
index 000000000..98c6f3689
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/__init__.py
@@ -0,0 +1 @@
+from ._overview_info import OverviewTabLayout
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/_overview_info.py b/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/_overview_info.py
new file mode 100644
index 000000000..1e3686224
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/overbiew_tab/_overview_info.py
@@ -0,0 +1,46 @@
+import webviz_core_components as wcc
+from dash import Input, Output, State, callback, html
+from dash.exceptions import PreventUpdate
+from webviz_config.webviz_plugin_subclasses import ViewABC
+
+from ..._plugin_ids import PlugInIDs
+from ..._swatint import SwatinitQcDataModel
+from ...view_elements import OverviewViewelement
+
+
+class OverviewTabLayout(ViewABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ OVERVIEW_TAB = "overview-tab"
+ MAIN_CLOUMN = "main-column"
+
+ def __init__(self, datamodel: SwatinitQcDataModel) -> None:
+ super().__init__("Overview and Information")
+ self.datamodel = datamodel
+
+ main_column = self.add_column(OverviewTabLayout.IDs.MAIN_CLOUMN)
+ row = main_column.make_row()
+ row.add_view_element(
+ OverviewViewelement(self.datamodel), OverviewTabLayout.IDs.OVERVIEW_TAB
+ )
+
+ def set_callbacks(self) -> None:
+ @callback(
+ Output(
+ self.view_element(OverviewTabLayout.IDs.OVERVIEW_TAB)
+ .component_unique_id(OverviewViewelement.IDs.INFO_DIALOG)
+ .to_string(),
+ "open",
+ ),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Overview.BUTTON), "data"),
+ State(
+ self.view_element(OverviewTabLayout.IDs.OVERVIEW_TAB)
+ .component_unique_id(OverviewViewelement.IDs.INFO_DIALOG)
+ .to_string(),
+ "open",
+ ),
+ )
+ def open_close_information_dialog(_n_click: list, is_open: bool) -> bool:
+ if _n_click is not None:
+ return not is_open
+ raise PreventUpdate
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/__init__.py
new file mode 100644
index 000000000..c4f38d69d
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/__init__.py
@@ -0,0 +1,2 @@
+from ._water_initialization import TabQqPlotLayout
+from .settings import WaterFilters, WaterSelections
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/_water_initialization.py b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/_water_initialization.py
new file mode 100644
index 000000000..2088ddfce
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/_water_initialization.py
@@ -0,0 +1,233 @@
+from typing import Dict, List, Optional, Tuple, Union
+
+import plotly.graph_objects as go
+from dash import ALL, Input, Output, State, callback, callback_context
+from webviz_config.webviz_plugin_subclasses import ViewABC
+
+from ..._plugin_ids import PlugInIDs
+from ..._swatint import SwatinitQcDataModel
+from ...view_elements import (
+ MapFigure,
+ PropertiesVsDepthSubplots,
+ WaterfallPlot,
+ WaterViewelement,
+)
+from .settings import WaterFilters, WaterSelections
+
+
+class TabQqPlotLayout(ViewABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ WATER_TAB = "water-tab"
+ MAIN_COLUMN = "main-column"
+
+ def __init__(
+ self,
+ datamodel: SwatinitQcDataModel,
+ ) -> None:
+ super().__init__("Water Initialization QC plots")
+ self.datamodel = datamodel
+
+ # Need to define these quanitities for the inital case
+ self.main_figure = main_figure
+ self.map_figure = map_figure
+ self.qc_volumes = qc_volumes
+
+ main_column = self.add_column(TabQqPlotLayout.IDs.MAIN_COLUMN)
+ row = main_column.make_row()
+ row.add_view_element(
+ WaterViewelement(
+ self.datamodel, self.main_figure, self.map_figure, self.qc_volumes
+ ),
+ TabQqPlotLayout.IDs.WATER_TAB,
+ )
+
+ self.add_settings_group(
+ WaterSelections(self.datamodel), PlugInIDs.SettingsGroups.WATER_SEELECTORS
+ )
+ self.add_settings_group(
+ WaterFilters(self.datamodel), PlugInIDs.SettingsGroups.WATER_FILTERS
+ )
+
+ def set_callbacks(self) -> None:
+ # update
+ @callback(
+ Output(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAIN_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ Output(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAP_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ Output(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.INFO_BOX_EQLNUMS)
+ .to_string(),
+ "children",
+ ),
+ Output(
+ self.view_elements(TabQqPlotLayout.IDs.WATER_TAB).component_unique_id(
+ WaterViewelement.IDs.INFO_BOX_SATNUMS
+ ),
+ "children",
+ ),
+ Output(
+ self.view_elements(TabQqPlotLayout.IDs.WATER_TAB).component_unique_id(
+ WaterViewelement.IDs.INFO_BOX_VOL_DIFF
+ ),
+ "children",
+ ),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Water.QC_VIZ), "data"),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Water.EQLNUM), "data"),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Water.COLOR_BY), "data"),
+ Input(self.get_store_unique_id(PlugInIDs.Stores.Water.MAX_POINTS), "data"),
+ Input({"id": WaterFilters.range_filters_id, "col": ALL}, "value"),
+ Input({"id": WaterFilters.descreate_filter_id, "col": ALL}, "value"),
+ State({"id": WaterFilters.range_filters_id, "col": ALL}, "id"),
+ State({"id": WaterFilters.descreate_filter_id, "col": ALL}, "id"),
+ )
+ # pylint: disable=too-many-arguments
+ def _update_plot(
+ qc_viz: str,
+ eqlnums: List[str],
+ color_by: str,
+ max_points: int,
+ continous_filters_val: List[List[str]],
+ descreate_filters_val: List[List[str]],
+ continous_filters_ids: List[Dict[str, str]],
+ descreate_filters_ids: List[Dict[str, str]],
+ ) -> list:
+
+ filters = zip_filters(descreate_filters_val, descreate_filters_ids)
+ filters.update({"EQLNUM": eqlnums})
+
+ df = self.datamodel.get_dataframe(
+ filters=filters,
+ range_filters=zip_filters(continous_filters_val, continous_filters_ids),
+ )
+ if df.empty:
+ return ["No data left after filtering"]
+
+ qc_volumes = self.datamodel.compute_qc_volumes(df)
+
+ df = self.datamodel.filter_dframe_on_depth(df)
+ df = self.datamodel.resample_dataframe(df, max_points=max_points)
+
+ colormap = self.datamodel.create_colormap(color_by)
+ main_plot = (
+ WaterfallPlot(qc_vols=qc_volumes).figure
+ if qc_viz == WaterSelections.Values.WATERFALL
+ else PropertiesVsDepthSubplots(
+ dframe=df,
+ color_by=color_by,
+ colormap=colormap,
+ discrete_color=color_by in self.datamodel.SELECTORS,
+ ).figure
+ )
+ map_figure = MapFigure(
+ dframe=df,
+ color_by=color_by,
+ faultlinedf=self.datamodel.faultlines_df,
+ colormap=colormap,
+ ).figure
+
+ return qc_plot_layout.main_layout(
+ main_figure=main_plot,
+ map_figure=map_figure,
+ qc_volumes=qc_volumes,
+ ) # this must return something else
+
+ @callback(
+ Output(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAIN_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ Output(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAP_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ Input(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAIN_FIGURE)
+ .to_string(),
+ "selectedData",
+ ),
+ Input(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAP_FIGURE)
+ .to_string(),
+ "selectedData",
+ ),
+ State(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAIN_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ State(
+ self.view_element(TabQqPlotLayout.IDs.WATER_TAB)
+ .component_unique_id(WaterViewelement.IDs.MAP_FIGURE)
+ .to_string(),
+ "figure",
+ ),
+ )
+ def _update_selected_points_in_figure(
+ selected_main: dict, selected_map: dict, mainfig: dict, mapfig: dict
+ ) -> Tuple[dict, dict]:
+ ctx = callback_context.triggered[0]["prop_id"]
+
+ selected = (
+ selected_map
+ if WaterViewelement.IDs.MAP_FIGURE in ctx
+ else selected_main
+ )
+ point_indexes = get_point_indexes_from_selected(selected)
+
+ for trace in mainfig["data"]:
+ update_selected_points_in_trace(trace, point_indexes)
+ for trace in mapfig["data"]:
+ update_selected_points_in_trace(trace, point_indexes)
+
+ return mainfig, mapfig
+
+ def get_point_indexes_from_selected(
+ selected: Optional[dict],
+ ) -> Union[list, dict]:
+ if not (isinstance(selected, dict) and "points" in selected):
+ return []
+
+ continous_color = "marker.color" in selected["points"][0]
+ if continous_color:
+ return [point["pointNumber"] for point in selected["points"]]
+
+ point_indexes: dict = {}
+ for point in selected["points"]:
+ trace_name = str(point["customdata"][0])
+ if trace_name not in point_indexes:
+ point_indexes[trace_name] = []
+ point_indexes[trace_name].append(point["pointNumber"])
+ return point_indexes
+
+ def update_selected_points_in_trace(
+ trace: dict, point_indexes: Union[dict, list]
+ ) -> None:
+ if "name" in trace:
+ selectedpoints = (
+ point_indexes
+ if isinstance(point_indexes, list)
+ else point_indexes.get(trace["name"], [])
+ )
+ trace.update(selectedpoints=selectedpoints if point_indexes else None)
+
+
+def zip_filters(filter_values: list, filter_ids: list) -> dict:
+ return {id_val["col"]: values for values, id_val in zip(filter_values, filter_ids)}
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/__init__.py b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/__init__.py
new file mode 100644
index 000000000..fc900d6d1
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/__init__.py
@@ -0,0 +1 @@
+from ._water_settings import WaterFilters, WaterSelections
diff --git a/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/_water_settings.py b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/_water_settings.py
new file mode 100644
index 000000000..20ad65680
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc/views/water_tab/settings/_water_settings.py
@@ -0,0 +1,163 @@
+from typing import List
+
+import webviz_core_components as wcc
+from dash import Input, Output, callback, dcc
+from dash.development.base_component import Component
+from webviz_config.webviz_plugin_subclasses import SettingsGroupABC
+
+from ...._plugin_ids import PlugInIDs
+from ...._swatint import SwatinitQcDataModel
+
+
+class WaterSelections(SettingsGroupABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ SELECT_QC = "select-qc"
+ EQLNUM = "eqlnum"
+ COLOR_BY = "color-by"
+ MAX_POINTS = "max-points"
+
+ class Values:
+ # pylint: disable=too-few-public-methods
+ WATERFALL = "waterfall"
+ PROP_VS_DEPTH = "prop-vs-depth"
+
+ def __init__(self, datamodel: SwatinitQcDataModel) -> None:
+ super().__init__("Selections")
+ self.datamodel = datamodel
+
+ def layout(self) -> List[Component]:
+ return [
+ wcc.Dropdown(
+ label="Select QC-visualization:",
+ id=self.register_component_unique_id(WaterSelections.IDs.SELECT_QC),
+ options=[
+ {
+ "label": "Waterfall plot for water vol changes",
+ "value": WaterSelections.Values.WATERFALL,
+ },
+ {
+ "label": "Reservoir properties vs Depth",
+ "value": WaterSelections.Values.PROP_VS_DEPTH,
+ },
+ ],
+ value=WaterSelections.Values.PROP_VS_DEPTH,
+ clearable=False,
+ ),
+ wcc.SelectWithLabel(
+ label="EQLNUM",
+ id=self.register_component_unique_id(WaterSelections.IDs.EQLNUM),
+ options=[
+ {"label": ens, "value": ens} for ens in self.datamodel.eqlnums
+ ],
+ value=self.datamodel.eqlnums[:1],
+ size=min(8, len(self.datamodel.eqlnums)),
+ multi=True,
+ ),
+ wcc.Dropdown(
+ label="Color by",
+ id=self.register_component_unique_id(WaterSelections.IDs.COLOR_BY),
+ options=[
+ {"label": ens, "value": ens}
+ for ens in self.datamodel.color_by_selectors
+ ],
+ value="QC_FLAG",
+ clearable=False,
+ ),
+ wcc.Label("Max number of points:"),
+ dcc.Input(
+ id=self.register_component_unique_id(WaterSelections.IDs.MAX_POINTS),
+ type="number",
+ value=5000,
+ ),
+ ]
+
+ def set_callbacks(self) -> None:
+ @callback(
+ Output(self.get_store_unique_id(PlugInIDs.Stores.Water.QC_VIZ), "data"),
+ Input(self.component_unique_id(WaterSelections.IDs.SELECT_QC), "value"),
+ )
+ def _set_qc_viz(qc_viz: str) -> str:
+ return qc_viz
+
+ @callback(
+ Output(self.get_store_unique_id(PlugInIDs.Stores.Water.EQLNUM), "data"),
+ Input(self.component_unique_id(WaterSelections.IDs.EQLNUM), "value"),
+ )
+ def _set_eqlnum(eqlnum: int) -> int:
+ return eqlnum
+
+ @callback(
+ Output(self.get_store_unique_id(PlugInIDs.Stores.Water.COLOR_BY), "data"),
+ Input(self.component_unique_id(WaterSelections.IDs.COLOR_BY), "value"),
+ )
+ def _set_color_by(color: str) -> str:
+ return color
+
+ @callback(
+ Output(self.get_store_unique_id(PlugInIDs.Stores.Water.MAX_POINTS), "data"),
+ Input(self.component_unique_id(WaterSelections.IDs.MAX_POINTS), "value"),
+ )
+ def _set_max_points(max: str) -> str:
+ return max
+
+
+class WaterFilters(SettingsGroupABC):
+ class IDs:
+ # pylint: disable=too-few-public-methods
+ DESCREATE_FILTERS = "descreate-filters"
+ RANGE_FILTERS = "range_filters"
+
+ def __init__(self, datamodel) -> None:
+ super().__init__("Filters")
+ self.datamodel = datamodel
+ self.descreate_fiters_id = self.register_component_unique_id(
+ WaterFilters.IDs.DESCREATE_FILTERS
+ )
+ self.range_filters_id = self.register_component_unique_id(
+ WaterFilters.IDs.RANGE_FILTERS
+ )
+
+ def layout(self) -> List[Component]:
+ return [
+ wcc.SelectWithLabel(
+ label="QC_FLAG",
+ id={"id": self.range_filters_id, "col": "qc-flag"},
+ options=[
+ {"label": ens, "value": ens} for ens in self.datamodel.qc_flag
+ ],
+ value=self.datamodel.qc_flag,
+ size=min(8, len(self.datamodel.qc_flag)),
+ ),
+ wcc.SelectWithLabel(
+ label="SATNUM",
+ id={"id": self.range_filters_id, "col": "satnum"},
+ options=[
+ {"label": ens, "value": ens} for ens in self.datamodel.satnums
+ ],
+ value=self.datamodel.satnums,
+ size=min(8, len(self.datamodel.satnums)),
+ ),
+ self.range_filters,
+ ]
+
+ @property
+ def range_filters(self) -> List:
+ dframe = self.datamodel.dframe
+ filters = []
+ for col in self.datamodel.filters_continuous:
+ min_val, max_val = dframe[col].min(), dframe[col].max()
+ filters.append(
+ wcc.RangeSlider(
+ label="Depth range" if col == "Z" else col,
+ id={"id": self.range_filters_id, "col": col},
+ min=min_val,
+ max=max_val,
+ value=[min_val, max_val],
+ marks={
+ str(val): {"label": f"{val:.2f}"} for val in [min_val, max_val]
+ },
+ tooltip={"always_visible": False},
+ )
+ )
+ return filters
diff --git a/webviz_subsurface/plugins/_swatinit_qc_old/__init__.py b/webviz_subsurface/plugins/_swatinit_qc_old/__init__.py
new file mode 100644
index 000000000..262986762
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc_old/__init__.py
@@ -0,0 +1 @@
+from ._plugin import SwatinitQC
diff --git a/webviz_subsurface/plugins/_swatinit_qc_old/_business_logic.py b/webviz_subsurface/plugins/_swatinit_qc_old/_business_logic.py
new file mode 100644
index 000000000..ea4ce335b
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc_old/_business_logic.py
@@ -0,0 +1,330 @@
+import re
+from enum import Enum
+from pathlib import Path
+from typing import Callable, Dict, List, Optional, Tuple
+
+import numpy as np
+import pandas as pd
+from webviz_config import WebvizSettings
+from webviz_config.common_cache import CACHE
+from webviz_config.webviz_store import webvizstore
+
+
+class QcFlags(str, Enum):
+ """Constants for use by check_swatinit"""
+
+ FINE_EQUIL = "FINE_EQUIL"
+ HC_BELOW_FWL = "HC_BELOW_FWL"
+ PC_SCALED = "PC_SCALED"
+ PPCWMAX = "PPCWMAX"
+ SWATINIT_1 = "SWATINIT_1"
+ SWL_TRUNC = "SWL_TRUNC"
+ UNKNOWN = "UNKNOWN"
+ WATER = "WATER"
+
+
+class SwatinitQcDataModel:
+ """Class keeping the data needed in the vizualisations and various
+ data providing methods.
+ """
+
+ COLNAME_THRESHOLD = "HC cells above threshold (%)"
+ SELECTORS = ["QC_FLAG", "SATNUM", "EQLNUM", "FIPNUM"]
+
+ DROP_COLUMNS = [
+ "Z_MIN",
+ "Z_MAX",
+ "GLOBAL_INDEX",
+ "SWATINIT_SWAT",
+ "SWATINIT_SWAT_WVOL",
+ "SWL_x",
+ "SWL_y",
+ "Z_DATUM",
+ "PRESSURE_DATUM",
+ ]
+
+ def __init__(
+ self,
+ webviz_settings: WebvizSettings,
+ csvfile: str,
+ ensemble: str = None,
+ realization: Optional[int] = None,
+ faultlines: Optional[Path] = None,
+ ):
+
+ self._theme = webviz_settings.theme
+ self._faultlines = faultlines
+ self.faultlines_df = read_csv(faultlines) if faultlines else None
+
+ if ensemble is not None:
+ if isinstance(ensemble, list):
+ raise TypeError(
+ 'Incorrent argument type, "ensemble" must be a string instead of a list'
+ )
+ if realization is None:
+ raise ValueError('Incorrent arguments, "realization" must be specified')
+
+ ens_path = webviz_settings.shared_settings["scratch_ensembles"][ensemble]
+ # replace realization in string from scratch_ensemble with input realization
+ ens_path = re.sub(
+ "realization-[^/]", f"realization-{realization}", ens_path
+ )
+ self.csvfile = Path(ens_path) / csvfile
+ else:
+ self.csvfile = Path(csvfile)
+
+ self.dframe = read_csv(self.csvfile)
+ self.dframe.drop(columns=self.DROP_COLUMNS, errors="ignore", inplace=True)
+
+ for col in self.SELECTORS + ["OWC", "GWC", "GOC"]:
+ if col in self.dframe:
+ self.dframe[col] = self.dframe[col].astype("category")
+
+ self._initial_qc_volumes = self.compute_qc_volumes(self.dframe)
+
+ @property
+ def webviz_store(self) -> List[Tuple[Callable, List[dict]]]:
+ return [
+ (
+ read_csv,
+ [
+ {"csv_file": path}
+ for path in [self._faultlines, self.csvfile]
+ if path is not None
+ ],
+ )
+ ]
+
+ @property
+ def colors(self) -> List[str]:
+ return self._theme.plotly_theme["layout"]["colorway"]
+
+ @property
+ def qc_flag_colors(self) -> Dict[str, str]:
+ """Predefined colors for the QC_FLAG column"""
+ return {
+ QcFlags.FINE_EQUIL.value: self.colors[8],
+ QcFlags.HC_BELOW_FWL.value: self.colors[5],
+ QcFlags.PC_SCALED.value: self.colors[2],
+ QcFlags.PPCWMAX.value: self.colors[9],
+ QcFlags.SWATINIT_1.value: self.colors[6],
+ QcFlags.SWL_TRUNC.value: self.colors[3],
+ QcFlags.UNKNOWN.value: self.colors[1],
+ QcFlags.WATER.value: self.colors[0],
+ }
+
+ @property
+ def eqlnums(self) -> List[str]:
+ return sorted(list(self.dframe["EQLNUM"].unique()), key=int)
+
+ @property
+ def satnums(self) -> List[str]:
+ return sorted(list(self.dframe["SATNUM"].unique()), key=int)
+
+ @property
+ def qc_flag(self) -> List[str]:
+ return sorted(list(self.dframe["QC_FLAG"].unique()))
+
+ @property
+ def filters_discrete(self) -> List[str]:
+ return ["QC_FLAG", "SATNUM"]
+
+ @property
+ def filters_continuous(self) -> List[str]:
+ return ["Z", "PC", "SWATINIT", "PERMX", "PORO"]
+
+ @property
+ def color_by_selectors(self) -> List[str]:
+ return self.SELECTORS + ["PERMX", "PORO"]
+
+ @property
+ def pc_scaling_min_max(self) -> Tuple[float, float]:
+ return (self.dframe["PC_SCALING"].max(), self.dframe["PC_SCALING"].min())
+
+ @property
+ def vol_diff_total(self) -> Tuple[float, float]:
+ return (
+ self._initial_qc_volumes["WVOL_DIFF_PERCENT"],
+ self._initial_qc_volumes["HCVOL_DIFF_PERCENT"],
+ )
+
+ def get_dataframe(
+ self,
+ filters: Optional[dict] = None,
+ range_filters: Optional[dict] = None,
+ ) -> pd.DataFrame:
+
+ df = self.dframe.copy()
+ filters = filters if filters is not None else {}
+ range_filters = range_filters if range_filters is not None else {}
+
+ for filt, value in filters.items():
+ df = df[df[filt].isin(value)]
+
+ for filt, value in range_filters.items():
+ min_val, max_val = value
+ df = df[(df[filt] >= min_val) & (df[filt] <= max_val) | (df[filt].isnull())]
+
+ return df
+
+ @staticmethod
+ def resample_dataframe(dframe: pd.DataFrame, max_points: int) -> pd.DataFrame:
+ """Resample a dataframe to max number of points. The sampling will be
+ weighted in order to avoid removal of points that has an important qc_flag.
+ Points will mostly be removed if they are flagged as "WATER" or "PC_SCALED"
+ """
+ if dframe.shape[0] > max_points:
+ dframe = dframe.copy()
+ dframe["sample_weight"] = 1
+ dframe.loc[dframe["QC_FLAG"] == "WATER", "sample_weight"] = 0.1
+ dframe.loc[dframe["QC_FLAG"] == "PC_SCALED", "sample_weight"] = 0.5
+ return dframe.sample(max_points, weights=dframe["sample_weight"])
+ return dframe
+
+ @staticmethod
+ def filter_dframe_on_depth(dframe: pd.DataFrame) -> pd.DataFrame:
+ """Suggest a deep depth limit for what to plot, in order to avoid
+ showing too much of a less interesting water zone
+ """
+ max_z = dframe["Z"].max()
+ hc_dframe = dframe[dframe["SWATINIT"] < 1]
+ if not hc_dframe.empty:
+ lowest_hc = hc_dframe["Z"].max()
+ hc_height = lowest_hc - dframe["Z"].min()
+ # Suggest to visualize a water height of 10% of the hc zone:
+ max_z = lowest_hc + 0.2 * hc_height
+
+ return dframe[dframe["Z"] <= max_z]
+
+ @staticmethod
+ def compute_qc_volumes(dframe: pd.DataFrame) -> dict:
+ """Compute numbers relevant for QC of saturation initialization of a
+ reservoir model.
+ Different volume numbers are typically related to the different QC_FLAG
+ """
+ qc_vols: dict = {}
+
+ # Ensure all QCFlag categories are represented:
+ for qc_cat in QcFlags:
+ qc_vols[qc_cat.value] = 0.0
+
+ # Overwrite dict values with correct figures:
+ for qc_cat, qc_df in dframe.groupby("QC_FLAG"):
+ qc_vols[qc_cat] = (
+ (qc_df["SWAT"] - qc_df["SWATINIT"]) * qc_df["PORV"]
+ ).sum()
+
+ if "VOLUME" in dframe:
+ qc_vols["VOLUME"] = dframe["VOLUME"].sum()
+
+ qc_vols["PORV"] = dframe["PORV"].sum()
+ qc_vols["SWATINIT_WVOL"] = (dframe["SWATINIT"] * dframe["PORV"]).sum()
+ qc_vols["SWATINIT_HCVOL"] = qc_vols["PORV"] - qc_vols["SWATINIT_WVOL"]
+ qc_vols["SWAT_WVOL"] = (dframe["SWAT"] * dframe["PORV"]).sum()
+ qc_vols["SWAT_HCVOL"] = qc_vols["PORV"] - qc_vols["SWAT_WVOL"]
+
+ # compute difference columns
+ qc_vols["WVOL_DIFF"] = qc_vols["SWAT_WVOL"] - qc_vols["SWATINIT_WVOL"]
+ qc_vols["WVOL_DIFF_PERCENT"] = (
+ qc_vols["WVOL_DIFF"] / qc_vols["SWATINIT_WVOL"]
+ ) * 100
+ qc_vols["HCVOL_DIFF"] = qc_vols["SWAT_HCVOL"] - qc_vols["SWATINIT_HCVOL"]
+ qc_vols["HCVOL_DIFF_PERCENT"] = (
+ ((qc_vols["HCVOL_DIFF"] / qc_vols["SWATINIT_HCVOL"]) * 100)
+ if qc_vols["HCVOL_DIFF"] != 0.0
+ else 0
+ )
+ qc_vols["EQLNUMS"] = sorted(dframe["EQLNUM"].unique())
+ qc_vols["SATNUMS"] = sorted(dframe["SATNUM"].unique())
+
+ return qc_vols
+
+ def create_colormap(self, color_by: str) -> dict:
+ """Create a colormap to ensure that the subplot and the mapfigure
+ has the same color for the same unique value. If 'QC_FLAG' is used as
+ color column, predefined colors are used.
+ """
+
+ return (
+ dict(zip(self.dframe[color_by].unique(), self.colors * 10))
+ if color_by != "QC_FLAG"
+ else self.qc_flag_colors
+ )
+
+ def get_max_pc_info_and_percent_for_data_matching_condition(
+ self,
+ dframe: pd.DataFrame,
+ condition: Optional[int],
+ groupby_eqlnum: bool = True,
+ ) -> pd.DataFrame:
+ def get_percent_of_match(df: pd.DataFrame, condition: Optional[int]) -> float:
+ df = df[df["QC_FLAG"] == "PC_SCALED"]
+ if condition is None or df.empty:
+ return np.nan
+ return (len(df[df["PC_SCALING"] >= condition]) / len(df)) * 100
+
+ groupby = ["SATNUM"] if not groupby_eqlnum else ["EQLNUM", "SATNUM"]
+ df_group = dframe.groupby(groupby)
+ df = df_group.max()[["PCOW_MAX", "PPCW", "PC_SCALING"]].round(6)
+ df[self.COLNAME_THRESHOLD] = df_group.apply(
+ lambda x: get_percent_of_match(x, condition)
+ )
+ return df.reset_index().sort_values(groupby, key=lambda col: col.astype(int))
+
+ def table_data_qc_vol_overview(self) -> tuple:
+ """Return data and columns for dash_table showing overview of qc volumes"""
+
+ skip_if_zero = [QcFlags.UNKNOWN.value, QcFlags.WATER.value]
+ column_order = [
+ "",
+ "Response",
+ "Water Volume Diff",
+ "HC Volume Diff",
+ "Water Volume Mrm3",
+ "HC Volume Mrm3",
+ ]
+ qc_vols = self._initial_qc_volumes
+
+ table_data = []
+ # First report the SWATINIT volumes
+ table_data.append(
+ {
+ "Response": "SWATINIT",
+ "Water Volume Mrm3": f"{qc_vols['SWATINIT_WVOL']/1e6:>10.3f}",
+ "HC Volume Mrm3": f" {qc_vols['SWATINIT_HCVOL']/1e6:>8.3f}",
+ }
+ )
+ # Then report the volume change per QC_FLAG
+ for key in [x.value for x in QcFlags]:
+ if key in skip_if_zero and np.isclose(qc_vols[key], 0, atol=1):
+ # Tolerance is 1 rm3, which is small in relevant contexts.
+ continue
+ table_data.append(
+ {
+ "": "+",
+ "Response": key,
+ "Water Volume Mrm3": f"{qc_vols[key]/1e6:>10.3f}",
+ "Water Volume Diff": f"{qc_vols[key]/qc_vols['SWATINIT_WVOL']*100:>3.2f} %",
+ "HC Volume Diff": f"{-qc_vols[key]/qc_vols['SWATINIT_HCVOL']*100:>3.2f} %"
+ if qc_vols["SWATINIT_HCVOL"] > 0
+ else "0.00 %",
+ }
+ )
+ # Last report the SWAT volumes and change from SWATINIT
+ table_data.append(
+ {
+ "": "=",
+ "Response": "SWAT",
+ "Water Volume Mrm3": f"{qc_vols['SWAT_WVOL']/1e6:>10.3f}",
+ "Water Volume Diff": f"{qc_vols['WVOL_DIFF_PERCENT']:>3.2f} %",
+ "HC Volume Diff": f"{qc_vols['HCVOL_DIFF_PERCENT']:>3.2f} %",
+ "HC Volume Mrm3": f"{qc_vols['SWAT_HCVOL']/1e6:>8.3f}",
+ }
+ )
+ return table_data, [{"name": i, "id": i} for i in column_order]
+
+
+@CACHE.memoize(timeout=CACHE.TIMEOUT)
+@webvizstore
+def read_csv(csv_file: str) -> pd.DataFrame:
+ return pd.read_csv(csv_file)
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_callbacks.py b/webviz_subsurface/plugins/_swatinit_qc_old/_callbacks.py
similarity index 100%
rename from webviz_subsurface/plugins/_swatinit_qc/_callbacks.py
rename to webviz_subsurface/plugins/_swatinit_qc_old/_callbacks.py
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_figures.py b/webviz_subsurface/plugins/_swatinit_qc_old/_figures.py
similarity index 100%
rename from webviz_subsurface/plugins/_swatinit_qc/_figures.py
rename to webviz_subsurface/plugins/_swatinit_qc_old/_figures.py
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_layout.py b/webviz_subsurface/plugins/_swatinit_qc_old/_layout.py
similarity index 100%
rename from webviz_subsurface/plugins/_swatinit_qc/_layout.py
rename to webviz_subsurface/plugins/_swatinit_qc_old/_layout.py
diff --git a/webviz_subsurface/plugins/_swatinit_qc/_markdown.py b/webviz_subsurface/plugins/_swatinit_qc_old/_markdown.py
similarity index 100%
rename from webviz_subsurface/plugins/_swatinit_qc/_markdown.py
rename to webviz_subsurface/plugins/_swatinit_qc_old/_markdown.py
diff --git a/webviz_subsurface/plugins/_swatinit_qc_old/_plugin.py b/webviz_subsurface/plugins/_swatinit_qc_old/_plugin.py
new file mode 100644
index 000000000..8cdc38e0d
--- /dev/null
+++ b/webviz_subsurface/plugins/_swatinit_qc_old/_plugin.py
@@ -0,0 +1,67 @@
+from pathlib import Path
+from typing import Callable, List, Optional, Tuple
+
+import webviz_core_components as wcc
+from webviz_config import WebvizPluginABC, WebvizSettings
+
+from ._business_logic import SwatinitQcDataModel
+from ._callbacks import plugin_callbacks
+from ._layout import plugin_main_layout
+
+
+class SwatinitQC(WebvizPluginABC):
+ """This plugin is used to visualize the output from [check_swatinit]\
+(https://fmu-docs.equinor.com/docs/subscript/scripts/check_swatinit.html) which is a QC tool
+for Water Initialization in Eclipse runs when the `SWATINIT` keyword has been used. It is used to
+quantify how much the volume changes from `SWATINIT` to `SWAT` at time zero in the dynamical model,
+and help understand why it changes.
+
+---
+* **`csvfile`:** Path to an csvfile from check_swatinit. The path should be relative to the runpath
+if ensemble and realization is given as input, if not the path needs to be absolute.
+* **`ensemble`:** Which ensemble in `shared_settings` to visualize.
+* **`realization`:** Which realization to pick from the ensemble
+* **`faultlines`**: A csv file containing faultpolygons to be visualized together with the map view.
+Export format from [xtgeo.xyz.polygons.dataframe](
+https://xtgeo.readthedocs.io/en/latest/apiref/xtgeo.xyz.polygons.html#xtgeo.xyz.polygons.Polygons.dataframe
+) \
+[(example file)](\
+https://github.com/equinor/webviz-subsurface-testdata/blob/master/01_drogon_ahm/\
+realization-0/iter-0/share/results/polygons/toptherys--gl_faultlines_extract_postprocess.csv).
+
+---
+The `csvfile` can be generated by running the [CHECK_SWATINIT](https://fmu-docs.equinor.com/\
+docs/ert/reference/forward_models.html?highlight=swatinit#CHECK_SWATINIT) forward model in ERT,
+or with the "check_swatinit" command line tool.
+
+"""
+
+ def __init__(
+ self,
+ webviz_settings: WebvizSettings,
+ csvfile: str = "share/results/tables/check_swatinit.csv",
+ ensemble: Optional[str] = None,
+ realization: Optional[int] = None,
+ faultlines: Path = None,
+ ) -> None:
+ super().__init__()
+
+ self._datamodel = SwatinitQcDataModel(
+ webviz_settings=webviz_settings,
+ csvfile=csvfile,
+ ensemble=ensemble,
+ realization=realization,
+ faultlines=faultlines,
+ )
+ self.add_webvizstore()
+ self.set_callbacks()
+
+ @property
+ def layout(self) -> wcc.Tabs:
+ return plugin_main_layout(self.uuid, self._datamodel)
+
+ def set_callbacks(self) -> None:
+ plugin_callbacks(self.uuid, self._datamodel)
+
+ def add_webvizstore(self) -> List[Tuple[Callable, List[dict]]]:
+ return self._datamodel.webviz_store