From ff9ddc0e0f51fc452616a753c0bcf4877970ce25 Mon Sep 17 00:00:00 2001 From: Asgeir Nyvoll Date: Thu, 16 Apr 2020 10:50:58 +0200 Subject: [PATCH] Simulation metadata --- ...reservoir_simulation_unit_terminology.json | 37 ++- .../abbreviation_data/volume_terminology.json | 1 + .../_abbreviations/number_formatting.py | 32 ++- .../_abbreviations/reservoir_simulation.py | 54 +++- webviz_subsurface/_datainput/fmu_input.py | 18 ++ .../_utils/simulation_timeseries.py | 38 +++ webviz_subsurface/plugins/_inplace_volumes.py | 69 +++-- .../plugins/_inplace_volumes_onebyone.py | 107 +++++--- .../_parameter_response_correlation.py | 25 +- .../_reservoir_simulation_timeseries.py | 245 +++++++++++------- ...eservoir_simulation_timeseries_onebyone.py | 145 ++++++++--- 11 files changed, 538 insertions(+), 233 deletions(-) create mode 100644 webviz_subsurface/_utils/simulation_timeseries.py diff --git a/webviz_subsurface/_abbreviations/abbreviation_data/reservoir_simulation_unit_terminology.json b/webviz_subsurface/_abbreviations/abbreviation_data/reservoir_simulation_unit_terminology.json index 24e271b6a2..9f34595ce9 100644 --- a/webviz_subsurface/_abbreviations/abbreviation_data/reservoir_simulation_unit_terminology.json +++ b/webviz_subsurface/_abbreviations/abbreviation_data/reservoir_simulation_unit_terminology.json @@ -1,17 +1,32 @@ { - "METRIC": - { - "SM3": "Sm³", + "METRIC": { + "M": "m", "M3": "m³", - "RM3": "m³", + "SECONDS": "seconds", + "DAYS": "days", + "YEARS": "years", + "KG/M3": "kg/m³", + "BARSA": "bara", + "bars": "bar", + "K": "K", + "C": "\u00B0C", + "CP": "cP", + "MD": "mD", + "SM3": "Sm³", + "RM3": "Rm³", "SM3/DAY": "Sm³/day", - "RM3/DAY": "m³/day", + "RM3/DAY": "Rm³/day", + "CPR3/DAY/BARS": "Rm³\u00D7cP/day/bar", + "MDM": "mD\u00D7m", + "KG": "kg", + "KG/DAY": "kg/day", "SM3/SM3": "Sm³/Sm³", - "RM3/SM3": "m³/Sm³", - "BARSA": "bara", - "BARS": "barg", + "RM3/SM3": "Rm³/Sm³", + "SM3/RM3": "Sm³/Rm³", + "SM3/DAY/BARS": "Sm³/day/bar", "SM3/D/B": "Sm³/day/bar", - "SECONDS": "s", - "DAYS": "days" - } + "KJ": "kJ", + "KJ/DAY": "kJ/day", + "SEC/D": "seconds/day" + } } diff --git a/webviz_subsurface/_abbreviations/abbreviation_data/volume_terminology.json b/webviz_subsurface/_abbreviations/abbreviation_data/volume_terminology.json index 70cfea37dc..a4b62b41bc 100644 --- a/webviz_subsurface/_abbreviations/abbreviation_data/volume_terminology.json +++ b/webviz_subsurface/_abbreviations/abbreviation_data/volume_terminology.json @@ -24,3 +24,4 @@ "RECOVERABLE_GAS": {"label": "Recoverable volume (gas zone)", "unit": "Sm³"}, "RECOVERABLE_TOTAL": {"label": "Recoverable volume (total)", "unit": "Sm³"} } + diff --git a/webviz_subsurface/_abbreviations/number_formatting.py b/webviz_subsurface/_abbreviations/number_formatting.py index e5f886d1b6..2c78cdaa8e 100644 --- a/webviz_subsurface/_abbreviations/number_formatting.py +++ b/webviz_subsurface/_abbreviations/number_formatting.py @@ -2,23 +2,33 @@ import json import pathlib import warnings -from typing import Optional, Union +from typing import Optional, Union, List _DATA_PATH = pathlib.Path(__file__).parent.absolute() / "abbreviation_data" SI_PREFIXES = json.loads((_DATA_PATH / "si_prefixes.json").read_text()) -TABLE_STATISTICS_BASE = [ - ( - i, - { - "type": "numeric", - "format": {"locale": {"symbol": ["", "unit_insert"]}, "specifier": "$.4s",}, - }, - ) - for i in ["Mean", "Stddev", "Minimum", "P90", "P10", "Maximum"] -] + +def table_statistics_base() -> List[tuple]: + return [ + ( + i, + { + "type": "numeric", + "format": {"locale": {"symbol": ["", ""]}, "specifier": "$.4s",}, + }, + ) + if i != "Stddev" + else ( + i, + { + "type": "numeric", + "format": {"locale": {"symbol": ["", ""]}, "specifier": "$.3s",}, + }, + ) + for i in ["Mean", "Stddev", "Minimum", "P90", "P10", "Maximum"] + ] def si_prefixed( diff --git a/webviz_subsurface/_abbreviations/reservoir_simulation.py b/webviz_subsurface/_abbreviations/reservoir_simulation.py index 458ef07833..183746931b 100644 --- a/webviz_subsurface/_abbreviations/reservoir_simulation.py +++ b/webviz_subsurface/_abbreviations/reservoir_simulation.py @@ -1,6 +1,9 @@ import json import pathlib import warnings +from typing import Optional + +import pandas as pd _DATA_PATH = pathlib.Path(__file__).parent.absolute() / "abbreviation_data" @@ -14,7 +17,7 @@ ) -def simulation_unit_reformat(ecl_unit: str, unit_set: str = "METRIC"): +def simulation_unit_reformat(ecl_unit: str, unit_set: str = "METRIC") -> str: """Returns the simulation unit in a different, more human friendly, format if possible, otherwise returns the simulation unit. * `ecl_unit`: Reservoir simulation vector unit to reformat @@ -23,15 +26,14 @@ def simulation_unit_reformat(ecl_unit: str, unit_set: str = "METRIC"): return RESERVOIR_SIMULATION_UNIT_TERMINOLOGY[unit_set].get(ecl_unit, ecl_unit) -def simulation_vector_base(vector: str): +def simulation_vector_base(vector: str) -> str: """Returns base name of simulation vector E.g. WOPR for WOPR:OP_1 and ROIP for ROIP_REG:1 """ - return vector.split(":", 1)[0].split("_", 1)[0] -def simulation_vector_description(vector: str): +def simulation_vector_description(vector: str) -> str: """Returns a more human friendly description of the simulation vector if possible, otherwise returns the input as is. """ @@ -71,3 +73,47 @@ def simulation_vector_description(vector: str): ) return description + + +def historical_vector( + vector: str, + smry_meta: Optional[pd.DataFrame] = None, + return_historical: Optional[bool] = True, +): + """This function is trying to make a best guess on converting between historical and + non-historical vector names. + + `vector`: An Eclipse-format vector string + `smry_meta`: Note: Not activate avaiting https://github.com/equinor/libecl/issues/708 + A pandas DataFrame with vector metadata on the format returned by + `load_smry_meta` in `../_datainput/fmu_input.py`. Here the field is_historical is + used to check if a vector is a historical vector. + `return_historical`: If return_historical is `True`, the corresponding guessed historical + vector name is returned if the guessed vector is thought to be a + historical vector, else None is returned. If `False` the corresponding + non-historical vector name is returned, if the input vector is thought to + be a historical vector, else None is returned. + """ + smry_meta = None # Temp deactivation waiting on https://github.com/equinor/libecl/issues/708 + parts = vector.split(":", 1) + if return_historical: + parts[0] += "H" + hist_vec = ":".join(parts) + return ( + None + if historical_vector(hist_vec, smry_meta=smry_meta, return_historical=False) + is None + else hist_vec + ) + + if smry_meta is None: + if parts[0].endswith("H") and parts[0].startswith(("F", "G", "W")): + parts[0] = parts[0][:-1] + return ":".join(parts) + return None + + try: + is_hist = smry_meta.is_historical[vector] + except KeyError: + is_hist = False + return parts[0][:-1] if is_hist else None diff --git a/webviz_subsurface/_datainput/fmu_input.py b/webviz_subsurface/_datainput/fmu_input.py index 481bbb2292..ac17a96ae4 100644 --- a/webviz_subsurface/_datainput/fmu_input.py +++ b/webviz_subsurface/_datainput/fmu_input.py @@ -63,6 +63,24 @@ def load_smry( ) +@CACHE.memoize(timeout=CACHE.TIMEOUT) +@webvizstore +def load_smry_meta( + ensemble_paths: dict, + ensemble_set_name: str = "EnsembleSet", + column_keys: Optional[list] = None, +) -> pd.DataFrame: + """Finds metadata for the summary vectors in the ensemble set. + Note that we assume the same units for all ensembles. + (meaning that we update/overwrite when checking the next ensemble) + """ + ensemble_set = load_ensemble_set(ensemble_paths, ensemble_set_name) + smry_meta = {} + for ensname in ensemble_set.ensemblenames: + smry_meta.update(ensemble_set[ensname].get_smry_meta(column_keys=column_keys)) + return pd.DataFrame(smry_meta).transpose() + + @CACHE.memoize(timeout=CACHE.TIMEOUT) @webvizstore def get_realizations( diff --git a/webviz_subsurface/_utils/simulation_timeseries.py b/webviz_subsurface/_utils/simulation_timeseries.py new file mode 100644 index 0000000000..45c644cb7f --- /dev/null +++ b/webviz_subsurface/_utils/simulation_timeseries.py @@ -0,0 +1,38 @@ +import warnings +from typing import Optional + +import pandas as pd + + +def set_simulation_line_shape_fallback(line_shape_fallback: str) -> str: + """ + Defines a Plotly line_shape to use if a vector is not thought to be a rate or a total. + """ + line_shape_fallback = line_shape_fallback.lower() + if line_shape_fallback in ("backfilled", "backfill"): + return "vh" + if line_shape_fallback in ["hv", "vh", "hvh", "vhv", "spline", "linear"]: + return line_shape_fallback + warnings.warn( + f"{line_shape_fallback}, is not a valid line_shape option, will use linear." + ) + return "linear" + + +def get_simulation_line_shape( + line_shape_fallback: str, vector: str, smry_meta: Optional[pd.DataFrame] = None +) -> str: + """Get simulation time series line shape, smry_meta is a pd.DataFrame on the format given + by `load_smry_meta` in `../_datainput/fmu_input.py`. + """ + + if smry_meta is None: + return line_shape_fallback + try: + if smry_meta.is_rate[vector]: + return "vh" + if smry_meta.is_total[vector]: + return "linear" + except (AttributeError, KeyError): + pass + return line_shape_fallback diff --git a/webviz_subsurface/plugins/_inplace_volumes.py b/webviz_subsurface/plugins/_inplace_volumes.py index 0041789698..b7f0d8b368 100644 --- a/webviz_subsurface/plugins/_inplace_volumes.py +++ b/webviz_subsurface/plugins/_inplace_volumes.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -import dash_table +from dash_table import DataTable import dash_html_components as html import dash_core_components as dcc from dash.dependencies import Input, Output @@ -14,7 +14,7 @@ from .._datainput.inplace_volumes import extract_volumes from .._abbreviations.volume_terminology import volume_description, volume_unit -from .._abbreviations.number_formatting import TABLE_STATISTICS_BASE +from .._abbreviations.number_formatting import table_statistics_base class InplaceVolumes(WebvizPluginABC): @@ -59,7 +59,7 @@ class InplaceVolumes(WebvizPluginABC): """ - TABLE_STATISTICS = [("Response", {}), ("Group", {})] + TABLE_STATISTICS_BASE + TABLE_STATISTICS = [("Group", {})] + table_statistics_base() def __init__( self, @@ -326,8 +326,9 @@ def layout(self): id=self.ids("layout"), children=[ html.Div( + style={"flex": 1}, children=[ - html.P("Filters:", style={"font-weight": "bold"}), + html.Span("Filters:", style={"font-weight": "bold"}), html.Div( id=self.ids("filters"), children=self.selector_dropdowns, @@ -335,6 +336,7 @@ def layout(self): ], ), html.Div( + style={"flex": 5}, children=[ self.plot_options_layout, html.Div( @@ -345,10 +347,20 @@ def layout(self): ), ], ), - dash_table.DataTable( - id=self.ids("table"), - columns=[ - {"name": i, "id": i} for i in InplaceVolumes.TABLE_STATISTICS + html.Div( + children=[ + html.Div( + id=self.ids("table_title"), + style={"textAlign": "center"}, + children="", + ), + DataTable( + id=self.ids("table"), + sort_action="native", + filter_action="native", + page_action="native", + page_size=10, + ), ], ), ] @@ -360,6 +372,7 @@ def set_callbacks(self, app): Output(self.ids("graph"), "figure"), Output(self.ids("table"), "data"), Output(self.ids("table"), "columns"), + Output(self.ids("table_title"), "children"), ], self.vol_callback_inputs, ) @@ -398,8 +411,11 @@ def _render_vol_chart(*args): plot_traces.append(trace) table.append(plot_table(dframe, response, name)) # Column specification - columns = table_columns(response) - # Else make a graph object + columns = [ + {**{"name": i[0], "id": i[0]}, **i[1]} + for i in InplaceVolumes.TABLE_STATISTICS + ] + # Make a graph object and return return ( { "data": plot_traces, @@ -407,6 +423,7 @@ def _render_vol_chart(*args): }, table, columns, + f"{volume_description(response)} [{volume_unit(response)}]", ) @app.callback( @@ -468,7 +485,6 @@ def plot_table(dframe, response, name): values = dframe[response] try: output = { - "Response": volume_description(response), "Group": str(name), "Minimum": values.min(), "Maximum": values.max(), @@ -483,19 +499,6 @@ def plot_table(dframe, response, name): return output -@CACHE.memoize(timeout=CACHE.TIMEOUT) -def table_columns(response): - columns = [ - {**{"name": i[0], "id": i[0]}, **i[1]} for i in InplaceVolumes.TABLE_STATISTICS - ] - for col in columns: - try: - col["format"]["locale"]["symbol"] = ["", f"{volume_unit(response)}"] - except KeyError: - pass - return columns - - @CACHE.memoize(timeout=CACHE.TIMEOUT) def plot_layout(plot_type, response, theme): layout = {} @@ -507,17 +510,27 @@ def plot_layout(plot_type, response, theme): "barmode": "overlay", "bargap": 0.01, "bargroupgap": 0.2, - "xaxis": {"title": volume_description(response)}, + "xaxis": { + "title": f"{volume_description(response)} [{volume_unit(response)}]" + }, "yaxis": {"title": "Count"}, } ) elif plot_type == "Box plot": - layout.update({"yaxis": {"title": volume_description(response)}}) + layout.update( + { + "yaxis": { + "title": f"{volume_description(response)} [{volume_unit(response)}]" + } + } + ) else: layout.update( { - "margin": {"l": 40, "r": 40, "b": 30, "t": 10}, - "yaxis": {"title": volume_description(response)}, + "margin": {"l": 60, "r": 40, "b": 30, "t": 10}, + "yaxis": { + "title": f"{volume_description(response)} [{volume_unit(response)}]" + }, "xaxis": {"title": "Realization"}, } ) diff --git a/webviz_subsurface/plugins/_inplace_volumes_onebyone.py b/webviz_subsurface/plugins/_inplace_volumes_onebyone.py index bcab4b2c4f..dfaf411a67 100644 --- a/webviz_subsurface/plugins/_inplace_volumes_onebyone.py +++ b/webviz_subsurface/plugins/_inplace_volumes_onebyone.py @@ -17,7 +17,7 @@ from .._datainput.inplace_volumes import extract_volumes from .._datainput.fmu_input import get_realizations, find_sens_type from .._abbreviations.volume_terminology import volume_description, volume_unit -from .._abbreviations.number_formatting import TABLE_STATISTICS_BASE +from .._abbreviations.number_formatting import table_statistics_base class InplaceVolumesOneByOne(WebvizPluginABC): @@ -73,7 +73,7 @@ class InplaceVolumesOneByOne(WebvizPluginABC): FILTERS = ["ZONE", "REGION", "FACIES", "LICENSE"] - TABLE_STATISTICS = [("Sens Name", {}), ("Sens Case", {})] + TABLE_STATISTICS_BASE + TABLE_STATISTICS = [("Sens Name", {}), ("Sens Case", {})] + table_statistics_base() ENSEMBLE_COLUMNS = [ "REAL", @@ -142,7 +142,7 @@ def __init__( self.tornadoplot = TornadoPlot(app, parameters, allow_click=True) self.uid = uuid4() self.selectors_id = {x: self.ids(x) for x in self.selectors} - self.plotly_theme = app.webviz_settings["theme"].plotly_theme + self.theme = app.webviz_settings["theme"] self.set_callbacks(app) def ids(self, element): @@ -361,28 +361,59 @@ def filter_selectors(self): def layout(self): """Main layout""" return html.Div( - id=self.ids("layout"), children=[ wcc.FlexBox( + id=self.ids("layout"), children=[ html.Div( - style={"flex": 1}, children=[ - self.selector("Ensemble", "ensemble", "ENSEMBLE"), - self.selector("Grid source", "source", "SOURCE"), - self.response_selector, - self.plot_selector, - html.Span("Filters:", style={"font-weight": "bold"}), + wcc.FlexBox( + children=[ + html.Div( + style={"flex": 1}, + children=[ + self.selector( + "Ensemble", "ensemble", "ENSEMBLE" + ), + self.selector( + "Grid source", "source", "SOURCE" + ), + self.response_selector, + self.plot_selector, + html.Span( + "Filters:", + style={"font-weight": "bold"}, + ), + html.Div( + id=self.ids("filters"), + children=self.filter_selectors, + ), + ], + ), + html.Div( + style={"flex": 3, "height": "600px"}, + id=self.ids("graph-wrapper"), + ), + ], + ), html.Div( - id=self.ids("filters"), - children=self.filter_selectors, + children=[ + html.Div( + id=self.ids("volume_title"), + style={"textAlign": "center"}, + children="", + ), + DataTable( + id=self.ids("table"), + sort_action="native", + filter_action="native", + page_action="native", + page_size=10, + ), + ], ), ], ), - html.Div( - id=self.ids("graph-wrapper"), - style={"flex": 3, "height": "600px"}, - ), html.Div( id=self.ids("tornado-wrapper"), style={"visibility": "visible", "flex": 2}, @@ -390,14 +421,7 @@ def layout(self): ), ], ), - DataTable( - id=self.ids("table"), - sort_action="native", - filter_action="native", - page_action="native", - page_size=10, - ), - ], + ] ) def set_callbacks(self, app): @@ -407,6 +431,7 @@ def set_callbacks(self, app): Output(self.tornadoplot.storage_id, "data"), Output(self.ids("table"), "data"), Output(self.ids("table"), "columns"), + Output(self.ids("volume_title"), "children"), ], [ Input(i, "value") @@ -433,13 +458,20 @@ def _render_vol_chart(plot_type, ensemble, response, source, *filters): # Calculate statistics for table table, columns = calculate_table(data, response) + # volume title: + volume_title = f"{volume_description(response)} [{volume_unit(response)}]" + # Make Plotly figure layout = {} - layout.update(self.plotly_theme["layout"]) layout.update({"margin": {"l": 100, "b": 100}}) if plot_type == "Per realization": # One bar per realization - layout.update({"xaxis": {"title": "Realizations"}}) + layout.update( + { + "xaxis": {"title": "Realizations"}, + "yaxis": {"title": volume_title}, + } + ) plot_data = data.groupby("REAL").sum().reset_index() figure = wcc.Graph( config={"displayModeBar": False}, @@ -454,12 +486,17 @@ def _render_vol_chart(plot_type, ensemble, response, source, *filters): "type": "bar", } ], - "layout": layout, + "layout": self.theme.create_themed_layout(layout), }, ) elif plot_type == "Per sensitivity case": # One box per sensitivity name - layout.update({"title": "Distribution for each sensitivity case"}) + layout.update( + { + "title": "Distribution for each sensitivity case", + "yaxis": {"title": volume_title}, + } + ) figure = wcc.Graph( config={"displayModeBar": False}, id=self.ids("graph"), @@ -482,7 +519,12 @@ def _render_vol_chart(plot_type, ensemble, response, source, *filters): ) elif plot_type == "Per sensitivity name": # One box per sensitivity name - layout.update({"title": "Distribution for each sensitivity name"}) + layout.update( + { + "title": "Distribution for each sensitivity name", + "yaxis": {"title": volume_title}, + } + ) figure = wcc.Graph( config={"displayModeBar": False}, id=self.ids("graph"), @@ -513,7 +555,7 @@ def _render_vol_chart(plot_type, ensemble, response, source, *filters): } ) - return figure, tornado, table, columns + return figure, tornado, table, columns, volume_title @app.callback( Output(self.ids("graph"), "figure"), @@ -561,11 +603,6 @@ def calculate_table(df, response): {**{"name": i[0], "id": i[0]}, **i[1]} for i in InplaceVolumesOneByOne.TABLE_STATISTICS ] - for col in columns: - try: - col["format"]["locale"]["symbol"] = ["", f"{volume_unit(response)}"] - except KeyError: - pass return table, columns diff --git a/webviz_subsurface/plugins/_parameter_response_correlation.py b/webviz_subsurface/plugins/_parameter_response_correlation.py index e0995ba945..a0f68d1562 100644 --- a/webviz_subsurface/plugins/_parameter_response_correlation.py +++ b/webviz_subsurface/plugins/_parameter_response_correlation.py @@ -372,8 +372,7 @@ def _update_correlation_graph(ensemble, response, *filters): ) parameterdf = self.parameterdf.loc[self.parameterdf["ENSEMBLE"] == ensemble] df = pd.merge(responsedf, parameterdf, on=["REAL"]) - corrdf = correlate(df, method=self.corr_method) - corrdf = corrdf.reindex(corrdf[response].abs().sort_values().index) + corrdf = correlate(df, response=response, method=self.corr_method) try: corr_response = ( corrdf[response].dropna().drop(["REAL", response], axis=0) @@ -505,21 +504,23 @@ def _filter_and_sum_responses( @CACHE.memoize(timeout=CACHE.TIMEOUT) -def correlate(inputdf, method="pearson"): +def correlate(inputdf, response, method="pearson"): """Cached wrapper for _correlate""" - return _correlate(inputdf=inputdf, method=method) + return _correlate(inputdf=inputdf, response=response, method=method) -def _correlate(inputdf, method="pearson"): +def _correlate(inputdf, response, method="pearson"): """Returns the correlation matrix for a dataframe""" if method == "pearson": - return inputdf.corr(method=method) - if method == "spearman": - return inputdf.rank().corr(method="pearson") - raise ValueError( - f"Correlation method {method} is invalid. " - "Available methods are 'pearson' and 'spearman'" - ) + corrdf = inputdf.corr(method=method) + elif method == "spearman": + corrdf = inputdf.rank().corr(method="pearson") + else: + raise ValueError( + f"Correlation method {method} is invalid. " + "Available methods are 'pearson' and 'spearman'" + ) + return corrdf.reindex(corrdf[response].abs().sort_values().index) def make_correlation_plot(series, response, theme, corr_method): diff --git a/webviz_subsurface/plugins/_reservoir_simulation_timeseries.py b/webviz_subsurface/plugins/_reservoir_simulation_timeseries.py index ba1fb8db78..25ffbede7c 100644 --- a/webviz_subsurface/plugins/_reservoir_simulation_timeseries.py +++ b/webviz_subsurface/plugins/_reservoir_simulation_timeseries.py @@ -1,8 +1,7 @@ -from uuid import uuid4 from pathlib import Path import json -import yaml +import yaml import pandas as pd from plotly.subplots import make_subplots from dash.exceptions import PreventUpdate @@ -14,33 +13,16 @@ from webviz_config.webviz_store import webvizstore from webviz_config.common_cache import CACHE -from .._datainput.fmu_input import load_smry -from .._abbreviations.reservoir_simulation import simulation_vector_description - - -def _historical_vector(vector, return_historical=True): - """This is a unnecessary complex function, trying to make a best guess - on converting between historical and non-historical vector names, while - waiting for a robust/correct solution, e.g. something like - https://github.com/equinor/fmu-ensemble/issues/87 - - If return_historical is `True`, the corresponding guessed historical vector name - is returned. If `False` the corresponding non-historical vector name is returned, - but in this case if the input vector is not believed to be a historical vector, - None is returned. - """ - - parts = vector.split(":", 1) - - if return_historical: - parts[0] += "H" - return ":".join(parts) - - if not parts[0].endswith("H"): - return None - - parts[0] = parts[0][:-1] - return ":".join(parts) +from .._datainput.fmu_input import load_smry, load_smry_meta +from .._abbreviations.reservoir_simulation import ( + simulation_vector_description, + simulation_unit_reformat, + historical_vector, +) +from .._utils.simulation_timeseries import ( + set_simulation_line_shape_fallback, + get_simulation_line_shape, +) class ReservoirSimulationTimeSeries(WebvizPluginABC): @@ -58,6 +40,11 @@ class ReservoirSimulationTimeSeries(WebvizPluginABC): * `sampling`: Time separation between extracted values. Can be e.g. `monthly` or `yearly`. * `options`: Options to initialize plots with. See below +* `line_shape_fallback`: Fallback interpolation method between points. Vectors identified as rates + or phase ratios are always backfilled, vectors identified as cumulative (totals) + are always linearly interpolated. The rest use the fallback. + Supported: `linear` (default), `backfilled` + regular Plotly options: `hv`, `vh`, + `hvh`, `vhv` and `spline`. Plot options: * `vector1` : First vector to display @@ -78,6 +65,7 @@ def __init__( column_keys: list = None, sampling: str = "monthly", options: dict = None, + line_shape_fallback: str = "linear", ): super().__init__() @@ -98,6 +86,7 @@ def __init__( ) if csvfile: self.smry = read_csv(csvfile) + self.smry_meta = None elif ensembles: self.ens_paths = { ensemble: app.webviz_settings["shared_settings"]["scratch_ensembles"][ @@ -111,6 +100,11 @@ def __init__( time_index=self.time_index, column_keys=self.column_keys, ) + self.smry_meta = load_smry_meta( + ensemble_paths=self.ens_paths, + ensemble_set_name="EnsembleSet", + column_keys=self.column_keys, + ) else: raise ValueError( 'Incorrent arguments. Either provide a "csvfile" or "ensembles"' @@ -120,7 +114,7 @@ def __init__( c for c in self.smry.columns if c not in ReservoirSimulationTimeSeries.ENSEMBLE_COLUMNS - and not _historical_vector(c, False) in self.smry.columns + and not historical_vector(c, self.smry_meta, False) in self.smry.columns ] self.dropdown_options = [ @@ -129,13 +123,16 @@ def __init__( ] self.ensembles = list(self.smry["ENSEMBLE"].unique()) - self.plotly_theme = app.webviz_settings["theme"].plotly_theme + self.theme = app.webviz_settings["theme"] self.plot_options = options if options else {} self.plot_options["date"] = ( str(self.plot_options.get("date")) if self.plot_options.get("date") else None ) + self.line_shape_fallback = set_simulation_line_shape_fallback( + line_shape_fallback + ) # Check if initially plotted vectors exist in data, raise ValueError if not. missing_vectors = [ value @@ -150,19 +147,14 @@ def __init__( "file." ) self.allow_delta = len(self.ensembles) > 1 - self.uid = uuid4() self.set_callbacks(app) - def ids(self, element): - """Generate unique id for dom element""" - return f"{element}-id-{self.uid}" - @property def ens_colors(self): try: - colors = self.plotly_theme["layout"]["colorway"] + colors = self.theme.plotly_theme["layout"]["colorway"] except KeyError: - colors = self.plotly_theme.get( + colors = self.theme.plotly_theme.get( "colorway", [ "#243746", @@ -191,32 +183,32 @@ def ens_colors(self): def tour_steps(self): return [ { - "id": self.ids("layout"), + "id": self.uuid("layout"), "content": "Dashboard displaying reservoir simulation time series.", }, { - "id": self.ids("graph"), + "id": self.uuid("graph"), "content": ( "Visualization of selected time series. " "Different options can be set in the menu to the left." ), }, { - "id": self.ids("ensemble"), + "id": self.uuid("ensemble"), "content": ( "Display time series from one or several ensembles. " "Different ensembles will be overlain in the same plot." ), }, { - "id": self.ids("vectors"), + "id": self.uuid("vectors"), "content": ( "Display up to three different time series. " "Each time series will be visualized in a separate plot." ), }, { - "id": self.ids("visualization"), + "id": self.uuid("visualization"), "content": ( "Choose between different visualizations. 1. Show time series as " "individual lines per realization. 2. Show statistical fanchart per " @@ -247,7 +239,7 @@ def delta_layout(self): children=[ html.Span("Mode:", style={"font-weight": "bold"}), dcc.RadioItems( - id=self.ids("mode"), + id=self.uuid("mode"), style={"marginBottom": "25px"}, options=[ { @@ -265,14 +257,14 @@ def delta_layout(self): ), ), html.Div( - id=self.ids("show_ensembles"), + id=self.uuid("show_ensembles"), children=html.Label( children=[ html.Span( "Selected ensembles:", style={"font-weight": "bold"} ), dcc.Dropdown( - id=self.ids("ensemble"), + id=self.uuid("ensemble"), clearable=False, multi=True, options=[ @@ -284,7 +276,7 @@ def delta_layout(self): ), ), html.Div( - id=self.ids("calc_delta"), + id=self.uuid("calc_delta"), style={"display": "none"}, children=[ html.Span( @@ -301,7 +293,7 @@ def delta_layout(self): children="Ensemble A", ), dcc.Dropdown( - id=self.ids("base_ens"), + id=self.uuid("base_ens"), clearable=False, options=[ {"label": i, "value": i} @@ -318,7 +310,7 @@ def delta_layout(self): children="Ensemble B", ), dcc.Dropdown( - id=self.ids("delta_ens"), + id=self.uuid("delta_ens"), clearable=False, options=[ {"label": i, "value": i} @@ -338,14 +330,14 @@ def delta_layout(self): @property def layout(self): return wcc.FlexBox( - id=self.ids("layout"), + id=self.uuid("layout"), children=[ html.Div( style={"flex": 1}, children=[ self.delta_layout, html.Div( - id=self.ids("vectors"), + id=self.uuid("vectors"), style={"marginTop": "25px"}, children=[ html.Span( @@ -358,7 +350,7 @@ def layout(self): "fontSize": ".95em", }, optionHeight=55, - id=self.ids("vector1"), + id=self.uuid("vector1"), clearable=False, multi=False, options=self.dropdown_options, @@ -369,7 +361,7 @@ def layout(self): dcc.Dropdown( style={"marginBottom": "5px", "fontSize": ".95em"}, optionHeight=55, - id=self.ids("vector2"), + id=self.uuid("vector2"), clearable=True, multi=False, placeholder="Add additional series", @@ -379,7 +371,7 @@ def layout(self): dcc.Dropdown( style={"fontSize": ".95em"}, optionHeight=55, - id=self.ids("vector3"), + id=self.uuid("vector3"), clearable=True, multi=False, placeholder="Add additional series", @@ -389,14 +381,14 @@ def layout(self): ], ), html.Div( - id=self.ids("visualization"), + id=self.uuid("visualization"), style={"marginTop": "25px"}, children=[ html.Span( "Visualization:", style={"font-weight": "bold"} ), dcc.RadioItems( - id=self.ids("statistics"), + id=self.uuid("statistics"), options=[ { "label": "Individual realizations", @@ -424,10 +416,10 @@ def layout(self): children=[ html.Div( style={"height": "300px"}, - children=wcc.Graph(id=self.ids("graph"),), + children=wcc.Graph(id=self.uuid("graph"),), ), dcc.Store( - id=self.ids("date"), + id=self.uuid("date"), data=json.dumps(self.plot_options.get("date", None)), ), ], @@ -438,17 +430,17 @@ def layout(self): # pylint: disable=too-many-statements def set_callbacks(self, app): @app.callback( - Output(self.ids("graph"), "figure"), + Output(self.uuid("graph"), "figure"), [ - Input(self.ids("vector1"), "value"), - Input(self.ids("vector2"), "value"), - Input(self.ids("vector3"), "value"), - Input(self.ids("ensemble"), "value"), - Input(self.ids("mode"), "value"), - Input(self.ids("base_ens"), "value"), - Input(self.ids("delta_ens"), "value"), - Input(self.ids("statistics"), "value"), - Input(self.ids("date"), "data"), + Input(self.uuid("vector1"), "value"), + Input(self.uuid("vector2"), "value"), + Input(self.uuid("vector3"), "value"), + Input(self.uuid("ensemble"), "value"), + Input(self.uuid("mode"), "value"), + Input(self.uuid("base_ens"), "value"), + Input(self.uuid("delta_ens"), "value"), + Input(self.uuid("statistics"), "value"), + Input(self.uuid("date"), "data"), ], ) # pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals, too-many-branches @@ -481,7 +473,13 @@ def _update_graph( # Titles for subplots titles = [] for vect in vectors: - titles.append(simulation_vector_description(vect)) + if self.smry_meta is None: + titles.append(simulation_vector_description(vect)) + else: + titles.append( + f"{simulation_vector_description(vect)}" + f" [{simulation_unit_reformat(self.smry_meta.unit[vect])}]" + ) if visualization == "statistics_hist": titles.append(date) @@ -497,22 +495,45 @@ def _update_graph( # Loop through each vector and calculate relevant plot legends = [] for i, vector in enumerate(vectors): + line_shape = get_simulation_line_shape( + line_shape_fallback=self.line_shape_fallback, + vector=vector, + smry_meta=self.smry_meta, + ) if calc_mode == "ensembles": - data = filter_df(self.smry, ensembles, vector) + data = filter_df(self.smry, ensembles, vector, self.smry_meta) elif calc_mode == "delta_ensembles": - data = filter_df(self.smry, [base_ens, delta_ens], vector) + data = filter_df( + self.smry, [base_ens, delta_ens], vector, self.smry_meta + ) data = calculate_delta(data, base_ens, delta_ens) else: raise PreventUpdate if visualization == "statistics": - traces = add_statistic_traces(data, vector, colors=self.ens_colors) + traces = add_statistic_traces( + data, + vector, + colors=self.ens_colors, + line_shape=line_shape, + smry_meta=self.smry_meta, + ) elif visualization == "realizations": traces = add_realization_traces( - data, vector, colors=self.ens_colors + data, + vector, + colors=self.ens_colors, + line_shape=line_shape, + smry_meta=self.smry_meta, ) elif visualization == "statistics_hist": - traces = add_statistic_traces(data, vector, colors=self.ens_colors) + traces = add_statistic_traces( + data, + vector, + colors=self.ens_colors, + line_shape=line_shape, + smry_meta=self.smry_meta, + ) histdata = add_histogram_traces( data, vector, date=date, colors=self.ens_colors ) @@ -529,14 +550,12 @@ def _update_graph( else: legends.append(trace.get("legendgroup")) fig.add_trace(trace, i + 1, 1) - # Add observations if calc_mode != "delta_ensembles" and self.observations.get(vector): for trace in add_observation_trace(self.observations.get(vector)): fig.add_trace(trace, i + 1, 1) - # Add additional styling to layout - fig["layout"].update(self.plotly_theme["layout"]) + fig = fig.to_dict() fig["layout"].update( height=800, margin={"t": 20, "b": 0}, @@ -544,6 +563,7 @@ def _update_graph( bargap=0.01, bargroupgap=0.2, ) + fig["layout"] = self.theme.create_themed_layout(fig["layout"]) if visualization == "statistics_hist": # Remove linked x-axis for histograms @@ -560,10 +580,10 @@ def _update_graph( @app.callback( [ - Output(self.ids("show_ensembles"), "style"), - Output(self.ids("calc_delta"), "style"), + Output(self.uuid("show_ensembles"), "style"), + Output(self.uuid("calc_delta"), "style"), ], - [Input(self.ids("mode"), "value")], + [Input(self.uuid("mode"), "value")], ) def _update_mode(mode): """Switch displayed ensemble selector for delta/no-delta""" @@ -574,9 +594,9 @@ def _update_mode(mode): return style @app.callback( - Output(self.ids("date"), "data"), - [Input(self.ids("graph"), "clickData")], - [State(self.ids("date"), "data")], + Output(self.uuid("date"), "data"), + [Input(self.uuid("graph"), "clickData")], + [State(self.uuid("date"), "data")], ) def _update_date(clickdata, date): """Store clicked date for use in other callback""" @@ -601,6 +621,18 @@ def add_webvizstore(self): ], ) ) + functions.append( + ( + load_smry_meta, + [ + { + "ensemble_paths": self.ens_paths, + "ensemble_set_name": "EnsembleSet", + "column_keys": self.column_keys, + } + ], + ) + ) if self.obsfile: functions.append((get_path, [{"path": self.obsfile}])) return functions @@ -614,12 +646,12 @@ def format_observations(obslist): @CACHE.memoize(timeout=CACHE.TIMEOUT) -def filter_df(df, ensembles, vector): +def filter_df(df, ensembles, vector, smry_meta): """Filter dataframe for current vector. Include history vector if present""" columns = ["REAL", "ENSEMBLE", vector, "DATE"] - if _historical_vector(vector) in df.columns: - columns.append(_historical_vector(vector)) + if historical_vector(vector=vector, smry_meta=smry_meta) in df.columns: + columns.append(historical_vector(vector=vector, smry_meta=smry_meta)) return df.loc[df["ENSEMBLE"].isin(ensembles)][columns] @@ -684,11 +716,11 @@ def add_observation_trace(obs): @CACHE.memoize(timeout=CACHE.TIMEOUT) -def add_realization_traces(dframe, vector, colors): +def add_realization_traces(dframe, vector, colors, line_shape, smry_meta): """Renders line trace for each realization, includes history line if present""" traces = [ { - # "type": "linegl", + "line": {"shape": line_shape}, "x": list(real_df["DATE"]), "y": list(real_df[vector]), "hovertext": f"Realization: {real_no}, Ensemble: {ensemble}", @@ -701,18 +733,25 @@ def add_realization_traces(dframe, vector, colors): for real_no, (real, real_df) in enumerate(ens_df.groupby("REAL")) ] - if _historical_vector(vector) in dframe.columns: - traces.append(add_history_trace(dframe, _historical_vector(vector))) + if historical_vector(vector=vector, smry_meta=smry_meta) in dframe.columns: + traces.append( + add_history_trace( + dframe, + historical_vector(vector=vector, smry_meta=smry_meta), + line_shape, + ) + ) return traces -def add_history_trace(dframe, vector): +def add_history_trace(dframe, vector, line_shape): """Renders the history line""" df = dframe.loc[ (dframe["REAL"] == dframe["REAL"].unique()[0]) & (dframe["ENSEMBLE"] == dframe["ENSEMBLE"].unique()[0]) ] return { + "line": {"shape": line_shape}, "x": df["DATE"], "y": df[vector], "hovertext": "History", @@ -723,7 +762,8 @@ def add_history_trace(dframe, vector): } -def add_statistic_traces(df, vector, colors): +@CACHE.memoize(timeout=CACHE.TIMEOUT) +def add_statistic_traces(df, vector, colors, line_shape, smry_meta): """Calculate statistics for a given vector for relevant ensembles""" quantiles = [10, 90] traces = [] @@ -744,14 +784,19 @@ def add_statistic_traces(df, vector, colors): pd.concat(dframes, names=["STATISTIC"], sort=False)[vector], colors.get(ensemble, colors[list(colors.keys())[0]]), ensemble, + line_shape, + ) + ) + if historical_vector(vector=vector, smry_meta=smry_meta) in df.columns: + traces.append( + add_history_trace( + df, historical_vector(vector=vector, smry_meta=smry_meta), line_shape, ) ) - if _historical_vector(vector) in df.columns: - traces.append(add_history_trace(df, _historical_vector(vector))) return traces -def add_fanchart_traces(vector_stats, color, legend_group: str): +def add_fanchart_traces(vector_stats, color, legend_group: str, line_shape): """Renders a fanchart for an ensemble vector""" fill_color = hex_to_rgb(color, 0.3) @@ -763,7 +808,7 @@ def add_fanchart_traces(vector_stats, color, legend_group: str): "x": vector_stats["maximum"].index.tolist(), "y": vector_stats["maximum"].values, "mode": "lines", - "line": {"width": 0, "color": line_color}, + "line": {"width": 0, "color": line_color, "shape": line_shape}, "legendgroup": legend_group, "showlegend": False, }, @@ -775,7 +820,7 @@ def add_fanchart_traces(vector_stats, color, legend_group: str): "mode": "lines", "fill": "tonexty", "fillcolor": fill_color, - "line": {"width": 0, "color": line_color}, + "line": {"width": 0, "color": line_color, "shape": line_shape}, "legendgroup": legend_group, "showlegend": False, }, @@ -787,7 +832,7 @@ def add_fanchart_traces(vector_stats, color, legend_group: str): "mode": "lines", "fill": "tonexty", "fillcolor": fill_color, - "line": {"color": line_color}, + "line": {"color": line_color, "shape": line_shape}, "legendgroup": legend_group, "showlegend": True, }, @@ -799,7 +844,7 @@ def add_fanchart_traces(vector_stats, color, legend_group: str): "mode": "lines", "fill": "tonexty", "fillcolor": fill_color, - "line": {"width": 0, "color": line_color}, + "line": {"width": 0, "color": line_color, "shape": line_shape}, "legendgroup": legend_group, "showlegend": False, }, @@ -811,7 +856,7 @@ def add_fanchart_traces(vector_stats, color, legend_group: str): "mode": "lines", "fill": "tonexty", "fillcolor": fill_color, - "line": {"width": 0, "color": line_color}, + "line": {"width": 0, "color": line_color, "shape": line_shape}, "legendgroup": legend_group, "showlegend": False, }, diff --git a/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py b/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py index a88ad1e0e3..06c67f6729 100644 --- a/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py +++ b/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py @@ -16,9 +16,21 @@ from webviz_config.webviz_store import webvizstore from .._private_plugins.tornado_plot import TornadoPlot -from .._datainput.fmu_input import load_smry, get_realizations, find_sens_type -from .._abbreviations.reservoir_simulation import simulation_vector_description -from .._abbreviations.number_formatting import TABLE_STATISTICS_BASE +from .._datainput.fmu_input import ( + load_smry, + get_realizations, + find_sens_type, + load_smry_meta, +) +from .._abbreviations.reservoir_simulation import ( + simulation_vector_description, + simulation_unit_reformat, +) +from .._abbreviations.number_formatting import table_statistics_base +from .._utils.simulation_timeseries import ( + set_simulation_line_shape_fallback, + get_simulation_line_shape, +) # pylint: disable=too-many-instance-attributes class ReservoirSimulationTimeSeriesOneByOne(WebvizPluginABC): @@ -55,6 +67,11 @@ class ReservoirSimulationTimeSeriesOneByOne(WebvizPluginABC): * `initial_vector`: Initial vector to display * `sampling`: Time separation between extracted values. Can be e.g. `monthly` or `yearly`. +* `line_shape_fallback`: Fallback interpolation method between points. Vectors identified as rates + or phase ratios are always backfilled, vectors identified as cumulative (totals) + are always linearly interpolated. The rest use the fallback. + Supported: `linear` (default), `backfilled` + regular Plotly options: `hv`, `vh`, + `hvh`, `vhv` and `spline`. """ ENSEMBLE_COLUMNS = [ @@ -67,7 +84,7 @@ class ReservoirSimulationTimeSeriesOneByOne(WebvizPluginABC): "RUNPATH", ] - TABLE_STAT = [("Sensitivity", {}), ("Case", {})] + TABLE_STATISTICS_BASE + TABLE_STAT = [("Sensitivity", {}), ("Case", {})] + table_statistics_base() # pylint: disable=too-many-arguments def __init__( @@ -79,6 +96,7 @@ def __init__( column_keys: list = None, initial_vector=None, sampling: str = "monthly", + line_shape_fallback: str = "linear", ): super().__init__() @@ -99,6 +117,7 @@ def __init__( parameters["SENSTYPE"] = parameters.apply( lambda row: find_sens_type(row.SENSCASE), axis=1 ) + self.smry_meta = None elif ensembles: self.ens_paths = { @@ -117,6 +136,11 @@ def __init__( time_index=self.time_index, column_keys=self.column_keys, ) + self.smry_meta = load_smry_meta( + ensemble_paths=self.ens_paths, + ensemble_set_name="EnsembleSet", + column_keys=self.column_keys, + ) else: raise ValueError( 'Incorrent arguments. Either provide a "csvfile_smry" and "csvfile_parameters" or ' @@ -134,9 +158,12 @@ def __init__( if initial_vector and initial_vector in self.smry_cols else self.smry_cols[0] ) + self.line_shape_fallback = set_simulation_line_shape_fallback( + line_shape_fallback + ) self.tornadoplot = TornadoPlot(app, parameters, allow_click=True) self.uid = uuid4() - self.plotly_theme = app.webviz_settings["theme"].plotly_theme + self.theme = app.webviz_settings["theme"] self.set_callbacks(app) def ids(self, element): @@ -246,6 +273,16 @@ def add_webvizstore(self): } ], ), + ( + load_smry_meta, + [ + { + "ensemble_paths": self.ens_paths, + "ensemble_set_name": "EnsembleSet", + "column_keys": self.column_keys, + } + ], + ), ( get_realizations, [ @@ -275,8 +312,8 @@ def layout(self): dcc.Store(id=self.ids("date-store")), ], ), - html.Div( - [ + wcc.FlexBox( + children=[ html.Div( id=self.ids("graph-wrapper"), style={"height": "450px"}, @@ -289,6 +326,22 @@ def layout(self): ), ] ), + html.Div( + children=[ + html.Div( + id=self.ids("table_title"), + style={"textAlign": "center"}, + children="", + ), + DataTable( + id=self.ids("table"), + sort_action="native", + filter_action="native", + page_action="native", + page_size=10, + ), + ], + ), ], ), html.Div( @@ -298,22 +351,17 @@ def layout(self): ), ], ), - DataTable( - id=self.ids("table"), - sort_action="native", - filter_action="native", - page_action="native", - page_size=10, - ), ] ) + # pylint: disable=too-many-statements def set_callbacks(self, app): @app.callback( [ # Output(self.ids("date-store"), "children"), Output(self.ids("table"), "data"), Output(self.ids("table"), "columns"), + Output(self.ids("table_title"), "children"), Output(self.tornadoplot.storage_id, "data"), ], [ @@ -336,13 +384,24 @@ def _render_date(ensemble, clickdata, vector): # json.dumps(f"{date}"), table_rows, table_columns, + ( + f"{simulation_vector_description(vector)} ({vector})" + + ( + "" + if get_unit(self.smry_meta, vector) is None + else f" [{get_unit(self.smry_meta, vector)}]" + ) + ), json.dumps( { "ENSEMBLE": ensemble, "data": data[["REAL", vector]].values.tolist(), "number_format": "#.4g", - # Unit placeholder. Need data from fmu-ensemble, see work in: - # https://github.com/equinor/fmu-ensemble/pull/89 + "unit": ( + "" + if get_unit(self.smry_meta, vector) is None + else get_unit(self.smry_meta, vector) + ), } ), ) @@ -357,7 +416,7 @@ def _render_date(ensemble, clickdata, vector): Input(self.ids("graph"), "clickData"), ], [State(self.ids("graph"), "figure")], - ) + ) # pylint: disable=too-many-branches def _render_tornado(tornado_click, ensemble, vector, date_click, figure): """Update graph with line coloring, vertical line and title""" if dash.callback_context.triggered is None: @@ -366,8 +425,6 @@ def _render_tornado(tornado_click, ensemble, vector, date_click, figure): # Draw initial figure and redraw if ensemble/vector changes if ctx in ["", self.ids("ensemble"), self.ids("vector")]: - layout = {} - layout.update(self.plotly_theme["layout"]) data = filter_ensemble(self.data, ensemble, vector) traces = [ { @@ -377,12 +434,21 @@ def _render_tornado(tornado_click, ensemble, vector, date_click, figure): "x": df["DATE"], "y": df[vector], "customdata": r, + "line": { + "shape": get_simulation_line_shape( + line_shape_fallback=self.line_shape_fallback, + vector=vector, + smry_meta=self.smry_meta, + ) + }, } for r, df in data.groupby(["REAL"]) ] traces[0]["hoverinfo"] = "x" - layout.update({"showlegend": False, "margin": {"t": 50}}) - figure = {"data": traces, "layout": layout} + figure = { + "data": traces, + "layout": {"showlegend": False, "margin": {"t": 50}}, + } # Update line colors if a sensitivity is selected in tornado if tornado_click: @@ -395,12 +461,16 @@ def _render_tornado(tornado_click, ensemble, vector, date_click, figure): for trace in figure["data"]: if trace["customdata"] in tornado_click["real_low"]: trace["marker"] = { - "color": self.plotly_theme["layout"]["colorway"][0] + "color": self.theme.plotly_theme["layout"]["colorway"][ + 0 + ] } trace["opacity"] = 1 elif trace["customdata"] in tornado_click["real_high"]: trace["marker"] = { - "color": self.plotly_theme["layout"]["colorway"][1] + "color": self.theme.plotly_theme["layout"]["colorway"][ + 1 + ] } trace["opacity"] = 1 else: @@ -415,7 +485,11 @@ def _render_tornado(tornado_click, ensemble, vector, date_click, figure): and figure["layout"].get("shapes") ): figure["layout"]["shapes"] = [] - figure["layout"]["title"] = None + figure["layout"]["title"] = ( + None + if get_unit(self.smry_meta, vector) is None + else f"[{get_unit(self.smry_meta, vector)}]" + ) return figure date = date_click["points"][0]["x"] @@ -426,9 +500,17 @@ def _render_tornado(tornado_click, ensemble, vector, date_click, figure): ] figure["layout"]["title"] = ( f"Date: {date}, " - f"sensitivity: {tornado_click['sens_name'] if tornado_click else None}" + f"Sensitivity: {tornado_click['sens_name'] if tornado_click else None}" ) - + figure["layout"]["yaxis"] = { + "title": f"{simulation_vector_description(vector)} ({vector})" + + ( + "" + if get_unit(self.smry_meta, vector) is None + else f" [{get_unit(self.smry_meta, vector)}]" + ) + } + figure["layout"] = self.theme.create_themed_layout(figure["layout"]) return figure @@ -452,16 +534,10 @@ def calculate_table(df, vector): ) except KeyError: pass - unit = "" # awaiting fmu-ensemble https://github.com/equinor/fmu-ensemble/pull/89 columns = [ {**{"name": i[0], "id": i[0]}, **i[1]} for i in ReservoirSimulationTimeSeriesOneByOne.TABLE_STAT ] - for col in columns: - try: - col["format"]["locale"]["symbol"] = ["", f"{unit}"] - except KeyError: - pass return table, columns @@ -476,3 +552,8 @@ def filter_ensemble(data, ensemble, vector): @webvizstore def read_csv(csv_file) -> pd.DataFrame: return pd.read_csv(csv_file, index_col=None) + + +@CACHE.memoize(timeout=CACHE.TIMEOUT) +def get_unit(smry_meta, vec): + return None if smry_meta is None else simulation_unit_reformat(smry_meta.unit[vec])