diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63ac1474e..267e978ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#1244](https://github.com/equinor/webviz-subsurface/pull/1244) - New functionality in `VolumetricAnalysis` to compute facies fractions if `FACIES` is present in the volumetric table. Also added possibility to have labels on bar plots with user defined value format.
- [#1247](https://github.com/equinor/webviz-subsurface/pull/1247) - Added P10/P90 to the Uncertainty table in `StructuralUncertainty`.
+### Changed
+- [#1252](https://github.com/equinor/webviz-subsurface/pull/1252) - Added CO2 mass visualization to `CO2Leakage`, plus various other improvements: support for regions, option to plot containment split into zones, remove code for simple volume.
+
+### Fixed
+- [#1252](https://github.com/equinor/webviz-subsurface/pull/1252) - Fixed zoom resetting issue in `CO2Leakage`. Also some fixes to avoid crash for large UNSMRY CSV-files.
## [0.2.22] - 2023-08-31
diff --git a/webviz_subsurface/plugins/_co2_leakage/_plugin.py b/webviz_subsurface/plugins/_co2_leakage/_plugin.py
index f370dd330..5464283eb 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_plugin.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_plugin.py
@@ -1,3 +1,4 @@
+import logging
from typing import Any, Dict, List, Optional, Tuple, Union
import plotly.graph_objects as go
@@ -16,6 +17,9 @@
generate_containment_figures,
generate_unsmry_figures,
get_plume_polygon,
+ process_containment_info,
+ process_summed_mass,
+ process_visualization_info,
property_origin,
readable_name,
)
@@ -33,6 +37,8 @@
init_surface_providers,
init_table_provider,
init_well_pick_provider,
+ init_zone_and_region_options,
+ process_files,
)
from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
MainView,
@@ -43,9 +49,11 @@
from . import _error
from ._utilities.color_tables import co2leakage_color_tables
-TILE_PATH = "share/results/tables"
+LOGGER = logging.getLogger(__name__)
+TABLES_PATH = "share/results/tables"
+# pylint: disable=too-many-instance-attributes
class CO2Leakage(WebvizPluginABC):
"""
Plugin for analyzing CO2 leakage potential across multiple realizations in an FMU
@@ -55,14 +63,11 @@ class CO2Leakage(WebvizPluginABC):
* **`file_containment_boundary`:** Path to a polygon representing the containment area
* **`file_hazardous_boundary`:** Path to a polygon representing the hazardous area
* **`well_pick_file`:** Path to a file containing well picks
- * **`co2_containment_relpath`:** Path to a table of co2 containment data (amount of
+ * **`plume_mass_relpath`:** Path to a table of co2 containment data (amount of
CO2 outside/inside a boundary), for co2 mass. Relative to each realization.
- * **`co2_containment_volume_actual_relpath`:** Path to a table of co2 containment data (amount
+ * **`plume_actual_volume_relpath`:** Path to a table of co2 containment data (amount
of CO2 outside/inside a boundary), for co2 volume of type "actual". Relative to each
realization.
- * **`co2_containment_volume_actual_simple_relpath`:** Path to a table of co2 containment data
- (amount of CO2 outside/inside a boundary), for co2 volume of type "actual_simple".
- Relative to each realization.
* **`unsmry_relpath`:** Relative path to a csv version of a unified summary file
* **`fault_polygon_attribute`:** Polygons with this attribute are used as fault
polygons
@@ -91,12 +96,9 @@ def __init__(
file_containment_boundary: Optional[str] = None,
file_hazardous_boundary: Optional[str] = None,
well_pick_file: Optional[str] = None,
- co2_containment_relpath: str = TILE_PATH + "/co2_volumes.csv",
- co2_containment_volume_actual_relpath: str = TILE_PATH
- + "/plume_volume_actual.csv",
- co2_containment_volume_actual_simple_relpath: str = TILE_PATH
- + "/plume_volume_actual_simple.csv",
- unsmry_relpath: str = TILE_PATH + "/unsmry--raw.csv",
+ plume_mass_relpath: str = TABLES_PATH + "/plume_mass.csv",
+ plume_actual_volume_relpath: str = TABLES_PATH + "/plume_actual_volume.csv",
+ unsmry_relpath: Optional[str] = None,
fault_polygon_attribute: str = "dl_extracted_faultlines",
initial_surface: Optional[str] = None,
map_attribute_names: Optional[Dict[str, str]] = None,
@@ -105,16 +107,24 @@ def __init__(
):
super().__init__()
self._error_message = ""
-
- self._file_containment_boundary = file_containment_boundary
- self._file_hazardous_boundary = file_hazardous_boundary
try:
- self._ensemble_paths = {
+ ensemble_paths = {
ensemble_name: webviz_settings.shared_settings["scratch_ensembles"][
ensemble_name
]
for ensemble_name in ensembles
}
+ (
+ containment_poly_dict,
+ hazardous_poly_dict,
+ well_pick_dict,
+ ) = process_files(
+ file_containment_boundary,
+ file_hazardous_boundary,
+ well_pick_file,
+ ensemble_paths,
+ )
+ self._polygon_files = [containment_poly_dict, hazardous_poly_dict]
self._surface_server = SurfaceImageServer.instance(app)
self._polygons_server = FaultPolygonsServer.instance(app)
@@ -127,7 +137,7 @@ def __init__(
self._fault_polygon_handlers = {
ens: FaultPolygonsHandler(
self._polygons_server,
- self._ensemble_paths[ens],
+ ensemble_paths[ens],
map_surface_names_to_fault_polygons or {},
fault_polygon_attribute,
)
@@ -135,41 +145,58 @@ def __init__(
}
# CO2 containment
self._co2_table_providers = init_table_provider(
- self._ensemble_paths,
- co2_containment_relpath,
+ ensemble_paths,
+ plume_mass_relpath,
)
- self._co2_volume_actual_table_providers = init_table_provider(
- self._ensemble_paths,
- co2_containment_volume_actual_relpath,
+ self._co2_actual_volume_table_providers = init_table_provider(
+ ensemble_paths,
+ plume_actual_volume_relpath,
)
- self._co2_volume_actual_simple_table_providers = init_table_provider(
- self._ensemble_paths,
- co2_containment_volume_actual_simple_relpath,
- )
- self._unsmry_providers = init_table_provider(
- self._ensemble_paths,
- unsmry_relpath,
+ self._unsmry_providers = (
+ init_table_provider(
+ ensemble_paths,
+ unsmry_relpath,
+ )
+ if unsmry_relpath is not None
+ else None
)
# Well picks
self._well_pick_provider = init_well_pick_provider(
- well_pick_file,
+ well_pick_dict,
map_surface_names_to_well_pick_names,
)
+ # Zone and region options
+ self._zone_and_region_options = init_zone_and_region_options(
+ ensemble_paths,
+ self._co2_table_providers,
+ self._co2_actual_volume_table_providers,
+ self._ensemble_surface_providers,
+ )
except Exception as err:
self._error_message = f"Plugin initialization failed: {err}"
raise
+ self._summed_co2: Dict[str, Any] = {}
+ self._visualization_info = {
+ "threshold": -1.0,
+ "n_clicks": 0,
+ "change": False,
+ "unit": "kg",
+ }
self._color_tables = co2leakage_color_tables()
+ self._well_pick_names = {
+ ens: prov.well_names() if prov is not None else []
+ for ens, prov in self._well_pick_provider.items()
+ }
self.add_shared_settings_group(
ViewSettings(
- self._ensemble_paths,
+ ensemble_paths,
self._ensemble_surface_providers,
initial_surface,
self._map_attribute_names,
[c["name"] for c in self._color_tables], # type: ignore
- self._well_pick_provider.well_names()
- if self._well_pick_provider
- else [],
+ self._well_pick_names,
+ self._zone_and_region_options,
),
self.Ids.MAIN_SETTINGS,
)
@@ -203,7 +230,14 @@ def _ensemble_dates(self, ens: str) -> List[str]:
return dates
def _set_callbacks(self) -> None:
+ # Cannot avoid many arguments since all the parameters are needed
+ # to determine what to plot
+ # pylint: disable=too-many-arguments
+ # pylint: disable=too-many-locals
@callback(
+ Output(
+ self._settings_component(ViewSettings.Ids.CONTAINMENT_VIEW), "value"
+ ),
Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
Output(
@@ -222,6 +256,9 @@ def _set_callbacks(self) -> None:
Input(self._settings_component(ViewSettings.Ids.Y_MIN_GRAPH), "value"),
Input(self._settings_component(ViewSettings.Ids.Y_MAX_AUTO_GRAPH), "value"),
Input(self._settings_component(ViewSettings.Ids.Y_MAX_GRAPH), "value"),
+ Input(self._settings_component(ViewSettings.Ids.ZONE), "value"),
+ Input(self._settings_component(ViewSettings.Ids.REGION), "value"),
+ Input(self._settings_component(ViewSettings.Ids.CONTAINMENT_VIEW), "value"),
)
@callback_typecheck
def update_graphs(
@@ -233,65 +270,70 @@ def update_graphs(
y_min_val: Optional[float],
y_max_auto: List[str],
y_max_val: Optional[float],
- ) -> Tuple[go.Figure, go.Figure, Dict, Dict]:
- styles = [{"display": "none"}] * 3
- figs = [no_update] * 3
+ zone: Optional[str],
+ region: Optional[str],
+ containment_view: str,
+ ) -> Tuple[Dict, go.Figure, go.Figure]:
+ out = {"figs": [no_update] * 3, "styles": [{"display": "none"}] * 3}
+ cont_info = process_containment_info(
+ zone,
+ region,
+ containment_view,
+ self._zone_and_region_options[ensemble][source],
+ source,
+ )
if source in [
GraphSource.CONTAINMENT_MASS,
- GraphSource.CONTAINMENT_VOLUME_ACTUAL,
- GraphSource.CONTAINMENT_VOLUME_ACTUAL_SIMPLE,
+ GraphSource.CONTAINMENT_ACTUAL_VOLUME,
]:
- y_limits = []
- if len(y_min_auto) == 0:
- y_limits.append(y_min_val)
- else:
- y_limits.append(None)
- if len(y_max_auto) == 0:
- y_limits.append(y_max_val)
- else:
- y_limits.append(None)
- styles = [{}] * 3
-
+ y_limits = [
+ y_min_val if len(y_min_auto) == 0 else None,
+ y_max_val if len(y_max_auto) == 0 else None,
+ ]
+ out["styles"] = [{}] * 3
if (
source == GraphSource.CONTAINMENT_MASS
and ensemble in self._co2_table_providers
):
- figs[: len(figs)] = generate_containment_figures(
+ out["figs"][: len(out["figs"])] = generate_containment_figures(
self._co2_table_providers[ensemble],
co2_scale,
realizations[0],
y_limits,
+ cont_info,
)
elif (
- source == GraphSource.CONTAINMENT_VOLUME_ACTUAL
- and ensemble in self._co2_volume_actual_table_providers
+ source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
+ and ensemble in self._co2_actual_volume_table_providers
):
- figs[: len(figs)] = generate_containment_figures(
- self._co2_volume_actual_table_providers[ensemble],
+ out["figs"][: len(out["figs"])] = generate_containment_figures(
+ self._co2_actual_volume_table_providers[ensemble],
co2_scale,
realizations[0],
y_limits,
+ cont_info,
)
- elif (
- source == GraphSource.CONTAINMENT_VOLUME_ACTUAL_SIMPLE
- and ensemble in self._co2_volume_actual_simple_table_providers
- ):
- figs[: len(figs)] = generate_containment_figures(
- self._co2_volume_actual_simple_table_providers[ensemble],
- co2_scale,
- realizations[0],
- y_limits,
+ for fig in out["figs"]:
+ fig["layout"][
+ "uirevision"
+ ] = f"{source}-{co2_scale}-{cont_info['zone']}-{cont_info['region']}"
+ out["figs"][-1]["layout"]["uirevision"] += f"-{realizations}"
+ elif source == GraphSource.UNSMRY:
+ if self._unsmry_providers is not None:
+ if ensemble in self._unsmry_providers:
+ u_figs = generate_unsmry_figures(
+ self._unsmry_providers[ensemble],
+ co2_scale,
+ self._co2_table_providers[ensemble],
+ )
+ out["figs"][: len(u_figs)] = u_figs
+ out["styles"][: len(u_figs)] = [{}] * len(u_figs)
+ else:
+ LOGGER.warning(
+ """UNSMRY file has not been specified as input.
+ Please use unsmry_relpath in the configuration."""
)
- elif source == GraphSource.UNSMRY and ensemble in self._unsmry_providers:
- u_figs = generate_unsmry_figures(
- self._unsmry_providers[ensemble],
- co2_scale,
- self._co2_table_providers[ensemble],
- )
- figs[: len(u_figs)] = u_figs
- styles[: len(u_figs)] = [{}] * len(u_figs)
-
- return *figs, *styles # type: ignore
+ return cont_info["containment_view"], *out["figs"], *out["styles"] # type: ignore
@callback(
Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
@@ -333,14 +375,34 @@ def make_unit_list(
Tuple[List[Co2MassScale], Co2MassScale],
Tuple[List[Co2VolumeScale], Co2VolumeScale],
]:
- if attribute in [
- GraphSource.CONTAINMENT_VOLUME_ACTUAL,
- GraphSource.CONTAINMENT_VOLUME_ACTUAL_SIMPLE,
- ]:
+ if attribute == GraphSource.CONTAINMENT_ACTUAL_VOLUME:
return list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS
-
return list(Co2MassScale), Co2MassScale.MTONS
+ @callback(
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "options"),
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "style"),
+ Output(ViewSettings.Ids.WELL_FILTER_HEADER, "style"),
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
+ )
+ def set_well_options(
+ ensemble: str,
+ ) -> Tuple[List[Any], List[str], Dict[Any, Any], Dict[Any, Any]]:
+ return (
+ [{"label": i, "value": i} for i in self._well_pick_names[ensemble]],
+ self._well_pick_names[ensemble],
+ {
+ "display": "block" if self._well_pick_names[ensemble] else "none",
+ "height": f"{len(self._well_pick_names[ensemble]) * 22}px",
+ },
+ {
+ "flex": 3,
+ "minWidth": "20px",
+ "display": "block" if self._well_pick_names[ensemble] else "none",
+ },
+ )
+
# Cannot avoid many arguments and/or locals since all layers of the DeckGL map
# needs to be updated simultaneously
# pylint: disable=too-many-arguments,too-many-locals
@@ -360,9 +422,19 @@ def make_unit_list(
Input(self._settings_component(ViewSettings.Ids.CM_MAX), "value"),
Input(self._settings_component(ViewSettings.Ids.PLUME_THRESHOLD), "value"),
Input(self._settings_component(ViewSettings.Ids.PLUME_SMOOTHING), "value"),
+ Input(
+ self._settings_component(ViewSettings.Ids.VISUALIZATION_THRESHOLD),
+ "value",
+ ),
+ Input(
+ self._settings_component(ViewSettings.Ids.VISUALIZATION_UPDATE),
+ "n_clicks",
+ ),
+ Input(self._settings_component(ViewSettings.Ids.MASS_UNIT), "value"),
Input(ViewSettings.Ids.OPTIONS_DIALOG_OPTIONS, "value"),
Input(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
- State(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
+ State(self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"),
)
def update_map_attribute(
attribute: MapAttribute,
@@ -377,14 +449,27 @@ def update_map_attribute(
cm_max_val: Optional[float],
plume_threshold: Optional[float],
plume_smoothing: Optional[float],
+ visualization_threshold: Optional[float],
+ visualization_update: int,
+ mass_unit: str,
options_dialog_options: List[int],
selected_wells: List[str],
ensemble: str,
+ current_views: List[Any],
) -> Tuple[List[Dict[Any, Any]], List[Any], Dict[Any, Any]]:
+ # Unable to clear cache (when needed) without the protected member
+ # pylint: disable=protected-access
+ self._visualization_info = process_visualization_info(
+ visualization_update,
+ visualization_threshold,
+ mass_unit,
+ self._visualization_info,
+ self._surface_server._image_cache,
+ )
+ if self._visualization_info["change"]:
+ return [], no_update, no_update
attribute = MapAttribute(attribute)
- if len(realization) == 0:
- raise PreventUpdate
- if ensemble is None:
+ if len(realization) == 0 or ensemble is None:
raise PreventUpdate
datestr = self._ensemble_dates(ensemble)[date]
# Contour data
@@ -396,9 +481,9 @@ def update_map_attribute(
"smoothing": plume_smoothing,
}
# Surface
- surf_data = None
+ surf_data, summed_mass = None, None
if formation is not None and len(realization) > 0:
- surf_data = SurfaceData.from_server(
+ surf_data, summed_mass = SurfaceData.from_server(
server=self._surface_server,
provider=self._ensemble_surface_providers[ensemble],
address=derive_surface_address(
@@ -416,7 +501,20 @@ def update_map_attribute(
),
color_map_name=color_map_name,
readable_name_=readable_name(attribute),
+ visualization_info=self._visualization_info,
+ map_attribute_names=self._map_attribute_names,
)
+ assert isinstance(self._visualization_info["unit"], str)
+ surf_data, self._summed_co2 = process_summed_mass(
+ formation,
+ realization,
+ datestr,
+ attribute,
+ summed_mass,
+ surf_data,
+ self._summed_co2,
+ self._visualization_info["unit"],
+ )
# Plume polygon
plume_polygon = None
if contour_data is not None:
@@ -437,9 +535,9 @@ def update_map_attribute(
realization,
)
),
- file_containment_boundary=self._file_containment_boundary,
- file_hazardous_boundary=self._file_hazardous_boundary,
- well_pick_provider=self._well_pick_provider,
+ file_containment_boundary=self._polygon_files[0][ensemble],
+ file_hazardous_boundary=self._polygon_files[1][ensemble],
+ well_pick_provider=self._well_pick_provider[ensemble],
plume_extent_data=plume_polygon,
options_dialog_options=options_dialog_options,
selected_wells=selected_wells,
@@ -448,9 +546,11 @@ def update_map_attribute(
formation=formation,
surface_data=surf_data,
colortables=self._color_tables,
+ attribute=attribute,
+ unit=self._visualization_info["unit"],
)
- viewports = create_map_viewports()
- return (layers, annotations, viewports)
+ viewports = no_update if current_views else create_map_viewports()
+ return layers, annotations, viewports
@callback(
Output(ViewSettings.Ids.OPTIONS_DIALOG, "open"),
@@ -460,3 +560,12 @@ def open_close_options_dialog(_n_clicks: Optional[int]) -> bool:
if _n_clicks is not None:
return _n_clicks > 0
raise PreventUpdate
+
+ @callback(
+ Output(ViewSettings.Ids.FEEDBACK, "open"),
+ Input(ViewSettings.Ids.FEEDBACK_BUTTON, "n_clicks"),
+ )
+ def open_close_feedback(_n_clicks: Optional[int]) -> bool:
+ if _n_clicks is not None:
+ return _n_clicks > 0
+ raise PreventUpdate
diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py
index ac669202f..944f5e223 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py
@@ -6,6 +6,7 @@
import numpy as np
import plotly.graph_objects as go
import webviz_subsurface_components as wsc
+from flask_caching import Cache
from webviz_subsurface._providers import (
EnsembleSurfaceProvider,
@@ -29,6 +30,8 @@
from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
Co2MassScale,
Co2VolumeScale,
+ ContainmentViews,
+ GraphSource,
LayoutLabels,
MapAttribute,
)
@@ -73,8 +76,16 @@ def from_server(
color_map_range: Tuple[Optional[float], Optional[float]],
color_map_name: str,
readable_name_: str,
- ) -> "SurfaceData":
- surf_meta, img_url = publish_and_get_surface_metadata(server, provider, address)
+ visualization_info: Dict[str, Any],
+ map_attribute_names: Dict[MapAttribute, str],
+ ) -> Tuple[Any, Optional[Any]]:
+ surf_meta, img_url, summed_mass = publish_and_get_surface_metadata(
+ server,
+ provider,
+ address,
+ visualization_info,
+ map_attribute_names,
+ )
assert surf_meta is not None # Should not occur
value_range = (
0.0 if np.ma.is_masked(surf_meta.val_min) else surf_meta.val_min,
@@ -84,13 +95,16 @@ def from_server(
value_range[0] if color_map_range[0] is None else color_map_range[0],
value_range[1] if color_map_range[1] is None else color_map_range[1],
)
- return SurfaceData(
- readable_name_,
- color_map_range,
- color_map_name,
- value_range,
- surf_meta,
- img_url,
+ return (
+ SurfaceData(
+ readable_name_,
+ color_map_range,
+ color_map_name,
+ value_range,
+ surf_meta,
+ img_url,
+ ),
+ summed_mass,
)
@@ -183,10 +197,20 @@ def get_plume_polygon(
)
+def _find_legend_title(attribute: MapAttribute, unit: str) -> str:
+ if attribute == MapAttribute.MIGRATION_TIME:
+ return "years"
+ if attribute in [MapAttribute.MASS, MapAttribute.DISSOLVED, MapAttribute.FREE]:
+ return unit
+ return ""
+
+
def create_map_annotations(
formation: str,
surface_data: Optional[SurfaceData],
colortables: List[Dict[str, Any]],
+ attribute: MapAttribute,
+ unit: str,
) -> List[wsc.ViewAnnotation]:
annotations = []
if surface_data is not None:
@@ -195,13 +219,14 @@ def create_map_annotations(
id="1_view",
children=[
wsc.WebVizColorLegend(
+ title=_find_legend_title(attribute, unit),
min=surface_data.color_map_range[0],
max=surface_data.color_map_range[1],
colorName=surface_data.color_map_name,
cssLegendStyles={"top": "0", "right": "0"},
openColorSelector=False,
legendScaleSize=0.1,
- legendFontSize=30,
+ legendFontSize=20,
colorTables=colortables,
),
wsc.ViewFooter(children=formation),
@@ -305,11 +330,23 @@ def create_map_layers(
)
if (
well_pick_provider is not None
+ and formation is not None
and LayoutLabels.SHOW_WELLS in options_dialog_options
):
well_data = dict(well_pick_provider.get_geojson(selected_wells, formation))
- if "features" in well_data and len(well_data["features"]) == 0:
- warnings.warn(f'Formation name "{formation}" not found in well picks file.')
+ if "features" in well_data:
+ if len(well_data["features"]) == 0:
+ wellstring = "well: " if len(selected_wells) == 1 else "wells: "
+ wellstring += ", ".join(selected_wells)
+ warnings.warn(
+ f"Combination of formation: {formation} and "
+ f"{wellstring} not found in well picks file."
+ )
+ for i in range(len(well_data["features"])):
+ current_attribute = well_data["features"][i]["properties"]["attribute"]
+ well_data["features"][i]["properties"]["attribute"] = (
+ " " + current_attribute
+ )
layers.append(
{
"@@type": "GeoJsonLayer",
@@ -346,20 +383,23 @@ def generate_containment_figures(
co2_scale: Union[Co2MassScale, Co2VolumeScale],
realization: int,
y_limits: List[Optional[float]],
+ containment_info: Dict[str, Union[str, None, List[str]]],
) -> Tuple[go.Figure, go.Figure, go.Figure]:
try:
fig0 = generate_co2_volume_figure(
table_provider,
table_provider.realizations(),
co2_scale,
+ containment_info,
)
fig1 = generate_co2_time_containment_figure(
table_provider,
table_provider.realizations(),
co2_scale,
+ containment_info,
)
fig2 = generate_co2_time_containment_one_realization_figure(
- table_provider, co2_scale, realization, y_limits
+ table_provider, co2_scale, realization, y_limits, containment_info
)
except KeyError as exc:
warnings.warn(f"Could not generate CO2 figures: {exc}")
@@ -415,3 +455,88 @@ def _parse_polygon_file(filename: str) -> Dict[str, Any]:
],
}
return as_geojson
+
+
+def process_visualization_info(
+ n_clicks: int,
+ threshold: Optional[float],
+ unit: str,
+ stored_info: Dict[str, Any],
+ cache: Cache,
+) -> Dict[str, Any]:
+ """
+ Clear surface cache if the threshold for visualization or mass unit is changed
+ """
+ stored_info["change"] = False
+ stored_info["n_clicks"] = n_clicks
+ if unit != stored_info["unit"]:
+ stored_info["unit"] = unit
+ stored_info["change"] = True
+ if threshold is not None and threshold != stored_info["threshold"]:
+ stored_info["threshold"] = threshold
+ stored_info["change"] = True
+ if stored_info["change"]:
+ cache.clear()
+ # stored_info["n_clicks"] = n_clicks
+ return stored_info
+
+
+def process_containment_info(
+ zone: Optional[str],
+ region: Optional[str],
+ view: Optional[str],
+ zone_and_region_options: Dict[str, List[str]],
+ source: str,
+) -> Dict[str, Union[str, None, List[str]]]:
+ zones = zone_and_region_options["zones"]
+ regions = zone_and_region_options["regions"]
+ if source in [
+ GraphSource.CONTAINMENT_MASS,
+ GraphSource.CONTAINMENT_ACTUAL_VOLUME,
+ ]:
+ if view == ContainmentViews.CONTAINMENTSPLIT:
+ return {"zone": zone, "region": region, "containment_view": view}
+ if view == ContainmentViews.ZONESPLIT and len(zones) > 0:
+ zones = [zone_name for zone_name in zones if zone_name != "all"]
+ elif view == ContainmentViews.REGIONSPLIT and len(regions) > 0:
+ regions = [reg_name for reg_name in regions if reg_name != "all"]
+ else:
+ return {
+ "zone": zone,
+ "region": region,
+ "containment_view": ContainmentViews.CONTAINMENTSPLIT,
+ }
+ return {
+ "zone": zone,
+ "region": region,
+ "containment_view": view,
+ "zones": zones,
+ "regions": regions,
+ }
+ return {"containment_view": ContainmentViews.CONTAINMENTSPLIT}
+
+
+def process_summed_mass(
+ formation: str,
+ realization: List[int],
+ datestr: str,
+ attribute: MapAttribute,
+ summed_mass: Optional[float],
+ surf_data: Optional[SurfaceData],
+ summed_co2: Dict[str, float],
+ unit: str,
+) -> Tuple[Optional[SurfaceData], Dict[str, float]]:
+ summed_co2_key = f"{formation}-{realization[0]}-{datestr}-{attribute}-{unit}"
+ if len(realization) == 1:
+ if attribute in [
+ MapAttribute.MASS,
+ MapAttribute.DISSOLVED,
+ MapAttribute.FREE,
+ ]:
+ if summed_mass is not None and summed_co2_key not in summed_co2:
+ summed_co2[summed_co2_key] = summed_mass
+ if summed_co2_key in summed_co2 and surf_data is not None:
+ surf_data.readable_name += (
+ f" ({unit}) (Total: {summed_co2[summed_co2_key]:.2E}): "
+ )
+ return surf_data, summed_co2
diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py
index dd7ac4e85..29ce1fb3f 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np
import pandas
@@ -10,6 +10,7 @@
from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
Co2MassScale,
Co2VolumeScale,
+ ContainmentViews,
)
@@ -24,22 +25,130 @@ class _Columns(Enum):
_COLOR_CONTAINED = "#00aa00"
_COLOR_OUTSIDE = "#006ddd"
_COLOR_HAZARDOUS = "#dd4300"
+_COLOR_ZONES = [
+ "#e91451",
+ "#daa218",
+ "#208eb7",
+ "#84bc04",
+ "#b74532",
+ "#9a89b4",
+ "#8d30ba",
+ "#256b33",
+ "#95704d",
+ "#1357ca",
+ "#f75ef0",
+ "#34b36f",
+]
def _read_dataframe(
table_provider: EnsembleTableProvider,
realization: int,
scale_factor: float,
+ containment_info: Dict[str, Union[str, None, List[str]]],
) -> pandas.DataFrame:
df = table_provider.get_column_data(table_provider.column_names(), [realization])
+ if any(split in list(df.columns) for split in ["zone", "region"]):
+ df = _process_containment_information(df, containment_info)
+ if containment_info["containment_view"] != ContainmentViews.CONTAINMENTSPLIT:
+ df["aqueous"] = (
+ df["aqueous_contained"]
+ + df["aqueous_outside"]
+ + df["aqueous_hazardous"]
+ )
+ df["gas"] = df["gas_contained"] + df["gas_outside"] + df["gas_hazardous"]
+ df = df.drop(
+ columns=[
+ "aqueous_contained",
+ "aqueous_outside",
+ "aqueous_hazardous",
+ "gas_contained",
+ "gas_outside",
+ "gas_hazardous",
+ ]
+ )
if scale_factor == 1.0:
return df
for col in df.columns:
- if col != "date":
+ if col not in ["date", "zone", "region"]:
df[col] /= scale_factor
return df
+def read_zone_and_region_options(
+ table_provider: EnsembleTableProvider,
+ realization: int,
+) -> Dict[str, List[str]]:
+ df = table_provider.get_column_data(table_provider.column_names(), [realization])
+ zones = ["all"]
+ if "zone" in list(df.columns):
+ for zone in list(df["zone"]):
+ if zone not in zones:
+ zones.append(zone)
+ regions = ["all"]
+ if "region" in list(df.columns):
+ for region in list(df["region"]):
+ if region not in regions:
+ regions.append(region)
+ return {
+ "zones": zones if len(zones) > 1 else [],
+ "regions": regions if len(regions) > 1 else [],
+ }
+
+
+def _process_containment_information(
+ df: pandas.DataFrame,
+ containment_info: Dict[str, Union[str, None, List[str]]],
+) -> pandas.DataFrame:
+ view = containment_info["containment_view"]
+ if view == ContainmentViews.ZONESPLIT:
+ return (
+ df[df["zone"] != "all"]
+ .drop(columns="region", errors="ignore")
+ .reset_index(drop=True)
+ )
+ if view == ContainmentViews.REGIONSPLIT:
+ return (
+ df[df["region"] != "all"]
+ .drop(columns="zone", errors="ignore")
+ .reset_index(drop=True)
+ )
+ zone = containment_info["zone"]
+ region = containment_info["region"]
+ if zone not in ["all", None]:
+ if zone in list(df["zone"]):
+ return df[df["zone"] == zone].drop(
+ columns=["zone", "region"], errors="ignore"
+ )
+ print(f"Zone {zone} not found, using sum for each unique date.")
+ elif region not in ["all", None]:
+ if region in list(df["region"]):
+ return df[df["region"] == region].drop(
+ columns=["zone", "region"], errors="ignore"
+ )
+ print(f"Region {region} not found, using sum for each unique date.")
+ if "zone" in list(df.columns):
+ if "region" in list(df.columns):
+ return df[
+ [a and b for a, b in zip(df["zone"] == "all", df["region"] == "all")]
+ ].drop(columns=["zone", "region"])
+ df = df[df["zone"] == "all"].drop(columns=["zone"])
+ elif "region" in list(df.columns):
+ df = df[df["region"] == "all"].drop(columns=["region"])
+ return df
+
+
+def _split_colors(num_cols: int, split: str = "zone") -> List[str]:
+ options = list(_COLOR_ZONES)
+ if split == "region":
+ options.reverse()
+ if len(options) >= num_cols:
+ return options[:num_cols]
+ num_lengths = int(np.ceil(num_cols / len(options)))
+ new_cols = options * num_lengths
+ return new_cols[:num_cols]
+
+
def _find_scale_factor(
table_provider: EnsembleTableProvider,
scale: Union[Co2MassScale, Co2VolumeScale],
@@ -58,40 +167,65 @@ def _read_terminal_co2_volumes(
table_provider: EnsembleTableProvider,
realizations: List[int],
scale: Union[Co2MassScale, Co2VolumeScale],
+ containment_info: Dict[str, Union[str, None, List[str]]],
) -> pandas.DataFrame:
+ view = containment_info["containment_view"]
records: Dict[str, List[Any]] = {
"real": [],
"amount": [],
- "containment": [],
"phase": [],
"sort_key": [],
"sort_key_secondary": [],
}
+ if view == ContainmentViews.ZONESPLIT:
+ records["zone"] = []
+ elif view == ContainmentViews.REGIONSPLIT:
+ records["region"] = []
+ else:
+ records["containment"] = []
scale_factor = _find_scale_factor(table_provider, scale)
for real in realizations:
- df = _read_dataframe(table_provider, real, scale_factor)
- last = df.iloc[np.argmax(df["date"])]
- label = str(real)
- records["real"] += [label] * 6
- records["amount"] += [
- last["aqueous_contained"],
- last["gas_contained"],
- last["aqueous_outside"],
- last["gas_outside"],
- last["aqueous_hazardous"],
- last["gas_hazardous"],
- ]
- records["containment"] += [
- "contained",
- "contained",
- "outside",
- "outside",
- "hazardous",
- "hazardous",
- ]
- records["phase"] += ["aqueous", "gas", "aqueous", "gas", "aqueous", "gas"]
- records["sort_key"] += [last["gas_hazardous"]] * 6
- records["sort_key_secondary"] += [last["gas_outside"]] * 6
+ df = _read_dataframe(table_provider, real, scale_factor, containment_info)
+ if view != ContainmentViews.CONTAINMENTSPLIT:
+ split = "zone" if view == ContainmentViews.ZONESPLIT else "region"
+ last_ = df[df["date"] == np.max(df["date"])]
+ for i in range(last_.shape[0]):
+ last = last_.iloc[i]
+ label = str(real)
+
+ records["real"] += [label] * 2
+ records["amount"] += [
+ last["aqueous"],
+ last["gas"],
+ ]
+ records["phase"] += ["aqueous", "gas"]
+ records[split] += [last[split]] * 2
+ records["sort_key"] += [label] * 2
+ records["sort_key_secondary"] += [last[split]] * 2
+ else:
+ last = df.iloc[np.argmax(df["date"])]
+ label = str(real)
+
+ records["real"] += [label] * 6
+ records["amount"] += [
+ last["aqueous_contained"],
+ last["gas_contained"],
+ last["aqueous_outside"],
+ last["gas_outside"],
+ last["aqueous_hazardous"],
+ last["gas_hazardous"],
+ ]
+ records["containment"] += [
+ "contained",
+ "contained",
+ "outside",
+ "outside",
+ "hazardous",
+ "hazardous",
+ ]
+ records["phase"] += ["aqueous", "gas", "aqueous", "gas", "aqueous", "gas"]
+ records["sort_key"] += [last["gas_hazardous"]] * 6
+ records["sort_key_secondary"] += [last["gas_outside"]] * 6
df = pandas.DataFrame.from_dict(records)
df.sort_values(
["sort_key", "sort_key_secondary"], inplace=True, ascending=[True, True]
@@ -103,11 +237,14 @@ def _read_co2_volumes(
table_provider: EnsembleTableProvider,
realizations: List[int],
scale: Union[Co2MassScale, Co2VolumeScale],
+ containment_info: Dict[str, Union[str, None, List[str]]],
) -> pandas.DataFrame:
scale_factor = _find_scale_factor(table_provider, scale)
return pandas.concat(
[
- _read_dataframe(table_provider, real, scale_factor).assign(realization=real)
+ _read_dataframe(
+ table_provider, real, scale_factor, containment_info
+ ).assign(realization=real)
for real in realizations
]
)
@@ -141,21 +278,36 @@ def generate_co2_volume_figure(
table_provider: EnsembleTableProvider,
realizations: List[int],
scale: Union[Co2MassScale, Co2VolumeScale],
+ containment_info: Dict[str, Any],
) -> go.Figure:
- df = _read_terminal_co2_volumes(table_provider, realizations, scale)
+ df = _read_terminal_co2_volumes(
+ table_provider, realizations, scale, containment_info
+ )
+ if containment_info["containment_view"] == ContainmentViews.ZONESPLIT:
+ color = "zone"
+ cat_ord = {"zone": containment_info["zones"], "phase": ["gas", "aqueous"]}
+ colors = _split_colors(len(containment_info["zones"]))
+ elif containment_info["containment_view"] == ContainmentViews.REGIONSPLIT:
+ color = "region"
+ cat_ord = {"region": containment_info["regions"], "phase": ["gas", "aqueous"]}
+ colors = _split_colors(len(containment_info["regions"]), "region")
+ else:
+ color = "containment"
+ cat_ord = {
+ "containment": ["hazardous", "outside", "contained"],
+ "phase": ["gas", "aqueous"],
+ }
+ colors = [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED]
fig = px.bar(
df,
y="real",
x="amount",
- color="containment",
+ color=color,
pattern_shape="phase",
title="End-state CO2 containment (all realizations)",
orientation="h",
- category_orders={
- "containment": ["hazardous", "outside", "contained"],
- "phase": ["gas", "aqueous"],
- },
- color_discrete_sequence=[_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED],
+ category_orders=cat_ord,
+ color_discrete_sequence=colors,
)
fig.layout.legend.title.text = ""
fig.layout.legend.orientation = "h"
@@ -173,8 +325,11 @@ def generate_co2_time_containment_one_realization_figure(
scale: Union[Co2MassScale, Co2VolumeScale],
time_series_realization: int,
y_limits: List[Optional[float]],
+ containment_info: Dict[str, Any],
) -> go.Figure:
- df = _read_co2_volumes(table_provider, [time_series_realization], scale)
+ df = _read_co2_volumes(
+ table_provider, [time_series_realization], scale, containment_info
+ )
df.sort_values(by="date", inplace=True)
df = df.drop(
columns=[
@@ -188,20 +343,43 @@ def generate_co2_time_containment_one_realization_figure(
"total_aqueous",
]
)
- df = pandas.melt(df, id_vars=["date"])
- df = df.rename(columns={"value": "mass", "variable": "type"})
- df.sort_values(by="date", inplace=True)
- _change_type_names(df)
- if y_limits[0] is None and y_limits[1] is not None:
- y_limits[0] = 0.0
- elif y_limits[1] is None and y_limits[0] is not None:
- y_limits[1] = max(df.groupby("date")["mass"].sum()) * 1.05
- fig = px.area(
- df,
- x="date",
- y="mass",
- color="type",
- category_orders={
+ if containment_info["containment_view"] == ContainmentViews.ZONESPLIT:
+ df = pandas.melt(df, id_vars=["date", "zone"])
+ df["variable"] = df["zone"] + ", " + df["variable"]
+ df = df.drop(columns=["zone"])
+ cat_ord = {
+ "type": [
+ zone_name + ", " + phase
+ for zone_name in containment_info["zones"]
+ for phase in ["gas", "aqueous"]
+ ]
+ }
+ pattern = ["", "/"] * len(containment_info["zones"])
+ colors = [
+ col
+ for col in _split_colors(len(containment_info["zones"]))
+ for i in range(2)
+ ]
+ elif containment_info["containment_view"] == ContainmentViews.REGIONSPLIT:
+ df = pandas.melt(df, id_vars=["date", "region"])
+ df["variable"] = df["region"] + ", " + df["variable"]
+ df = df.drop(columns=["region"])
+ cat_ord = {
+ "type": [
+ region_name + ", " + phase
+ for region_name in containment_info["regions"]
+ for phase in ["gas", "aqueous"]
+ ]
+ }
+ pattern = ["", "/"] * len(containment_info["regions"])
+ colors = [
+ col
+ for col in _split_colors(len(containment_info["regions"]), "region")
+ for i in range(2)
+ ]
+ else:
+ df = pandas.melt(df, id_vars=["date"])
+ cat_ord = {
"type": [
"Hazardous mobile gas",
"Hazardous aqueous",
@@ -210,24 +388,32 @@ def generate_co2_time_containment_one_realization_figure(
"Contained mobile gas",
"Contained aqueous",
]
- },
- color_discrete_sequence=[
+ }
+ pattern = ["", "/"] * 3
+ colors = [
_COLOR_HAZARDOUS,
_COLOR_HAZARDOUS,
_COLOR_OUTSIDE,
_COLOR_OUTSIDE,
_COLOR_CONTAINED,
_COLOR_CONTAINED,
- ],
+ ]
+ df = df.rename(columns={"value": "mass", "variable": "type"})
+ df.sort_values(by="date", inplace=True)
+ _change_type_names(df)
+ if y_limits[0] is None and y_limits[1] is not None:
+ y_limits[0] = 0.0
+ elif y_limits[1] is None and y_limits[0] is not None:
+ y_limits[1] = max(df.groupby("date")["mass"].sum()) * 1.05
+ fig = px.area(
+ df,
+ x="date",
+ y="mass",
+ color="type",
+ category_orders=cat_ord,
+ color_discrete_sequence=colors,
pattern_shape="type",
- pattern_shape_sequence=[
- "",
- "/",
- "",
- "/",
- "",
- "/",
- ], # ['', '/', '\\', 'x', '-', '|', '+', '.'],
+ pattern_shape_sequence=pattern, # ['', '/', '\\', 'x', '-', '|', '+', '.'],
range_y=y_limits,
)
fig.layout.yaxis.range = y_limits
@@ -240,35 +426,95 @@ def generate_co2_time_containment_one_realization_figure(
)
fig.layout.xaxis.title = "Time"
fig.layout.yaxis.title = scale.value
+ fig.layout.yaxis.exponentformat = "power"
_adjust_figure(fig)
return fig
+def _prepare_time_figure_options(
+ df: pandas.DataFrame,
+ containment_info: Dict[str, Any],
+) -> Tuple[pandas.DataFrame, Dict[str, Tuple[str, str, str]], List[str]]:
+ view = containment_info["containment_view"]
+ if view != ContainmentViews.CONTAINMENTSPLIT:
+ split = "zone" if view == ContainmentViews.ZONESPLIT else "region"
+ options = (
+ containment_info["zones"]
+ if split == "zone"
+ else containment_info["regions"]
+ )
+ df = df.drop(
+ columns=[
+ "REAL",
+ "total_gas",
+ "total_aqueous",
+ "total_contained",
+ "total_outside",
+ "total_hazardous",
+ "region" if split == "zone" else "zone",
+ ],
+ errors="ignore",
+ )
+ df.sort_values(by=["date", "realization"], inplace=True)
+ df_ = df[["date", "realization"]][df[split] == options[0]].reset_index(
+ drop=True
+ )
+ for name in options:
+ part_df = df[["total", "gas", "aqueous"]][df[split] == name]
+ part_df = part_df.rename(
+ columns={
+ "total": name + ", total",
+ "gas": name + ", gas",
+ "aqueous": name + ", aqueous",
+ }
+ ).reset_index(drop=True)
+ df_ = pandas.concat([df_, part_df], axis=1)
+ colors = _split_colors(len(options), split)
+ cols_to_plot = {}
+ for phase, line_type in zip(
+ ["total", "gas", "aqueous"], ["solid", "dot", "dash"]
+ ):
+ for name, col in zip(options, colors):
+ cols_to_plot[name + ", " + phase] = (
+ name + ", " + phase,
+ line_type,
+ col,
+ )
+ active_cols_at_startup = [name + ", total" for name in options]
+ df = df_
+ else:
+ df.sort_values(by="date", inplace=True)
+ cols_to_plot = {
+ "Total": ("total", "solid", _COLOR_TOTAL),
+ "Contained": ("total_contained", "solid", _COLOR_CONTAINED),
+ "Outside": ("total_outside", "solid", _COLOR_OUTSIDE),
+ "Hazardous": ("total_hazardous", "solid", _COLOR_HAZARDOUS),
+ "Gas": ("total_gas", "dot", _COLOR_TOTAL),
+ "Aqueous": ("total_aqueous", "dash", _COLOR_TOTAL),
+ "Contained mobile gas": ("gas_contained", "dot", _COLOR_CONTAINED),
+ "Outside mobile gas": ("gas_outside", "dot", _COLOR_OUTSIDE),
+ "Hazardous mobile gas": ("gas_hazardous", "dot", _COLOR_HAZARDOUS),
+ "Contained aqueous": ("aqueous_contained", "dash", _COLOR_CONTAINED),
+ "Outside aqueous": ("aqueous_outside", "dash", _COLOR_OUTSIDE),
+ "Hazardous aqueous": ("aqueous_hazardous", "dash", _COLOR_HAZARDOUS),
+ }
+ active_cols_at_startup = ["Total", "Outside", "Hazardous"]
+ return df, cols_to_plot, active_cols_at_startup
+
+
def generate_co2_time_containment_figure(
table_provider: EnsembleTableProvider,
realizations: List[int],
scale: Union[Co2MassScale, Co2VolumeScale],
+ containment_info: Dict[str, Any],
) -> go.Figure:
- df = _read_co2_volumes(table_provider, realizations, scale)
- df.sort_values(by="date", inplace=True)
+ df = _read_co2_volumes(table_provider, realizations, scale, containment_info)
+ df, cols_to_plot, active_cols_at_startup = _prepare_time_figure_options(
+ df, containment_info
+ )
fig = go.Figure()
- cols_to_plot = {
- "Total": ("total", "solid", _COLOR_TOTAL),
- "Contained": ("total_contained", "solid", _COLOR_CONTAINED),
- "Outside": ("total_outside", "solid", _COLOR_OUTSIDE),
- "Hazardous": ("total_hazardous", "solid", _COLOR_HAZARDOUS),
- "Gas": ("total_gas", "dot", _COLOR_TOTAL),
- "Aqueous": ("total_aqueous", "dash", _COLOR_TOTAL),
- "Contained mobile gas": ("gas_contained", "dot", _COLOR_CONTAINED),
- "Outside mobile gas": ("gas_outside", "dot", _COLOR_OUTSIDE),
- "Hazardous mobile gas": ("gas_hazardous", "dot", _COLOR_HAZARDOUS),
- "Contained aqueous": ("aqueous_contained", "dash", _COLOR_CONTAINED),
- "Outside aqueous": ("aqueous_outside", "dash", _COLOR_OUTSIDE),
- "Hazardous aqueous": ("aqueous_hazardous", "dash", _COLOR_HAZARDOUS),
- }
- active_cols_at_startup = ["Total", "Outside", "Hazardous"]
# Generate dummy scatters for legend entries
- dummy_args = {"mode": "lines", "hoverinfo": "none"}
+ dummy_args = {"x": df["date"], "mode": "lines", "hoverinfo": "none"}
for col, value in cols_to_plot.items():
args = {
"line_dash": value[1],
@@ -305,8 +551,8 @@ def generate_co2_time_containment_figure(
fig.layout.title = "CO2 containment (all realizations)"
fig.layout.xaxis.title = "Time"
fig.layout.yaxis.title = scale.value
- fig.layout.yaxis.exponentformat = "none"
- fig.layout.yaxis.range = (0, 1.05 * df["total"].max())
+ fig.layout.yaxis.exponentformat = "power"
+ fig.layout.yaxis.autorange = True
_adjust_figure(fig)
# fig.update_layout(legend=dict(font=dict(size=8)), legend_tracegroupgap=0)
return fig
diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py
index aec549150..c66f5d5e6 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py
@@ -9,6 +9,9 @@ class MapAttribute(Enum):
MAX_AMFG = "Maximum AMFG"
SGAS_PLUME = "Plume (SGAS)"
AMFG_PLUME = "Plume (AMFG)"
+ MASS = "Mass"
+ DISSOLVED = "Dissolved mass"
+ FREE = "Free mass"
class Co2MassScale(StrEnum):
@@ -26,8 +29,7 @@ class Co2VolumeScale(StrEnum):
class GraphSource(StrEnum):
UNSMRY = "UNSMRY"
CONTAINMENT_MASS = "Containment Data (mass)"
- CONTAINMENT_VOLUME_ACTUAL = "Containment Data (volume, actual)"
- CONTAINMENT_VOLUME_ACTUAL_SIMPLE = "Containment Data (volume, actual_simple)"
+ CONTAINMENT_ACTUAL_VOLUME = "Containment Data (volume, actual)"
class LayoutLabels(str, Enum):
@@ -39,6 +41,8 @@ class LayoutLabels(str, Enum):
SHOW_WELLS = "Show wells"
WELL_FILTER = "Well filter"
COMMON_SELECTIONS = "Options and global filters"
+ FEEDBACK = "User feedback"
+ VISUALIZATION_UPDATE = "Update threshold"
# pylint: disable=too-few-public-methods
@@ -52,3 +56,24 @@ class LayoutStyle:
"line-height": "30px",
"background-color": "lightgrey",
}
+
+ FEEDBACK_BUTTON = {
+ "marginBottom": "10px",
+ "width": "100%",
+ "height": "30px",
+ "line-height": "30px",
+ "background-color": "lightgrey",
+ }
+
+ VISUALIZATION_BUTTON = {
+ "marginLeft": "10px",
+ "height": "30px",
+ "line-height": "30px",
+ "background-color": "lightgrey",
+ }
+
+
+class ContainmentViews(StrEnum):
+ CONTAINMENTSPLIT = "Split into containment polygons"
+ ZONESPLIT = "Split into zones"
+ REGIONSPLIT = "Split into regions"
diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py
index bd7848dc7..1f40be077 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py
@@ -1,4 +1,8 @@
+import glob
import logging
+import os
+import warnings
+from pathlib import Path
from typing import Dict, List, Optional
from webviz_config import WebvizSettings
@@ -10,12 +14,19 @@
EnsembleTableProviderFactory,
)
from webviz_subsurface._utils.webvizstore_functions import read_csv
-from webviz_subsurface.plugins._co2_leakage._utilities.generic import MapAttribute
+from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import (
+ read_zone_and_region_options,
+)
+from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
+ GraphSource,
+ MapAttribute,
+)
from webviz_subsurface.plugins._map_viewer_fmu._tmp_well_pick_provider import (
WellPickProvider,
)
LOGGER = logging.getLogger(__name__)
+WARNING_THRESHOLD_CSV_FILE_SIZE_MB = 100.0
def init_map_attribute_names(
@@ -27,6 +38,9 @@ def init_map_attribute_names(
MapAttribute.MIGRATION_TIME: "migrationtime",
MapAttribute.MAX_SGAS: "max_sgas",
MapAttribute.MAX_AMFG: "max_amfg",
+ MapAttribute.MASS: "co2-mass-total",
+ MapAttribute.DISSOLVED: "co2-mass-aqu-phase",
+ MapAttribute.FREE: "co2-mass-gas-phase",
}
return {MapAttribute[key]: value for key, value in mapping.items()}
@@ -45,17 +59,23 @@ def init_surface_providers(
def init_well_pick_provider(
- well_pick_path: Optional[str],
+ well_pick_dict: Dict[str, Optional[str]],
map_surface_names_to_well_pick_names: Optional[Dict[str, str]],
-) -> Optional[WellPickProvider]:
- if well_pick_path is None:
- return None
- try:
- return WellPickProvider(
- read_csv(well_pick_path), map_surface_names_to_well_pick_names
- )
- except OSError:
- return None
+) -> Dict[str, Optional[WellPickProvider]]:
+ well_pick_provider: Dict[str, Optional[WellPickProvider]] = {}
+ ensembles = list(well_pick_dict.keys())
+ for ens in ensembles:
+ well_pick_path = well_pick_dict[ens]
+ if well_pick_path is None:
+ well_pick_provider[ens] = None
+ else:
+ try:
+ well_pick_provider[ens] = WellPickProvider(
+ read_csv(well_pick_path), map_surface_names_to_well_pick_names
+ )
+ except OSError:
+ well_pick_provider[ens] = None
+ return well_pick_provider
def init_table_provider(
@@ -65,6 +85,16 @@ def init_table_provider(
providers = {}
factory = EnsembleTableProviderFactory.instance()
for ens, ens_path in ensemble_roots.items():
+ max_size_mb = _find_max_file_size_mb(ens_path, table_rel_path)
+ if max_size_mb > WARNING_THRESHOLD_CSV_FILE_SIZE_MB:
+ text = (
+ "Some CSV-files are very large and might create problems when loading."
+ )
+ text += f"\n ensembles: {ens}"
+ text += f"\n CSV-files: {table_rel_path}"
+ text += f"\n Max size : {max_size_mb:.2f} MB"
+ LOGGER.warning(text)
+
try:
providers[ens] = factory.create_from_per_realization_csv_file(
ens_path, table_rel_path
@@ -74,3 +104,72 @@ def init_table_provider(
f'Did not load "{table_rel_path}" for ensemble "{ens}" with error {exc}'
)
return providers
+
+
+def _find_max_file_size_mb(ens_path: str, table_rel_path: str) -> float:
+ glob_pattern = os.path.join(ens_path, table_rel_path)
+ paths = glob.glob(glob_pattern)
+ max_size = 0.0
+ for file in paths:
+ if os.path.exists(file):
+ file_stats = os.stat(file)
+ size_in_mb = file_stats.st_size / (1024 * 1024)
+ max_size = max(max_size, size_in_mb)
+ return max_size
+
+
+def init_zone_and_region_options(
+ ensemble_roots: Dict[str, str],
+ mass_table: Dict[str, EnsembleTableProvider],
+ actual_volume_table: Dict[str, EnsembleTableProvider],
+ ensemble_provider: Dict[str, EnsembleSurfaceProvider],
+) -> Dict[str, Dict[str, Dict[str, List[str]]]]:
+ options: Dict[str, Dict[str, Dict[str, List[str]]]] = {}
+ for ens in ensemble_roots.keys():
+ options[ens] = {}
+ real = ensemble_provider[ens].realizations()[0]
+ for source, table in zip(
+ [GraphSource.CONTAINMENT_MASS, GraphSource.CONTAINMENT_ACTUAL_VOLUME],
+ [mass_table, actual_volume_table],
+ ):
+ try:
+ options[ens][source] = read_zone_and_region_options(table[ens], real)
+ except KeyError:
+ options[ens][source] = {"zones": [], "regions": []}
+ options[ens][GraphSource.UNSMRY] = {"zones": [], "regions": []}
+ return options
+
+
+def process_files(
+ cont_bound: Optional[str],
+ haz_bound: Optional[str],
+ well_file: Optional[str],
+ ensemble_paths: Dict[str, str],
+) -> List[Dict[str, Optional[str]]]:
+ """
+ Checks if the files exist (otherwise gives a warning and returns None)
+ Concatenates ensemble root dir and path to file if relative
+ """
+ ensembles = list(ensemble_paths.keys())
+ return [
+ {ens: _process_file(source, ensemble_paths[ens]) for ens in ensembles}
+ for source in [cont_bound, haz_bound, well_file]
+ ]
+
+
+def _process_file(file: Optional[str], ensemble_path: str) -> Optional[str]:
+ if file is not None:
+ if Path(file).is_absolute():
+ if os.path.isfile(Path(file)):
+ return file
+ warnings.warn(f"Cannot find specified file {file}.")
+ return None
+ file = os.path.join(Path(ensemble_path).parents[1], file)
+ if not os.path.isfile(file):
+ warnings.warn(
+ f"Cannot find specified file {file}.\n"
+ "Note that relative paths are accepted from ensemble root "
+ "(directory with the realizations)."
+ )
+ return None
+ return file
diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py
index b7876f17f..83f8b7ae0 100644
--- a/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py
+++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py
@@ -1,6 +1,7 @@
from dataclasses import dataclass
-from typing import List, Optional, Tuple, Union
+from typing import Any, Dict, List, Optional, Tuple, Union
+import numpy as np
import xtgeo
from webviz_subsurface._providers import (
@@ -15,10 +16,13 @@
from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_provider import (
SurfaceStatistic,
)
+from webviz_subsurface.plugins._co2_leakage._utilities.generic import MapAttribute
from webviz_subsurface.plugins._co2_leakage._utilities.plume_extent import (
truncate_surfaces,
)
+SCALE_DICT = {"kg": 1, "tons": 1000, "M tons": 1000000}
+
@dataclass
class TruncatedSurfaceAddress:
@@ -38,27 +42,42 @@ def publish_and_get_surface_metadata(
server: SurfaceImageServer,
provider: EnsembleSurfaceProvider,
address: Union[SurfaceAddress, TruncatedSurfaceAddress],
-) -> Tuple[Optional[SurfaceImageMeta], str]:
+ visualization_info: Dict[str, Any],
+ map_attribute_names: Dict[MapAttribute, str],
+) -> Tuple[Optional[SurfaceImageMeta], str, Optional[Any]]:
if isinstance(address, TruncatedSurfaceAddress):
return _publish_and_get_truncated_surface_metadata(server, provider, address)
provider_id: str = provider.provider_id()
qualified_address = QualifiedSurfaceAddress(provider_id, address)
surf_meta = server.get_surface_metadata(qualified_address)
+ summed_mass = None
if not surf_meta:
# This means we need to compute the surface
surface = provider.get_surface(address)
if not surface:
raise ValueError(f"Could not get surface for address: {address}")
+ if address.attribute in [
+ map_attribute_names[MapAttribute.MASS],
+ map_attribute_names[MapAttribute.FREE],
+ map_attribute_names[MapAttribute.DISSOLVED],
+ ]:
+ surface.values = surface.values / SCALE_DICT[visualization_info["unit"]]
+ summed_mass = np.ma.sum(surface.values)
+ if (
+ address.attribute != map_attribute_names[MapAttribute.MIGRATION_TIME]
+ and visualization_info["threshold"] >= 0
+ ):
+ surface.operation("elile", visualization_info["threshold"])
server.publish_surface(qualified_address, surface)
surf_meta = server.get_surface_metadata(qualified_address)
- return surf_meta, server.encode_partial_url(qualified_address)
+ return surf_meta, server.encode_partial_url(qualified_address), summed_mass
def _publish_and_get_truncated_surface_metadata(
server: SurfaceImageServer,
provider: EnsembleSurfaceProvider,
address: TruncatedSurfaceAddress,
-) -> Tuple[Optional[SurfaceImageMeta], str]:
+) -> Tuple[Optional[SurfaceImageMeta], str, Optional[Any]]:
qualified_address = QualifiedSurfaceAddress(
provider.provider_id(),
# TODO: Should probably use a dedicated address type for this. Statistical surface
@@ -74,13 +93,15 @@ def _publish_and_get_truncated_surface_metadata(
),
)
surf_meta = server.get_surface_metadata(qualified_address)
+ summed_mass = None
if surf_meta is None:
surface = _generate_surface(provider, address)
if surface is None:
raise ValueError(f"Could not generate surface for address: {address}")
+ summed_mass = np.ma.sum(surface.values)
server.publish_surface(qualified_address, surface)
surf_meta = server.get_surface_metadata(qualified_address)
- return surf_meta, server.encode_partial_url(qualified_address)
+ return surf_meta, server.encode_partial_url(qualified_address), summed_mass
def _generate_surface(
diff --git a/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py b/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py
index ce6b4f81c..530b943c9 100644
--- a/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py
+++ b/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py
@@ -1,8 +1,8 @@
-from typing import Any, Dict, List, Optional, Tuple
+import warnings
+from typing import Any, Dict, List, Optional, Tuple, Union
-import dash
import webviz_core_components as wcc
-from dash import Input, Output, State, callback, dcc, html
+from dash import Input, Output, State, callback, dcc, html, no_update
from dash.development.base_component import Component
from webviz_config.utils import StrEnum
from webviz_config.webviz_plugin_subclasses import SettingsGroupABC
@@ -14,6 +14,7 @@
from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import property_origin
from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
Co2MassScale,
+ ContainmentViews,
GraphSource,
LayoutLabels,
LayoutStyle,
@@ -27,6 +28,7 @@ class Ids(StrEnum):
OPTIONS_DIALOG = "options-dialog"
OPTIONS_DIALOG_OPTIONS = "options-dialog-options"
OPTIONS_DIALOG_WELL_FILTER = "options-dialog-well-filter"
+ WELL_FILTER_HEADER = "well-filter-header"
FORMATION = "formation"
ENSEMBLE = "ensemble"
@@ -46,10 +48,20 @@ class Ids(StrEnum):
Y_MAX_GRAPH = "y-max-graph"
Y_MIN_AUTO_GRAPH = "y-min-auto-graph"
Y_MAX_AUTO_GRAPH = "y-max-auto-graph"
+ ZONE = "zone"
+ REGION = "region"
+ CONTAINMENT_VIEW = "containment_view"
PLUME_THRESHOLD = "plume-threshold"
PLUME_SMOOTHING = "plume-smoothing"
+ VISUALIZATION_THRESHOLD = "visualization-threshold"
+ VISUALIZATION_UPDATE = "visualization-update"
+ MASS_UNIT = "mass-unit"
+
+ FEEDBACK_BUTTON = "feedback-button"
+ FEEDBACK = "feedback"
+
def __init__(
self,
ensemble_paths: Dict[str, str],
@@ -57,7 +69,8 @@ def __init__(
initial_surface: Optional[str],
map_attribute_names: Dict[MapAttribute, str],
color_scale_names: List[str],
- well_names: List[str],
+ well_names_dict: Dict[str, List[str]],
+ zone_and_region_options: Dict[str, Dict[str, Dict[str, List[str]]]],
):
super().__init__("Settings")
self._ensemble_paths = ensemble_paths
@@ -65,11 +78,22 @@ def __init__(
self._map_attribute_names = map_attribute_names
self._color_scale_names = color_scale_names
self._initial_surface = initial_surface
- self._well_names = well_names
+ self._well_names_dict = well_names_dict
+ self._zone_and_region_options = zone_and_region_options
+ self._has_zones = max(
+ len(inner_dict["zones"]) > 0
+ for outer_dict in zone_and_region_options.values()
+ for inner_dict in outer_dict.values()
+ )
+ self._has_regions = max(
+ len(inner_dict["regions"]) > 0
+ for outer_dict in zone_and_region_options.values()
+ for inner_dict in outer_dict.values()
+ )
def layout(self) -> List[Component]:
return [
- DialogLayout(self._well_names),
+ DialogLayout(self._well_names_dict, list(self._ensemble_paths.keys())),
OpenDialogButton(),
EnsembleSelectorLayout(
self.register_component_unique_id(self.Ids.ENSEMBLE),
@@ -86,19 +110,35 @@ def layout(self) -> List[Component]:
self.register_component_unique_id(self.Ids.CM_MAX),
self.register_component_unique_id(self.Ids.CM_MIN_AUTO),
self.register_component_unique_id(self.Ids.CM_MAX_AUTO),
+ self.register_component_unique_id(self.Ids.VISUALIZATION_THRESHOLD),
+ self.register_component_unique_id(self.Ids.VISUALIZATION_UPDATE),
+ self.register_component_unique_id(self.Ids.MASS_UNIT),
),
GraphSelectorsLayout(
self.register_component_unique_id(self.Ids.GRAPH_SOURCE),
self.register_component_unique_id(self.Ids.CO2_SCALE),
- self.register_component_unique_id(self.Ids.Y_MIN_GRAPH),
- self.register_component_unique_id(self.Ids.Y_MAX_GRAPH),
- self.register_component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH),
- self.register_component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH),
+ [
+ self.register_component_unique_id(self.Ids.Y_MIN_GRAPH),
+ self.register_component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH),
+ ],
+ [
+ self.register_component_unique_id(self.Ids.Y_MAX_GRAPH),
+ self.register_component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH),
+ ],
+ [
+ self.register_component_unique_id(self.Ids.ZONE),
+ self.register_component_unique_id(self.Ids.REGION),
+ self.register_component_unique_id(self.Ids.CONTAINMENT_VIEW),
+ ],
+ self._has_zones,
+ self._has_regions,
),
ExperimentalFeaturesLayout(
self.register_component_unique_id(self.Ids.PLUME_THRESHOLD),
self.register_component_unique_id(self.Ids.PLUME_SMOOTHING),
),
+ FeedbackLayout(),
+ FeedbackButton(),
]
def set_callbacks(self) -> None:
@@ -132,6 +172,11 @@ def set_formations(
# Map
prop_name = property_origin(MapAttribute(prop), self._map_attribute_names)
surfaces = surface_provider.surface_names_for_attribute(prop_name)
+ if len(surfaces) == 0:
+ warnings.warn(
+ f"Surface not found for property: {prop}.\n"
+ f"Expected name: --{prop_name}--.gri"
+ )
# Formation names
formations = [{"label": v.title(), "value": v} for v in surfaces]
picked_formation = None
@@ -139,7 +184,7 @@ def set_formations(
if current_value is None and self._initial_surface in surfaces:
picked_formation = self._initial_surface
elif current_value in surfaces:
- picked_formation = dash.no_update
+ picked_formation = no_update
else:
picked_formation = (
"all"
@@ -176,6 +221,16 @@ def set_color_range_data(
) -> Tuple[bool, bool]:
return len(min_auto) == 1, len(max_auto) == 1
+ @callback(
+ Output(
+ self.component_unique_id(self.Ids.VISUALIZATION_THRESHOLD).to_string(),
+ "disabled",
+ ),
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
+ )
+ def set_visualization_threshold(attribute: str) -> bool:
+ return MapAttribute(attribute) == MapAttribute.MIGRATION_TIME
+
@callback(
Output(
self.component_unique_id(self.Ids.Y_MIN_GRAPH).to_string(), "disabled"
@@ -195,6 +250,119 @@ def set_y_min_max(
) -> Tuple[bool, bool]:
return len(min_auto) == 1, len(max_auto) == 1
+ @callback(
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"),
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
+ State(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
+ )
+ def set_zones(
+ source: GraphSource,
+ ensemble: str,
+ current_value: str,
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
+ if ensemble is not None:
+ zones = self._zone_and_region_options[ensemble][source]["zones"]
+ if len(zones) > 0:
+ options = [{"label": zone.title(), "value": zone} for zone in zones]
+ return options, no_update if current_value in zones else "all"
+ return [], None
+
+ @callback(
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "disabled"),
+ Input(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
+ Input(
+ self.component_unique_id(self.Ids.CONTAINMENT_VIEW).to_string(), "value"
+ ),
+ )
+ def disable_zone(zone: str, region: str, containment_view: str) -> bool:
+ return (
+ zone is None
+ or containment_view != ContainmentViews.CONTAINMENTSPLIT
+ or (region is not None and region != "all")
+ )
+
+ @callback(
+ Output(self.component_unique_id(self.Ids.REGION).to_string(), "options"),
+ Output(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
+ State(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
+ )
+ def set_regions(
+ source: GraphSource,
+ ensemble: str,
+ current_value: str,
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
+ if ensemble is not None:
+ regions = self._zone_and_region_options[ensemble][source]["regions"]
+ if len(regions) > 0:
+ options = [{"label": reg.title(), "value": reg} for reg in regions]
+ return options, no_update if current_value in regions else "all"
+ return [], None
+
+ @callback(
+ Output(self.component_unique_id(self.Ids.REGION).to_string(), "disabled"),
+ Input(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
+ Input(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
+ Input(
+ self.component_unique_id(self.Ids.CONTAINMENT_VIEW).to_string(), "value"
+ ),
+ )
+ def disable_region(region: str, zone: str, containment_view: str) -> bool:
+ return (
+ region is None
+ or containment_view != ContainmentViews.CONTAINMENTSPLIT
+ or (zone is not None and zone != "all")
+ )
+
+ @callback(
+ Output(
+ self.component_unique_id(self.Ids.MASS_UNIT).to_string(), "disabled"
+ ),
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
+ )
+ def toggle_unit(attribute: str) -> bool:
+ if MapAttribute(attribute) not in (
+ MapAttribute.MASS,
+ MapAttribute.FREE,
+ MapAttribute.DISSOLVED,
+ ):
+ return True
+ return False
+
+ @callback(
+ Output("zone_col", "style"),
+ Output("region_col", "style"),
+ Output("both_col", "style"),
+ Output("zone_region_header", "style"),
+ Input(
+ self.component_unique_id(self.Ids.CONTAINMENT_VIEW).to_string(), "value"
+ ),
+ )
+ def hide_dropdowns(view: str) -> List[Dict[str, str]]:
+ if view != ContainmentViews.CONTAINMENTSPLIT:
+ return [{"display": "none"}] * 4
+ disp_zone = "flex" if self._has_zones else "none"
+ disp_region = "flex" if self._has_regions else "none"
+ disp_either = "flex" if self._has_zones or self._has_regions else "none"
+ return [
+ {
+ "width": "50%" if self._has_regions else "100%",
+ "display": disp_zone,
+ "flex-direction": "column",
+ },
+ {
+ "width": "50%" if self._has_zones else "100%",
+ "display": disp_region,
+ "flex-direction": "column",
+ },
+ {"display": disp_either},
+ {"display": disp_either},
+ ]
+
class OpenDialogButton(html.Button):
def __init__(self) -> None:
@@ -211,7 +379,8 @@ class DialogLayout(wcc.Dialog):
def __init__(
self,
- well_names: List[str],
+ well_names_dict: Dict[str, List[str]],
+ ensembles: List[str],
) -> None:
checklist_options = []
checklist_values = []
@@ -238,12 +407,15 @@ def __init__(
wcc.FlexBox(
children=[
html.Div(
+ id=ViewSettings.Ids.WELL_FILTER_HEADER,
style={
"flex": 3,
"minWidth": "20px",
- "display": "block" if well_names else "none",
+ "display": (
+ "block" if well_names_dict[ensembles[0]] else "none"
+ ),
},
- children=WellFilter(well_names),
+ children=WellFilter(well_names_dict, ensembles),
),
],
style={"width": "20vw"},
@@ -253,15 +425,19 @@ def __init__(
class WellFilter(html.Div):
- def __init__(self, well_names: List[str]) -> None:
+ def __init__(
+ self, well_names_dict: Dict[str, List[str]], ensembles: List[str]
+ ) -> None:
super().__init__(
- style={"display": "block" if well_names else "none"},
children=wcc.SelectWithLabel(
+ style={"display": "block" if well_names_dict[ensembles[0]] else "none"},
label=LayoutLabels.WELL_FILTER,
id=ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER,
- options=[{"label": i, "value": i} for i in well_names],
- value=well_names,
- size=min(20, len(well_names)),
+ options=[
+ {"label": i, "value": i} for i in well_names_dict[ensembles[0]]
+ ],
+ value=well_names_dict[ensembles[0]],
+ size=min(20, len(well_names_dict[ensembles[0]])),
),
)
@@ -297,6 +473,9 @@ def __init__(
cm_max_id: str,
cm_min_auto_id: str,
cm_max_auto_id: str,
+ visualization_threshold_id: str,
+ visualization_update_id: str,
+ mass_unit_id: str,
):
default_colormap = (
"turbo (Seq)"
@@ -361,6 +540,32 @@ def __init__(
],
style=self._CM_RANGE,
),
+ "Visualization threshold",
+ html.Div(
+ [
+ dcc.Input(
+ id=visualization_threshold_id,
+ type="number",
+ value=-1.0,
+ style={"width": "70%"},
+ ),
+ html.Div(style={"width": "5%"}),
+ html.Button(
+ "Update",
+ id=visualization_update_id,
+ style=LayoutStyle.VISUALIZATION_BUTTON,
+ n_clicks=0,
+ ),
+ ],
+ style={"display": "flex"},
+ ),
+ "Mass unit (for mass maps)",
+ wcc.Dropdown(
+ id=mass_unit_id,
+ options=["kg", "tons", "M tons"],
+ value="kg",
+ clearable=False,
+ ),
],
)
],
@@ -377,15 +582,41 @@ def __init__(
self,
graph_source_id: str,
co2_scale_id: str,
- y_min_id: str,
- y_max_id: str,
- y_min_auto_id: str,
- y_max_auto_id: str,
+ y_min_ids: List[str],
+ y_max_ids: List[str],
+ containment_ids: List[str],
+ has_zones: bool,
+ has_regions: bool,
):
+ disp = "flex" if has_zones or has_regions else "none"
+ disp_zone = "flex" if has_zones else "none"
+ disp_region = "flex" if has_regions else "none"
+ only_zone = has_zones and not has_regions
+ only_region = has_regions and not has_zones
+ header = "Containment for specific"
+ if only_zone:
+ header += " zone"
+ elif only_region:
+ header += " region"
+ options = [ContainmentViews.CONTAINMENTSPLIT]
+ if has_zones:
+ options.append(ContainmentViews.ZONESPLIT)
+ if has_regions:
+ options.append(ContainmentViews.REGIONSPLIT)
super().__init__(
label="Graph Settings",
open_details=False,
children=[
+ html.Div(
+ [
+ dcc.RadioItems(
+ options,
+ ContainmentViews.CONTAINMENTSPLIT,
+ id=containment_ids[2],
+ ),
+ ],
+ style={"display": disp, "flex-direction": "column"},
+ ),
"Source",
wcc.Dropdown(
id=graph_source_id,
@@ -393,6 +624,45 @@ def __init__(
value=GraphSource.CONTAINMENT_MASS,
clearable=False,
),
+ html.Div(
+ header,
+ id="zone_region_header",
+ style={"display": disp},
+ ),
+ html.Div(
+ [
+ html.Div(
+ ([] if only_zone else ["zone"])
+ + [
+ wcc.Dropdown(
+ id=containment_ids[0],
+ clearable=False,
+ ),
+ ],
+ id="zone_col",
+ style={
+ "width": "50%" if has_regions else "100%",
+ "display": disp_zone,
+ },
+ ),
+ html.Div(
+ ([] if only_region else ["region"])
+ + [
+ wcc.Dropdown(
+ id=containment_ids[1],
+ clearable=False,
+ ),
+ ],
+ id="region_col",
+ style={
+ "width": "50%" if has_zones else "100%",
+ "display": disp_region,
+ },
+ ),
+ ],
+ id="both_col",
+ style={"display": disp},
+ ),
"Unit",
wcc.Dropdown(
id=co2_scale_id,
@@ -403,11 +673,11 @@ def __init__(
"Minimum",
html.Div(
[
- dcc.Input(id=y_min_id, type="number"),
+ dcc.Input(id=y_min_ids[0], type="number"),
dcc.Checklist(
["Auto"],
["Auto"],
- id=y_min_auto_id,
+ id=y_min_ids[1],
),
],
style=self._CM_RANGE,
@@ -415,11 +685,11 @@ def __init__(
"Maximum",
html.Div(
[
- dcc.Input(id=y_max_id, type="number"),
+ dcc.Input(id=y_max_ids[0], type="number"),
dcc.Checklist(
["Auto"],
["Auto"],
- id=y_max_auto_id,
+ id=y_max_ids[1],
),
],
style=self._CM_RANGE,
@@ -494,7 +764,11 @@ def __init__(self, ensemble_id: str, realization_id: str, ensembles: List[str]):
def _compile_property_options() -> List[Dict[str, Any]]:
return [
- {"label": "SGAS", "value": "", "disabled": True},
+ {
+ "label": html.Span(["SGAS:"], style={"text-decoration": "underline"}),
+ "value": "",
+ "disabled": True,
+ },
{
"label": MapAttribute.MIGRATION_TIME.value,
"value": MapAttribute.MIGRATION_TIME.value,
@@ -504,10 +778,54 @@ def _compile_property_options() -> List[Dict[str, Any]]:
"label": MapAttribute.SGAS_PLUME.value,
"value": MapAttribute.SGAS_PLUME.value,
},
- {"label": "AMFG", "value": "", "disabled": True},
+ {
+ "label": html.Span(["AMFG:"], style={"text-decoration": "underline"}),
+ "value": "",
+ "disabled": True,
+ },
{"label": MapAttribute.MAX_AMFG.value, "value": MapAttribute.MAX_AMFG.value},
{
"label": MapAttribute.AMFG_PLUME.value,
"value": MapAttribute.AMFG_PLUME.value,
},
+ {
+ "label": html.Span(["MASS:"], style={"text-decoration": "underline"}),
+ "value": "",
+ "disabled": True,
+ },
+ {"label": MapAttribute.MASS.value, "value": MapAttribute.MASS.value},
+ {"label": MapAttribute.DISSOLVED.value, "value": MapAttribute.DISSOLVED.value},
+ {"label": MapAttribute.FREE.value, "value": MapAttribute.FREE.value},
]
+
+
+class FeedbackLayout(wcc.Dialog):
+ """Layout for the options dialog"""
+
+ def __init__(
+ self,
+ ) -> None:
+ super().__init__(
+ title=LayoutLabels.FEEDBACK,
+ id=ViewSettings.Ids.FEEDBACK,
+ draggable=True,
+ open=False,
+ children=[
+ dcc.Markdown(
+ """If you have any feedback regarding the CO2-leakage application,
+ please contact XXX@XX.X."""
+ )
+ ],
+ )
+
+
+class FeedbackButton(html.Button):
+ def __init__(self) -> None:
+ style = LayoutStyle.FEEDBACK_BUTTON
+ style["display"] = "none"
+ super().__init__(
+ LayoutLabels.FEEDBACK,
+ id=ViewSettings.Ids.FEEDBACK_BUTTON,
+ style=style,
+ n_clicks=0,
+ )