Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CO₂ migration plugin #994

Merged
merged 101 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 100 commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
b4c0161
Add files for first version of CO2 migration plugin
vegardkv Mar 16, 2022
fad30c8
Replace explicit ensemble paths by shared settings
vegardkv Mar 21, 2022
1276759
Clean up some TODOs
vegardkv Mar 21, 2022
d685ea1
Move layer model code to separate function
vegardkv Mar 22, 2022
7ae3d90
Add date slider and license boundary
vegardkv Mar 22, 2022
6ee5526
Add well picks to canvas with selector
vegardkv Mar 29, 2022
3d0c507
Add dropdown for zone-based map filter
vegardkv Mar 30, 2022
61e36da
Add comment about date slider
vegardkv Mar 31, 2022
eb954fc
Add function generating out-of-bounds CO2 figure
vegardkv Mar 31, 2022
2af60bf
Implement consistent handling of ensembles/realizations
vegardkv Apr 4, 2022
6f17f9d
Refactor code
vegardkv Apr 4, 2022
484da9b
Add support for overriding default map names
vegardkv May 4, 2022
87df7ac
Merge branch 'equinor:master' into co2-migration-plugin
vegardkv May 4, 2022
b3f9d6d
Clean up code based on TODOs
vegardkv May 4, 2022
44aa3f4
Fix a few bugs from previous commit
vegardkv May 4, 2022
8c0a110
Add "Effective Plume Height" as a map option
vegardkv May 6, 2022
e39f30e
Fix bug with fully masked maps
vegardkv May 6, 2022
1f374ab
Add CO2 leakage plot with time axis
vegardkv May 6, 2022
5b3434b
Move plots to below map
vegardkv May 12, 2022
d59e934
Unify formation/zone handling
vegardkv May 13, 2022
a67e5ba
Change date slider to discrete time steps
vegardkv May 13, 2022
6e03614
Update time-based co2 volume plot
vegardkv May 13, 2022
b867691
Split callbacks and do some minor clean-up
vegardkv Jun 13, 2022
081b6ee
Change how formations are presented
vegardkv Jun 14, 2022
852b021
Fix bug with how formation aliases are defined
vegardkv Jun 14, 2022
e6a5742
Rework formation aliases and move func to sep module
vegardkv Jun 14, 2022
5121ed8
Fix issues with formation aliases
vegardkv Jun 14, 2022
792d687
Make sure formation is not reset unless necessary
vegardkv Jun 14, 2022
a47c987
Merge branch 'equinor:master' into co2-migration-plugin
vegardkv Jun 14, 2022
933f717
Replace deckgl_props use after merging with master
vegardkv Jun 14, 2022
2e6a74e
Make color map and bounds adjustable
vegardkv Jun 15, 2022
055a4ec
Add support for statistical maps
vegardkv Jun 16, 2022
b61f938
Add feature calculating plume contours
vegardkv Jun 30, 2022
e43b8cc
Change how fault polygon layers are set
vegardkv Jul 1, 2022
a465e74
Fix contour issue with MPL version
vegardkv Jul 1, 2022
0b0ddb0
Rework layout to accommodate plume settings
vegardkv Jul 6, 2022
00d3a2b
Adjust checkbox min size
vegardkv Jul 6, 2022
8935d05
Add AMFG as property option
vegardkv Jul 6, 2022
7597c42
Fix browser warnings and add initial map bounds
vegardkv Jul 6, 2022
ca87217
Adjust initial date to last
vegardkv Jul 6, 2022
4e2a952
Merge branch 'equinor:master' into co2-migration-plugin
vegardkv Aug 10, 2022
f08b30c
Fix map bounds after initialization
vegardkv Aug 10, 2022
786b473
Move date slider to below map
vegardkv Aug 10, 2022
f8fdd5e
Add separtor before color bar settings
vegardkv Aug 11, 2022
037705e
Add frequency maps
vegardkv Aug 15, 2022
fbf78d7
Fix minor bug
vegardkv Aug 16, 2022
767bb64
Add data store for plume contour data
vegardkv Aug 16, 2022
e82b8ee
Sync how plume smoothing is performed
vegardkv Aug 16, 2022
1f7e8a1
Reduce date slider label to show only year
vegardkv Aug 22, 2022
2f719d3
Make date slider conditionally hide
vegardkv Aug 22, 2022
cbfd41e
Toggle "statistic" based on realization count
vegardkv Aug 22, 2022
b7cd5bc
Rename "Maximum saturation"
vegardkv Aug 22, 2022
043d33e
Rearrange order and color in bar plot
vegardkv Aug 22, 2022
e672b55
Add headlines in property dropdown
vegardkv Aug 22, 2022
15464cd
Change "plume extent" names
vegardkv Aug 22, 2022
e5f6d66
Toggle stats dropdown when plume is shown
vegardkv Aug 22, 2022
c66139b
Add colormaps based on MPL
vegardkv Aug 24, 2022
12acb15
Change hover name on deck gl canvas
vegardkv Aug 24, 2022
8b740cf
Change hover name on deck gl canvas
vegardkv Aug 24, 2022
2e08d65
Fix error message when AMFG data is missing
vegardkv Aug 24, 2022
bd3786e
Let surface names determine formation dropdown
vegardkv Aug 24, 2022
4dc6a76
Change "plume" attribute from ratio to count
vegardkv Aug 24, 2022
650c521
Fix bug with plume contour generation
vegardkv Aug 24, 2022
8b368bc
Opt into showing deck gl toolbar and change default formation
vegardkv Aug 25, 2022
7828e28
Add relpath arguments in place of hard-coded values
vegardkv Aug 25, 2022
2db5961
Fix path error
vegardkv Aug 26, 2022
b7496b2
Rename plugin
vegardkv Aug 26, 2022
e7cd835
Refactor names and clean-up code
vegardkv Aug 29, 2022
daa8efd
Merge branch 'equinor:master' into co2-migration-plugin
vegardkv Aug 29, 2022
73efa9c
Start conversion to new layout framework
vegardkv Sep 1, 2022
55d31c2
Move utilities to submodule
vegardkv Sep 1, 2022
39bf17e
Move callbacks to settings.py
vegardkv Sep 1, 2022
78bb1d4
Remove color map data store
vegardkv Sep 2, 2022
a8fb68f
Remove plume data store
vegardkv Sep 2, 2022
20e92da
Replace formation aliases with MapViewerFMU equivalent
vegardkv Sep 5, 2022
f6e6d62
Encapsulate fault polygon extraction code
vegardkv Sep 5, 2022
0ab6453
Minor name fix
vegardkv Sep 5, 2022
2ad9e60
Refactor some code
vegardkv Sep 5, 2022
4eccdc3
Minor adjustment
vegardkv Sep 5, 2022
e864eb2
Fix issue with missing data
vegardkv Sep 6, 2022
370ae9a
Remove fixed figure height/width
vegardkv Sep 12, 2022
24eb199
Remove fixed figure height/width
vegardkv Sep 12, 2022
ae1b537
Fix duplcated color table definition
vegardkv Sep 12, 2022
3ecccb3
Replace containment table file IO with provider
vegardkv Sep 12, 2022
eaa221e
Remove some dependencies on FMU file structure
vegardkv Sep 12, 2022
9639fa8
Remove remaining file IO-dependent functionality
vegardkv Sep 13, 2022
c080777
Replace date store by class function
vegardkv Sep 13, 2022
3eaddcc
Apply black formatting
vegardkv Sep 15, 2022
ae87a8f
Fix pylint issues
vegardkv Sep 15, 2022
5cac086
Fix import order issues (isort)
vegardkv Sep 21, 2022
1780522
Fix mypy complaints
vegardkv Sep 21, 2022
a25d642
Update docstring
vegardkv Sep 21, 2022
ccb55e9
Add 'initial_surface' option to co2 plugin
vegardkv Sep 23, 2022
a96ce4b
Make color scale not clearable
vegardkv Sep 23, 2022
d421543
Fix code styling and linting errors++
vegardkv Sep 23, 2022
b7764aa
Make plume contours depend on library availability
vegardkv Sep 30, 2022
aad5905
Merge branch 'equinor:master' into co2-migration-plugin
vegardkv Oct 7, 2022
0b7318f
Fix pylint issues
vegardkv Oct 7, 2022
365aaae
Fix black issue
vegardkv Oct 7, 2022
96cbd63
Fix isort issue
vegardkv Oct 7, 2022
2ed4475
Merge branch 'master' into co2-migration-plugin
HansKallekleiv Nov 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
entry_points={
"webviz_config_plugins": [
"BhpQc = webviz_subsurface.plugins:BhpQc",
"CO2Leakage = webviz_subsurface.plugins:CO2Leakage",
"DiskUsage = webviz_subsurface.plugins:DiskUsage",
"GroupTree = webviz_subsurface.plugins:GroupTree",
"HistoryMatch = webviz_subsurface.plugins:HistoryMatch",
Expand Down
1 change: 1 addition & 0 deletions webviz_subsurface/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from ._assisted_history_matching_analysis import AssistedHistoryMatchingAnalysis
from ._bhp_qc import BhpQc
from ._co2_leakage import CO2Leakage
from ._disk_usage import DiskUsage
from ._group_tree import GroupTree
from ._history_match import HistoryMatch
Expand Down
1 change: 1 addition & 0 deletions webviz_subsurface/plugins/_co2_leakage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._plugin import CO2Leakage
5 changes: 5 additions & 0 deletions webviz_subsurface/plugins/_co2_leakage/_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from dash import html


def error(error_message: str) -> html.Div:
return html.Div(children=error_message, style={"color": "red"})
306 changes: 306 additions & 0 deletions webviz_subsurface/plugins/_co2_leakage/_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
from typing import Any, Dict, List, Optional, Tuple

import dash
import plotly.graph_objects as go
from dash import Dash, Input, Output, State, callback, html
from dash.exceptions import PreventUpdate
from webviz_config import WebvizPluginABC, WebvizSettings
from webviz_config.utils import StrEnum

from webviz_subsurface._providers import FaultPolygonsServer, SurfaceServer
from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import (
SurfaceData,
create_map_layers,
derive_surface_address,
get_plume_polygon,
property_origin,
readable_name,
)
from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import (
generate_co2_time_containment_figure,
generate_co2_volume_figure,
)
from webviz_subsurface.plugins._co2_leakage._utilities.fault_polygons import (
FaultPolygonsHandler,
)
from webviz_subsurface.plugins._co2_leakage._utilities.generic import MapAttribute
from webviz_subsurface.plugins._co2_leakage._utilities.initialization import (
init_co2_containment_table_providers,
init_map_attribute_names,
init_surface_providers,
init_well_pick_provider,
)
from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
INITIAL_BOUNDS,
MainView,
MapViewElement,
)
from webviz_subsurface.plugins._co2_leakage.views.mainview.settings import ViewSettings

from . import _error
from ._utilities.color_tables import CO2LEAKAGE_COLOR_TABLES


class CO2Leakage(WebvizPluginABC):
"""
Plugin for analyzing CO2 leakage potential across multiple realizations in an FMU
ensemble

* **`ensembles`:** Which ensembles in `shared_settings` to visualize.
* **`boundary_file`:** Path to a polygon representing the containment area
* **`well_pick_file`:** Path to a file containing well picks
* **`co2_containment_relpath`:** Path to a table of co2 containment data (amount of
CO2 outside/inside a boundary). Relative to each realization.
* **`fault_polygon_attribute`:** Polygons with this attribute are used as fault
polygons
* **`map_attribute_names`:** Dictionary for overriding the default mapping between
attributes visualized by the plugin and attributes names used by
EnsembleSurfaceProvider
* **`initial_surface`:** Name of the surface/formation to show when the plugin is
launched. If no name is provided, the first alphabetical surface is shown.
* **`map_surface_names_to_well_pick_names`:** Optional mapping between surface map
names and surface names used in the well pick file
* **`map_surface_names_to_fault_polygons`:** Optional mapping between surface map
names and surface names used by the fault polygons
"""

class Ids(StrEnum):
MAIN_VIEW = "main-view"
MAIN_SETTINGS = "main-settings"

# pylint: disable=too-many-arguments
def __init__(
self,
app: Dash,
webviz_settings: WebvizSettings,
ensembles: List[str],
boundary_file: Optional[str] = None,
well_pick_file: Optional[str] = None,
co2_containment_relpath: str = "share/results/tables/co2_volumes.csv",
fault_polygon_attribute: str = "dl_extracted_faultlines",
initial_surface: Optional[str] = None,
map_attribute_names: Optional[Dict[str, str]] = None,
map_surface_names_to_well_pick_names: Optional[Dict[str, str]] = None,
map_surface_names_to_fault_polygons: Optional[Dict[str, str]] = None,
):
super().__init__()
self._error_message = ""

self._boundary_file = boundary_file
try:
self._ensemble_paths = webviz_settings.shared_settings["scratch_ensembles"]
self._surface_server = SurfaceServer.instance(app)
self._polygons_server = FaultPolygonsServer.instance(app)

self._map_attribute_names = init_map_attribute_names(map_attribute_names)
# Surfaces
self._ensemble_surface_providers = init_surface_providers(
webviz_settings, ensembles
)
# Polygons
self._fault_polygon_handlers = {
ens: FaultPolygonsHandler(
self._polygons_server,
self._ensemble_paths[ens],
map_surface_names_to_fault_polygons or {},
fault_polygon_attribute,
)
for ens in ensembles
}
# CO2 containment
self._co2_table_providers = init_co2_containment_table_providers(
self._ensemble_paths,
co2_containment_relpath,
)
# Well picks
self._well_pick_provider = init_well_pick_provider(
well_pick_file,
map_surface_names_to_well_pick_names,
)
except Exception as err:
self._error_message = f"Plugin initialization failed: {err}"
raise

self.add_shared_settings_group(
ViewSettings(
self._ensemble_paths,
self._ensemble_surface_providers,
initial_surface,
self._map_attribute_names,
[c["name"] for c in CO2LEAKAGE_COLOR_TABLES], # type: ignore
),
self.Ids.MAIN_SETTINGS,
)
self.add_view(MainView(CO2LEAKAGE_COLOR_TABLES), self.Ids.MAIN_VIEW)

@property
def layout(self) -> html.Div:
return _error.error(self._error_message)

def _view_component(self, component_id: str) -> str:
return (
self.view(self.Ids.MAIN_VIEW)
.view_element(MainView.Ids.MAIN_ELEMENT)
.component_unique_id(component_id)
.to_string()
)

def _settings_component(self, component_id: str) -> str:
return (
self.shared_settings_group(self.Ids.MAIN_SETTINGS)
.component_unique_id(component_id)
.to_string()
)

def _ensemble_dates(self, ens: str) -> List[str]:
surface_provider = self._ensemble_surface_providers[ens]
att_name = self._map_attribute_names[MapAttribute.MAX_SGAS]
dates = surface_provider.surface_dates_for_attribute(att_name)
if dates is None:
raise ValueError(f"Failed to fetch dates for attribute '{att_name}'")
return dates

def _set_callbacks(self) -> None:
@callback(
Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
)
def update_graphs(ensemble: str) -> Tuple[go.Figure, go.Figure]:
fig_args = (
self._co2_table_providers[ensemble],
self._co2_table_providers[ensemble].realizations(),
)
fig0 = generate_co2_volume_figure(*fig_args)
fig1 = generate_co2_time_containment_figure(*fig_args)
return fig0, fig1

@callback(
vegardkv marked this conversation as resolved.
Show resolved Hide resolved
Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
)
def set_dates(ensemble: str) -> Tuple[Dict[int, Dict[str, Any]], Optional[int]]:
if ensemble is None:
return {}, None
# Dates
date_list = self._ensemble_dates(ensemble)
dates = {
i: {
"label": f"{d[:4]}",
"style": {"writingMode": "vertical-rl"},
}
for i, d in enumerate(date_list)
}
initial_date = max(dates.keys())
return dates, initial_date

@callback(
Output(self._view_component(MapViewElement.Ids.DATE_WRAPPER), "style"),
Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
)
def toggle_date_slider(attribute: str) -> Dict[str, str]:
if MapAttribute(attribute) == MapAttribute.MIGRATION_TIME:
return {"display": "none"}
return {}

# 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
@callback(
Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "layers"),
Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "bounds"),
Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
Input(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
Input(self._settings_component(ViewSettings.Ids.FORMATION), "value"),
Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
Input(self._settings_component(ViewSettings.Ids.STATISTIC), "value"),
Input(self._settings_component(ViewSettings.Ids.COLOR_SCALE), "value"),
Input(self._settings_component(ViewSettings.Ids.CM_MIN_AUTO), "value"),
Input(self._settings_component(ViewSettings.Ids.CM_MIN), "value"),
Input(self._settings_component(ViewSettings.Ids.CM_MAX_AUTO), "value"),
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"),
State(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
State(self._view_component(MapViewElement.Ids.DECKGL_MAP), "bounds"),
)
def update_map_attribute(
attribute: MapAttribute,
date: int,
formation: str,
realization: List[int],
statistic: str,
color_map_name: str,
cm_min_auto: List[str],
cm_min_val: Optional[float],
cm_max_auto: List[str],
cm_max_val: Optional[float],
plume_threshold: Optional[float],
plume_smoothing: Optional[float],
ensemble: str,
current_bounds: List[float],
) -> Tuple[List[Dict[str, Any]], Optional[List[float]]]:
attribute = MapAttribute(attribute)
if len(realization) == 0:
raise PreventUpdate
if ensemble is None:
raise PreventUpdate
datestr = self._ensemble_dates(ensemble)[date]
# Contour data
contour_data = None
if attribute in (MapAttribute.SGAS_PLUME, MapAttribute.AMFG_PLUME):
contour_data = {
"property": property_origin(attribute, self._map_attribute_names),
"threshold": plume_threshold,
"smoothing": plume_smoothing,
}
# Surface
surf_data = None
if formation is not None and len(realization) > 0:
surf_data = SurfaceData.from_server(
server=self._surface_server,
provider=self._ensemble_surface_providers[ensemble],
address=derive_surface_address(
formation,
attribute,
datestr,
realization,
self._map_attribute_names,
statistic,
contour_data,
),
color_map_range=(
cm_min_val if len(cm_min_auto) == 0 else None,
cm_max_val if len(cm_max_auto) == 0 else None,
),
color_map_name=color_map_name,
readable_name_=readable_name(attribute),
)
# Plume polygon
plume_polygon = None
if contour_data is not None:
plume_polygon = get_plume_polygon(
self._ensemble_surface_providers[ensemble],
realization,
formation,
datestr,
contour_data,
)
# Create layers and view bounds
layers, viewport_bounds = create_map_layers(
formation=formation,
surface_data=surf_data,
fault_polygon_url=(
self._fault_polygon_handlers[ensemble].extract_fault_polygon_url(
formation,
realization,
)
),
license_boundary_file=self._boundary_file,
well_pick_provider=self._well_pick_provider,
plume_extent_data=plume_polygon,
)
if tuple(current_bounds) != INITIAL_BOUNDS or viewport_bounds is None:
viewport_bounds = dash.no_update
return layers, viewport_bounds
Empty file.
Loading