Skip to content

Commit

Permalink
GroupTree plugin for network visualization (#771)
Browse files Browse the repository at this point in the history
  • Loading branch information
lindjoha authored Sep 24, 2021
1 parent 8ebbfa6 commit 0a4d12e
Show file tree
Hide file tree
Showing 11 changed files with 874 additions and 0 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"webviz_config_plugins": [
"BhpQc = webviz_subsurface.plugins:BhpQc",
"DiskUsage = webviz_subsurface.plugins:DiskUsage",
"GroupTree = webviz_subsurface.plugins:GroupTree",
"HistoryMatch = webviz_subsurface.plugins:HistoryMatch",
"HorizonUncertaintyViewer = webviz_subsurface.plugins:HorizonUncertaintyViewer",
"InplaceVolumes = webviz_subsurface.plugins:InplaceVolumes",
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 @@ -58,3 +58,4 @@
from ._well_log_viewer import WellLogViewer
from ._assisted_history_matching_analysis import AssistedHistoryMatchingAnalysis
from ._well_completions import WellCompletions
from ._group_tree import GroupTree
1 change: 1 addition & 0 deletions webviz_subsurface/plugins/_group_tree/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .group_tree import GroupTree
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .controllers import controllers
86 changes: 86 additions & 0 deletions webviz_subsurface/plugins/_group_tree/controllers/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Callable, Optional, Any, Tuple, List, Dict

import dash
from dash.dependencies import Input, Output, State
import webviz_subsurface_components

from ..group_tree_data import GroupTreeData


def controllers(
app: dash.Dash, get_uuid: Callable, grouptreedata: GroupTreeData
) -> None:
@app.callback(
Output({"id": get_uuid("controls"), "element": "tree_mode"}, "options"),
Output({"id": get_uuid("controls"), "element": "tree_mode"}, "value"),
Output({"id": get_uuid("controls"), "element": "realization"}, "options"),
Output({"id": get_uuid("controls"), "element": "realization"}, "value"),
Input({"id": get_uuid("controls"), "element": "ensemble"}, "value"),
)
def _update_ensemble_options(
ensemble: str,
) -> Tuple[List[Dict[str, Any]], str, List[Dict[str, Any]], Optional[int]]:
"""Updates the selection options when the ensemble value changes"""
tree_mode_options: List[Dict[str, Any]] = [
{
"label": "Ensemble mean",
"value": "plot_mean",
},
{
"label": "Single realization",
"value": "single_real",
},
]
tree_mode_value = "plot_mean"

if not grouptreedata.tree_is_equivalent_in_all_real(ensemble):
tree_mode_options[0]["label"] = "Ensemble mean (disabled)"
tree_mode_options[0]["disabled"] = True
tree_mode_value = "single_real"

real_options, real_default = grouptreedata.get_ensemble_real_options(ensemble)
return (
tree_mode_options,
tree_mode_value,
real_options,
real_default,
)

@app.callback(
Output(get_uuid("grouptree_wrapper"), "children"),
Input({"id": get_uuid("controls"), "element": "tree_mode"}, "value"),
Input({"id": get_uuid("controls"), "element": "realization"}, "value"),
Input({"id": get_uuid("filters"), "element": "prod_inj_other"}, "value"),
State({"id": get_uuid("controls"), "element": "ensemble"}, "value"),
)
def _render_grouptree(
tree_mode: str, real: int, prod_inj_other: list, ensemble: str
) -> list:
"""This callback updates the input dataset to the Grouptree component."""
data, edge_options, node_options = grouptreedata.create_grouptree_dataset(
ensemble, tree_mode, real, prod_inj_other
)

return [
webviz_subsurface_components.GroupTree(
id="grouptree",
data=data,
edge_options=edge_options,
node_options=node_options,
),
]

@app.callback(
Output(
{"id": get_uuid("controls"), "element": "single_real_options"},
"style",
),
Input(
{"id": get_uuid("controls"), "element": "tree_mode"},
"value",
),
)
def _show_hide_single_real_options(tree_mode: str) -> Dict:
if tree_mode == "plot_mean":
return {"display": "none"}
return {"display": "block"}
201 changes: 201 additions & 0 deletions webviz_subsurface/plugins/_group_tree/group_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
from typing import List, Dict, Tuple, Callable

import pandas as pd
import dash
from dash import html

from webviz_config.webviz_store import webvizstore
from webviz_config import WebvizPluginABC
from webviz_config import WebvizSettings

from ..._models import EnsembleSetModel
from ..._models import caching_ensemble_set_model_factory
from ..._datainput.fmu_input import scratch_ensemble
from .group_tree_data import GroupTreeData
from .controllers import controllers
from .views import main_view


class GroupTree(WebvizPluginABC):
"""This plugin vizualizes the network tree and displays pressures,
rates and other network related information.
---
* **`ensembles`:** Which ensembles in `shared_settings` to include.
* **`gruptree_file`:** `.csv` with gruptree information.
* **`time_index`:** Frequency for the data sampling.
---
**Summary data**
This plugin needs the following summary vectors to be exported:
* FOPR, FWPR, FOPR, FWIR and FGIR
* GPR for all group nodes in the network
* GOPR, GWPR and GGPR for all group nodes in the production network \
(GOPRNB etc for BRANPROP trees)
* GGIR and/or GWIR for all group nodes in the injection network
* WSTAT, WTHP, WBHP, WMCTL for all wells
* WOPR, WWPR, WGPR for all producers
* WWIR and/or WGIR for all injectors
**GRUPTREE input**
`gruptree_file` is a path to a file stored per realization (e.g. in \
`share/results/tables/gruptree.csv"`).
The `gruptree_file` file can be dumped to disk per realization by the `ECL2CSV` forward
model with subcommand `gruptree`:
[Link to ECL2CSV](https://fmu-docs.equinor.com/docs/ert/reference/forward_models.html).
The forward model uses `ecl2df` to export a table representation of the Eclipse network:
[Link to ecl2csv gruptree documentation.](https://equinor.github.io/ecl2df/usage/gruptree.html).
**time_index**
This is the sampling interval of the summary data. It is `yearly` by default, but can be set
to `monthly` if needed.
"""

def __init__(
self,
app: dash.Dash,
webviz_settings: WebvizSettings,
ensembles: list,
gruptree_file: str = "share/results/tables/gruptree.csv",
time_index: str = "yearly",
):
super().__init__()
assert time_index in [
"monthly",
"yearly",
], "time_index must be monthly or yearly"
self.ensembles = ensembles
self.gruptree_file = gruptree_file
self.time_index = time_index
self.ens_paths = {
ens: webviz_settings.shared_settings["scratch_ensembles"][ens]
for ens in ensembles
}

self.emodel: EnsembleSetModel = (
caching_ensemble_set_model_factory.get_or_create_model(
ensemble_paths={
ens: webviz_settings.shared_settings["scratch_ensembles"][ens]
for ens in ensembles
}
)
)
smry = self.emodel.get_or_load_smry_cached()
gruptree = read_gruptree_files(self.ens_paths, self.gruptree_file)
smry["DATE"] = pd.to_datetime(smry["DATE"])
gruptree["DATE"] = pd.to_datetime(gruptree["DATE"])

if time_index == "yearly":
smry = smry[smry["DATE"].dt.is_year_start]

self.grouptreedata = GroupTreeData(smry, gruptree)

self.set_callbacks(app)

def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]:
functions: List[Tuple[Callable, List[Dict]]] = self.emodel.webvizstore
functions.append(
(
read_gruptree_files,
[{"ens_paths": self.ens_paths, "gruptree_file": self.gruptree_file}],
)
)
return functions

@property
def tour_steps(self) -> List[dict]:
return [
{
"id": self.uuid("layout"),
"content": "Dashboard vizualizing Eclipse network tree.",
},
{
"id": self.uuid("selections_layout"),
"content": "Menu for selecting ensemble and other options.",
},
{
"id": self.uuid("filters_layout"),
"content": "Menu for filtering options.",
},
{
"id": self.uuid("grouptree_wrapper"),
"content": "Vizualisation of network tree.",
},
]

@property
def layout(self) -> html.Div:
return html.Div(
children=[
# clientside_stores(get_uuid=self.uuid),
main_view(get_uuid=self.uuid, ensembles=self.ensembles),
],
)

def set_callbacks(self, app: dash.Dash) -> None:
controllers(app=app, get_uuid=self.uuid, grouptreedata=self.grouptreedata)


@webvizstore
def read_gruptree_files(ens_paths: Dict[str, str], gruptree_file: str) -> pd.DataFrame:
"""Searches for gruptree files on the scratch disk. These
files can be exported in the FMU workflow using the ECL2CSV
forward model with subcommand gruptree.
"""
df = pd.DataFrame()
for ens_name, ens_path in ens_paths.items():
df_ens = read_ensemble_gruptree(ens_name, ens_path, gruptree_file)
df_ens["ENSEMBLE"] = ens_name
df = pd.concat([df, df_ens])
return df.where(pd.notnull(df), None)


def read_ensemble_gruptree(
ens_name: str, ens_path: str, gruptree_file: str
) -> pd.DataFrame:
"""Reads the gruptree file for an ensemble.
If BRANPROP is found in the KEYWORD column, then GRUPTREE rows
are filtered out.
If the trees are equal in every realization, only one realization is kept.
"""

ens = scratch_ensemble(ens_name, ens_path, filter_file="OK")
df_files = ens.find_files(gruptree_file)

if df_files.empty:
raise ValueError(f"No gruptree file available for ensemble: {ens_name}")

# Load all gruptree dataframes and check if they are equal
compare_columns = ["DATE", "CHILD", "KEYWORD", "PARENT"]
df_prev = pd.DataFrame()
dataframes = []
gruptrees_are_equal = True
for i, row in df_files.iterrows():
df_real = pd.read_csv(row["FULLPATH"])

if "BRANPROP" in df_real["KEYWORD"].unique():
df_real = df_real[df_real["KEYWORD"] != "GRUPTREE"]
if (
i > 0
and gruptrees_are_equal
and not df_real[compare_columns].equals(df_prev)
):
gruptrees_are_equal = False
else:
df_prev = df_real[compare_columns].copy()

df_real["REAL"] = row["REAL"]
dataframes.append(df_real)
df_all = pd.concat(dataframes)

# Return either one or all realization in a common dataframe
if gruptrees_are_equal:
return df_all[df_all["REAL"] == df_all["REAL"].min()]
return df_all
Loading

0 comments on commit 0a4d12e

Please sign in to comment.