From fa2f75867e0623c15e3ceaae6f6991fb763a5609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Fri, 5 May 2023 08:11:13 +0200 Subject: [PATCH 01/19] Initial commit --- setup.py | 1 + .../__init__.py | 1 + .../_plugin.py | 163 ++++++++++++++++++ .../_views/__init__.py | 0 .../_views/_onebyone_view/__init__.py | 1 + .../_onebyone_view/_settings/__init__.py | 1 + .../_settings/_general_settings.py | 0 .../_onebyone_view/_settings/_selections.py | 84 +++++++++ .../_settings/_sensitivity_filter.py | 0 .../_settings/_vizualisation.py | 0 .../_views/_onebyone_view/_utils.py | 13 ++ .../_views/_onebyone_view/_view.py | 46 +++++ 12 files changed, 310 insertions(+) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/__init__.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py diff --git a/setup.py b/setup.py index e912f5ae3..8244555e2 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ "SegyViewer = webviz_subsurface.plugins._segy_viewer:SegyViewer", "SeismicMisfit = webviz_subsurface.plugins._seismic_misfit:SeismicMisfit", "SimulationTimeSeries = webviz_subsurface.plugins._simulation_time_series:SimulationTimeSeries", + "SimulationTimeSeriesOneByOne = webviz_subsurface.plugins._simulation_time_series_onebyone:SimulationTimeSeriesOneByOne", "StructuralUncertainty = webviz_subsurface.plugins._structural_uncertainty:StructuralUncertainty", "SubsurfaceMap = webviz_subsurface.plugins._subsurface_map:SubsurfaceMap", "SurfaceViewerFMU = webviz_subsurface.plugins._surface_viewer_fmu:SurfaceViewerFMU", diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py new file mode 100644 index 000000000..4e6153728 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py @@ -0,0 +1 @@ +from ._plugin import SimulationTimeSeriesOneByOne \ No newline at end of file diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py new file mode 100644 index 000000000..d8854e40e --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py @@ -0,0 +1,163 @@ +from typing import Dict + +import pandas as pd +from webviz_config import WebvizPluginABC, WebvizSettings +from webviz_config.utils import StrEnum + +from webviz_subsurface._models.parameter_model import ParametersModel +from webviz_subsurface._providers import ( + EnsembleSummaryProviderFactory, + EnsembleTableProviderFactory, + EnsembleTableProvider, + Frequency, +) + +from ._views._onebyone_view import OneByOneView +# from ._business_logic import SimulationTimeSeriesOneByOneDataModel +# from ._callbacks import plugin_callbacks +# from ._layout import main_view +# from .models import ProviderTimeSeriesDataModel + + +class SimulationTimeSeriesOneByOne(WebvizPluginABC): + """Visualizes reservoir simulation time series data for sensitivity studies based \ +on a design matrix. +A tornado plot can be calculated interactively for each date/vector by selecting a date. +After selecting a date individual sensitivities can be selected to highlight the realizations +run with that sensitivity. +--- +**Using simulation time series data directly from `UNSMRY` files** +* **`ensembles`:** Which ensembles in `shared_settings` to visualize. +* **`column_keys`:** List of vectors to extract. If not given, all vectors \ + from the simulations will be extracted. Wild card asterisk `*` can be used. +* **`sampling`:** Time separation between extracted values. Can be e.g. `monthly` (default) or \ + `yearly`. +**Common optional settings for both input options** +* **`initial_vector`:** Initial vector to display +* **`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 options: + * `linear` (default) + * `backfilled` + * `hv`, `vh`, `hvh`, `vhv` and `spline` (regular Plotly options). +**Using simulation time series data directly from `.UNSMRY` files** +Time series data are extracted automatically from the `UNSMRY` files in the individual +realizations, using the `fmu-ensemble` library. The `SENSNAME` and `SENSCASE` values are read +directly from the `parameters.txt` files of the individual realizations, assuming that these +exist. If the `SENSCASE` of a realization is `p10_p90`, the sensitivity case is regarded as a +**Monte Carlo** style sensitivity, otherwise the case is evaluated as a **scalar** sensitivity. +?> Using the `UNSMRY` method will also extract metadata like units, and whether the vector is a \ +rate, a cumulative, or historical. Units are e.g. added to the plot titles, while rates and \ +cumulatives are used to decide the line shapes in the plot. +""" + class Ids(StrEnum): + ONEBYONE_VIEW = "onebyone-view" + + def __init__( + self, + webviz_settings: WebvizSettings, + ensembles: list, + time_index: str = "monthly", + rel_file_pattern: str = "share/results/unsmry/*.arrow", + column_keys: list = None, + initial_vector: str = None, + line_shape_fallback: str = "linear", + ) -> None: + + super().__init__() + + # vectormodel: ProviderTimeSeriesDataModel + table_provider = EnsembleTableProviderFactory.instance() + provider_factory = EnsembleSummaryProviderFactory.instance() + resampling_frequency = Frequency(time_index) + + ensemble_paths = { + ensemble_name: webviz_settings.shared_settings["scratch_ensembles"][ + ensemble_name + ] + for ensemble_name in ensembles + } + try: + provider_set = { + ens: provider_factory.create_from_arrow_unsmry_presampled( + str(ens_path), rel_file_pattern, resampling_frequency + ) + for ens, ens_path in ensemble_paths.items() + } + # vectormodel = ProviderTimeSeriesDataModel( + # provider_set=provider_set, + # column_keys=column_keys, + # line_shape_fallback=line_shape_fallback, + # ) + except ValueError as error: + message = ( + f"Some/all ensembles are missing arrow files at {rel_file_pattern}.\n" + "If no arrow files have been generated with `ERT` using `ECL2CSV`, " + "the commandline tool `smry2arrow_batch` can be used to generate arrow " + "files for an ensemble" + ) + raise ValueError(message) from error + + parameterproviderset = { + ens_name: table_provider.create_from_per_realization_parameter_file( + ens_path + ) + for ens_name, ens_path in ensemble_paths.items() + } + parameter_df = create_df_from_table_provider(parameterproviderset) + + parametermodel = ParametersModel(dataframe=parameter_df, drop_constants=True) + # self.datamodel = SimulationTimeSeriesOneByOneDataModel( + # vectormodel=vectormodel, + # parametermodel=parametermodel, + # webviz_settings=webviz_settings, + # initial_vector=initial_vector, + # ) + + self.add_view( + OneByOneView( + provider_set=provider_set, + parameter_model=parametermodel + ), + self.Ids.ONEBYONE_VIEW, + ) + + # @property + # def tour_steps(self) -> List[dict]: + # return [ + # { + # "id": self.uuid("layout"), + # "content": ( + # "Dashboard displaying time series from a sensitivity study." + # ), + # }, + # { + # "id": self.uuid("graph-wrapper"), + # "content": ( + # "Selected time series displayed per realization. " + # "Click in the plot to calculate tornadoplot for the " + # "corresponding date, then click on the tornado plot to " + # "highlight the corresponding sensitivity." + # ), + # }, + # { + # "id": self.uuid("table"), + # "content": ( + # "Table statistics for all sensitivities for the selected date." + # ), + # }, + # {"id": self.uuid("vector"), "content": "Select time series"}, + # {"id": self.uuid("ensemble"), "content": "Select ensemble"}, + # ] + + +def create_df_from_table_provider( + providerset: Dict[str, EnsembleTableProvider] +) -> pd.DataFrame: + dfs = [] + for ens, provider in providerset.items(): + df = provider.get_column_data(column_names=provider.column_names()) + df["ENSEMBLE"] = ens + dfs.append(df) + return pd.concat(dfs) \ No newline at end of file diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py new file mode 100644 index 000000000..4176aa199 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py @@ -0,0 +1 @@ +from ._view import OneByOneView \ No newline at end of file diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py new file mode 100644 index 000000000..2c3373966 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py @@ -0,0 +1 @@ +from ._selections import Selections \ No newline at end of file diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py new file mode 100644 index 000000000..3b41ef807 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -0,0 +1,84 @@ +from typing import List +import datetime + +import webviz_core_components as wcc +import webviz_subsurface_components as wsc +from dash import dcc, html +from dash.development.base_component import Component +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ......_providers import Frequency +from .._utils import date_from_str, date_to_str + + + +class Selections(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLE = "ensemble" + VECTOR_SELECTOR = "vector-selector" + SELECTED_DATE = "selected-date" + DATE_SLIDER = "date-slider" + + def __init__( + self, + ensembles: List[str], + vectors: List[str], + dates: List[datetime.datetime] + ) -> None: + super().__init__("Selections") + self._ensembles = ensembles + self._vectors = vectors + self._dates = dates + + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Ensemble", + id=self.register_component_unique_id(self.Ids.ENSEMBLE), + options=[{"label": ens, "value": ens} for ens in self._ensembles], + multi=False, + value=self._ensembles[0], + clearable=False, + ), + wsc.VectorSelector( + label="Time Series", + id=self.register_component_unique_id(self.Ids.VECTOR_SELECTOR), + maxNumSelectedNodes=1, + data=self._vectors, + persistence=True, + persistence_type="session", + selectedTags=["FOPT"] + if "FOPT" in self._vectors + else None, + numSecondsUntilSuggestionsAreShown=0.5, + lineBreakAfterTag=True, + ), + html.Div( + style={"display": "inline-flex"}, + children=[ + wcc.Label("Date:"), + wcc.Label( + date_to_str(self._dates[-1]), + id=self.register_component_unique_id(self.Ids.SELECTED_DATE), + style={"margin-left": "10px"}, + ), + ], + ), + wcc.Slider( + id=self.register_component_unique_id(self.Ids.DATE_SLIDER), + value=len(self._dates) - 1, + min=0, + max=len(self._dates) - 1, + step=1, + included=False, + marks={ + idx: { + "label": date_to_str(self._dates[idx]), + "style": {"white-space": "nowrap"}, + } + for idx in [0, len(self._dates) - 1] + }, + ), + ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py new file mode 100644 index 000000000..c402df5d9 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py @@ -0,0 +1,13 @@ +import datetime + +def date_from_str(date_str: str) -> datetime.datetime: + return datetime.datetime.strptime(date_str, "%Y-%m-%d") + + +def date_to_str(date: datetime.datetime) -> str: + if date.hour != 0 or date.minute != 0 or date.second != 0 or date.microsecond != 0: + raise ValueError( + f"Invalid date resolution, expected no data for hour, minute, second" + f" or microsecond for {str(date)}" + ) + return date.strftime("%Y-%m-%d") \ No newline at end of file diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py new file mode 100644 index 000000000..48a91d00f --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -0,0 +1,46 @@ +import datetime +from typing import Dict, List, Optional, Tuple, Union + +import dash +import pandas as pd +from dash import Input, Output, State, callback +from dash.exceptions import PreventUpdate +from webviz_config import EncodedFile, WebvizPluginABC +from webviz_config._theme_class import WebvizConfigTheme +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import ViewABC + +from webviz_subsurface._providers import EnsembleSummaryProvider +from webviz_subsurface._models.parameter_model import ParametersModel + +from ._settings import Selections + +class OneByOneView(ViewABC): + class Ids(StrEnum): + TIMESERIES_PLOT = "time-series-plot" + TORNADO_PLOT = "tornado-plot" + DATA_TABLE = "data-table" + + SELECTIONS = "selections" + VIZUALISATION = "vizualisation" + SENSITIVITY_FILTER = "sensitivity-filter" + SETTINGS = "settings" + + def __init__( + self, + provider_set: Dict[str, EnsembleSummaryProvider], + parameter_model: ParametersModel, + ) -> None: + super().__init__("OneByOne View") + self._provider_set = provider_set + self._parameter_model = parameter_model + + self.add_settings_groups( + { + self.Ids.SELECTIONS : Selections( + ensembles=list(self._provider_set.keys()), + vectors=[], + dates=[], + ) + } + ) From 514cd5a34a45cdfa1266511877e618fe5b860ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Fri, 5 May 2023 09:31:48 +0200 Subject: [PATCH 02/19] Improvements to the data provider structure, black and isort --- .../__init__.py | 2 +- .../_plugin.py | 85 ++++---- .../_utils/__init__.py | 4 + .../_utils.py => _utils/_datetime_utils.py} | 3 +- ...mulation_time_series_onebyone_datamodel.py | 205 ++++++++++++++++++ .../_views/_onebyone_view/__init__.py | 2 +- .../_onebyone_view/_settings/__init__.py | 2 +- .../_onebyone_view/_settings/_selections.py | 13 +- .../_views/_onebyone_view/_view.py | 24 +- 9 files changed, 271 insertions(+), 69 deletions(-) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py rename webviz_subsurface/plugins/_simulation_time_series_onebyone/{_views/_onebyone_view/_utils.py => _utils/_datetime_utils.py} (91%) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py index 4e6153728..884005c53 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/__init__.py @@ -1 +1 @@ -from ._plugin import SimulationTimeSeriesOneByOne \ No newline at end of file +from ._plugin import SimulationTimeSeriesOneByOne diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py index d8854e40e..2039a19f7 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Dict import pandas as pd @@ -7,16 +8,17 @@ from webviz_subsurface._models.parameter_model import ParametersModel from webviz_subsurface._providers import ( EnsembleSummaryProviderFactory, - EnsembleTableProviderFactory, EnsembleTableProvider, + EnsembleTableProviderFactory, Frequency, ) +from webviz_subsurface._utils.ensemble_summary_provider_set_factory import ( + create_lazy_ensemble_summary_provider_set_from_paths, + create_presampled_ensemble_summary_provider_set_from_paths, +) +from ._utils import SimulationTimeSeriesOneByOneDataModel from ._views._onebyone_view import OneByOneView -# from ._business_logic import SimulationTimeSeriesOneByOneDataModel -# from ._callbacks import plugin_callbacks -# from ._layout import main_view -# from .models import ProviderTimeSeriesDataModel class SimulationTimeSeriesOneByOne(WebvizPluginABC): @@ -51,6 +53,7 @@ class SimulationTimeSeriesOneByOne(WebvizPluginABC): rate, a cumulative, or historical. Units are e.g. added to the plot titles, while rates and \ cumulatives are used to decide the line shapes in the plot. """ + class Ids(StrEnum): ONEBYONE_VIEW = "onebyone-view" @@ -60,44 +63,44 @@ def __init__( ensembles: list, time_index: str = "monthly", rel_file_pattern: str = "share/results/unsmry/*.arrow", - column_keys: list = None, + perform_presampling: bool = False, initial_vector: str = None, line_shape_fallback: str = "linear", ) -> None: - super().__init__() # vectormodel: ProviderTimeSeriesDataModel table_provider = EnsembleTableProviderFactory.instance() - provider_factory = EnsembleSummaryProviderFactory.instance() resampling_frequency = Frequency(time_index) - ensemble_paths = { - ensemble_name: webviz_settings.shared_settings["scratch_ensembles"][ - ensemble_name - ] - for ensemble_name in ensembles - } - try: - provider_set = { - ens: provider_factory.create_from_arrow_unsmry_presampled( - str(ens_path), rel_file_pattern, resampling_frequency - ) - for ens, ens_path in ensemble_paths.items() + if ensembles is not None: + ensemble_paths: Dict[str, Path] = { + ensemble_name: webviz_settings.shared_settings["scratch_ensembles"][ + ensemble_name + ] + for ensemble_name in ensembles } - # vectormodel = ProviderTimeSeriesDataModel( - # provider_set=provider_set, - # column_keys=column_keys, - # line_shape_fallback=line_shape_fallback, - # ) - except ValueError as error: - message = ( - f"Some/all ensembles are missing arrow files at {rel_file_pattern}.\n" - "If no arrow files have been generated with `ERT` using `ECL2CSV`, " - "the commandline tool `smry2arrow_batch` can be used to generate arrow " - "files for an ensemble" + if perform_presampling: + self._presampled_frequency = resampling_frequency + summary_provider_set = ( + create_presampled_ensemble_summary_provider_set_from_paths( + ensemble_paths, rel_file_pattern, self._presampled_frequency + ) + ) + else: + summary_provider_set = ( + create_lazy_ensemble_summary_provider_set_from_paths( + ensemble_paths, rel_file_pattern + ) + ) + else: + raise ValueError('Incorrect argument, must provide "ensembles"') + + if not summary_provider_set: + raise ValueError( + "Initial provider set is undefined, and ensemble summary providers" + " are not instantiated for plugin" ) - raise ValueError(message) from error parameterproviderset = { ens_name: table_provider.create_from_per_realization_parameter_file( @@ -106,19 +109,17 @@ def __init__( for ens_name, ens_path in ensemble_paths.items() } parameter_df = create_df_from_table_provider(parameterproviderset) - parametermodel = ParametersModel(dataframe=parameter_df, drop_constants=True) - # self.datamodel = SimulationTimeSeriesOneByOneDataModel( - # vectormodel=vectormodel, - # parametermodel=parametermodel, - # webviz_settings=webviz_settings, - # initial_vector=initial_vector, - # ) self.add_view( OneByOneView( - provider_set=provider_set, - parameter_model=parametermodel + data_model=SimulationTimeSeriesOneByOneDataModel( + provider_set=summary_provider_set, + parametermodel=parametermodel, + webviz_settings=webviz_settings, + resampling_frequency=resampling_frequency, + initial_vector=initial_vector, + ), ), self.Ids.ONEBYONE_VIEW, ) @@ -160,4 +161,4 @@ def create_df_from_table_provider( df = provider.get_column_data(column_names=provider.column_names()) df["ENSEMBLE"] = ens dfs.append(df) - return pd.concat(dfs) \ No newline at end of file + return pd.concat(dfs) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py new file mode 100644 index 000000000..b21dec50b --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py @@ -0,0 +1,4 @@ +from ._datetime_utils import date_from_str, date_to_str +from ._simulation_time_series_onebyone_datamodel import ( + SimulationTimeSeriesOneByOneDataModel, +) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_datetime_utils.py similarity index 91% rename from webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py rename to webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_datetime_utils.py index c402df5d9..8c9c72303 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_utils.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_datetime_utils.py @@ -1,5 +1,6 @@ import datetime + def date_from_str(date_str: str) -> datetime.datetime: return datetime.datetime.strptime(date_str, "%Y-%m-%d") @@ -10,4 +11,4 @@ def date_to_str(date: datetime.datetime) -> str: f"Invalid date resolution, expected no data for hour, minute, second" f" or microsecond for {str(date)}" ) - return date.strftime("%Y-%m-%d") \ No newline at end of file + return date.strftime("%Y-%m-%d") diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py new file mode 100644 index 000000000..584d35f80 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -0,0 +1,205 @@ +import datetime +from typing import Dict, List, Optional, Tuple + +import pandas as pd +import plotly.graph_objects as go +from webviz_config import WebvizSettings + +from webviz_subsurface._components.tornado._tornado_bar_chart import TornadoBarChart +from webviz_subsurface._components.tornado._tornado_data import TornadoData +from webviz_subsurface._components.tornado._tornado_table import TornadoTable +from webviz_subsurface._figures import TimeSeriesFigure, create_figure +from webviz_subsurface._models.parameter_model import ParametersModel +from webviz_subsurface._providers import EnsembleSummaryProvider, Frequency +from webviz_subsurface._utils.ensemble_summary_provider_set import ( + EnsembleSummaryProviderSet, +) + +from ._datetime_utils import date_from_str, date_to_str + + +class SimulationTimeSeriesOneByOneDataModel: + """Class keeping the data needed in the vizualisations and various + data providing methods. + """ + + SELECTORS = ["QC_FLAG", "SATNUM", "EQLNUM", "FIPNUM"] + + def __init__( + self, + provider_set: EnsembleSummaryProviderSet, + parametermodel: ParametersModel, + webviz_settings: WebvizSettings, + resampling_frequency: Frequency, + initial_vector: Optional[str], + ) -> None: + self._theme = webviz_settings.theme + self._pmodel = parametermodel + self._provider_set = provider_set + self._vectors = self._provider_set.all_vector_names() + self._resampling_frequency = resampling_frequency + + self._parameter_df = parametermodel.sens_df.copy() + + def test(x: pd.Series) -> pd.Series: + return x.apply(lambda v: list(x.unique()).index(v)) + + self._parameter_df["t"] = self._parameter_df.groupby("SENSNAME")[ + "SENSCASE" + ].transform(test) + + self._smry_meta = None + self._senscolormap = { + sens: color for sens, color in zip(self._pmodel.sensitivities, self.colors) + } + self._initial_vector = ( + initial_vector + if initial_vector and initial_vector in self._vectors + else self._vectors[0] + ) + + def create_vectors_statistics_df(self, dframe: pd.DataFrame) -> pd.DataFrame: + cols = [x for x in self._parameter_df.columns if x != "REAL"] + return dframe.groupby(["DATE"] + cols).mean().reset_index() + + @property + def colors(self) -> list: + return self._theme.plotly_theme["layout"]["colorway"] * 5 + + @property + def realizations(self) -> List[int]: + return self._provider_set.all_realizations() + + @property + def ensembles(self) -> List[str]: + return self._provider_set.provider_names() + + @property + def dates(self) -> List[datetime.datetime]: + return self._provider_set.all_dates( + resampling_frequency=self._resampling_frequency + ) + + @property + def sensname_colormap(self) -> dict: + return self._senscolormap + + def get_sensitivity_dataframe_for_ensemble(self, ensemble: str) -> pd.DataFrame: + return self._parameter_df[self._parameter_df["ENSEMBLE"] == ensemble] + + def get_unique_sensitivities_for_ensemble(self, ensemble: str) -> list: + df = self.get_sensitivity_dataframe_for_ensemble(ensemble) + return list(df["SENSNAME"].unique()) + + @staticmethod + def get_tornado_reference(sensitivities: List[str], existing_reference: str) -> str: + if existing_reference in sensitivities: + return existing_reference + if "rms_seed" in sensitivities: + return "rms_seed" + return sensitivities[0] + + def get_tornado_data( + self, dframe: pd.DataFrame, response: str, selections: dict + ) -> TornadoData: + dframe.rename(columns={response: "VALUE"}, inplace=True) + return TornadoData( + dframe=dframe, + reference=selections["Reference"], + response_name=response, + scale=selections["Scale"], + cutbyref=bool(selections["Remove no impact"]), + ) + + def create_tornado_figure( + self, tornado_data: TornadoData, selections: dict, use_si_format: bool + ) -> tuple: + return ( + TornadoBarChart( + tornado_data=tornado_data, + plotly_theme=self._theme.plotly_theme, + label_options=selections["labeloptions"], + number_format="#.3g", + locked_si_prefix=None if use_si_format else "", + use_true_base=selections["Scale"] == "True", + show_realization_points=bool(selections["real_scatter"]), + show_reference=selections["torn_ref"], + color_by_sensitivity=selections["color_by_sens"], + sensitivity_color_map=self.sensname_colormap, + ) + .figure.update_xaxes(side="bottom", title=None) + .update_layout( + title_text=f"Tornadoplot for {tornado_data.response_name}
", + margin={"t": 70}, + ) + ) + + def create_realplot(self, tornado_data: TornadoData) -> go.Figure: + df = tornado_data.real_df + senscasecolors = { + senscase: self.sensname_colormap[sensname] + for senscase, sensname in zip(df["sensname_case"], df["sensname"]) + } + + return ( + create_figure( + plot_type="bar", + data_frame=df, + x="REAL", + y="VALUE", + color="sensname_case", + color_discrete_map=senscasecolors, + barmode="overlay", + custom_data=["casetype"], + yaxis={"range": [df["VALUE"].min() * 0.7, df["VALUE"].max() * 1.1]}, + opacity=0.85, + ) + .update_layout(legend={"orientation": "h", "yanchor": "bottom", "y": 1.02}) + .update_layout(legend_title_text="", margin_b=0, margin_r=10) + .for_each_trace( + lambda t: ( + t.update(marker_line_color="black") + if t["customdata"][0][0] == "high" + else t.update(marker_line_color="white", marker_line_width=2) + ) + if t["customdata"][0][0] != "mc" + else None + ) + ) + + def create_tornado_table( + self, + tornado_data: TornadoData, + use_si_format: bool, + ) -> Tuple[List[dict], List[dict]]: + tornado_table = TornadoTable( + tornado_data=tornado_data, + use_si_format=use_si_format, + precision=4 if use_si_format else 3, + ) + return tornado_table.as_plotly_table, tornado_table.columns + + def create_timeseries_figure( + self, + dframe: pd.DataFrame, + vector: str, + ensemble: str, + date: str, + visualization: str, + ) -> go.Figure: + return go.Figure( + TimeSeriesFigure( + dframe=dframe, + visualization=visualization, + vector=vector, + ensemble=ensemble, + dateline=date_from_str(date), + historical_vector_df=self.vmodel.get_historical_vector_df( + vector, ensemble + ), + color_col="SENSNAME", + line_shape_fallback=self.vmodel.line_shape_fallback, + discrete_color_map=self.sensname_colormap, + groupby="SENSNAME_CASE", + ).figure + ).update_layout({"title": f"{vector}, Date: {date}"}) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py index 4176aa199..96ae48dbd 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/__init__.py @@ -1 +1 @@ -from ._view import OneByOneView \ No newline at end of file +from ._view import OneByOneView diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py index 2c3373966..a06dddb90 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py @@ -1 +1 @@ -from ._selections import Selections \ No newline at end of file +from ._selections import Selections diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py index 3b41ef807..11640bbc4 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -1,5 +1,5 @@ -from typing import List import datetime +from typing import List import webviz_core_components as wcc import webviz_subsurface_components as wsc @@ -12,7 +12,6 @@ from .._utils import date_from_str, date_to_str - class Selections(SettingsGroupABC): class Ids(StrEnum): ENSEMBLE = "ensemble" @@ -21,17 +20,13 @@ class Ids(StrEnum): DATE_SLIDER = "date-slider" def __init__( - self, - ensembles: List[str], - vectors: List[str], - dates: List[datetime.datetime] + self, ensembles: List[str], vectors: List[str], dates: List[datetime.datetime] ) -> None: super().__init__("Selections") self._ensembles = ensembles self._vectors = vectors self._dates = dates - def layout(self) -> List[Component]: return [ wcc.Dropdown( @@ -49,9 +44,7 @@ def layout(self) -> List[Component]: data=self._vectors, persistence=True, persistence_type="session", - selectedTags=["FOPT"] - if "FOPT" in self._vectors - else None, + selectedTags=["FOPT"] if "FOPT" in self._vectors else None, numSecondsUntilSuggestionsAreShown=0.5, lineBreakAfterTag=True, ), diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 48a91d00f..e6ea40a92 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -10,11 +10,14 @@ from webviz_config.utils import StrEnum, callback_typecheck from webviz_config.webviz_plugin_subclasses import ViewABC -from webviz_subsurface._providers import EnsembleSummaryProvider -from webviz_subsurface._models.parameter_model import ParametersModel +from webviz_subsurface._utils.ensemble_summary_provider_set import ( + EnsembleSummaryProviderSet, +) +from ..._utils import SimulationTimeSeriesOneByOneDataModel from ._settings import Selections + class OneByOneView(ViewABC): class Ids(StrEnum): TIMESERIES_PLOT = "time-series-plot" @@ -26,21 +29,16 @@ class Ids(StrEnum): SENSITIVITY_FILTER = "sensitivity-filter" SETTINGS = "settings" - def __init__( - self, - provider_set: Dict[str, EnsembleSummaryProvider], - parameter_model: ParametersModel, - ) -> None: + def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: super().__init__("OneByOne View") - self._provider_set = provider_set - self._parameter_model = parameter_model + self._data_model = data_model self.add_settings_groups( { - self.Ids.SELECTIONS : Selections( - ensembles=list(self._provider_set.keys()), - vectors=[], - dates=[], + self.Ids.SELECTIONS: Selections( + ensembles=self._data_model.ensembles, + vectors=self._data_model._vectors, + dates=self._data_model.dates, ) } ) From 92e8bd54f9dabc21924e490b8c91b2224f0545c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Fri, 5 May 2023 10:37:02 +0200 Subject: [PATCH 03/19] Fixed vector selector problem and added visualization settings group --- .../_types.py | 6 +++ ...mulation_time_series_onebyone_datamodel.py | 25 +++++++++- .../_onebyone_view/_settings/__init__.py | 1 + .../_onebyone_view/_settings/_selections.py | 15 ++++-- .../_settings/_vizualisation.py | 49 +++++++++++++++++++ .../_views/_onebyone_view/_view.py | 12 +++-- 6 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py new file mode 100644 index 000000000..c2ffc4a4f --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py @@ -0,0 +1,6 @@ +from webviz_config.utils import StrEnum + + +class LineType(StrEnum): + REALIZATION = "realization" + MEAN = "mean" diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index 584d35f80..6fd822340 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -5,6 +5,7 @@ import plotly.graph_objects as go from webviz_config import WebvizSettings +from webviz_subsurface._abbreviations.reservoir_simulation import historical_vector from webviz_subsurface._components.tornado._tornado_bar_chart import TornadoBarChart from webviz_subsurface._components.tornado._tornado_data import TornadoData from webviz_subsurface._components.tornado._tornado_table import TornadoTable @@ -14,6 +15,7 @@ from webviz_subsurface._utils.ensemble_summary_provider_set import ( EnsembleSummaryProviderSet, ) +from webviz_subsurface._utils.vector_selector import add_vector_to_vector_selector_data from ._datetime_utils import date_from_str, date_to_str @@ -38,7 +40,6 @@ def __init__( self._provider_set = provider_set self._vectors = self._provider_set.all_vector_names() self._resampling_frequency = resampling_frequency - self._parameter_df = parametermodel.sens_df.copy() def test(x: pd.Series) -> pd.Series: @@ -52,11 +53,14 @@ def test(x: pd.Series) -> pd.Series: self._senscolormap = { sens: color for sens, color in zip(self._pmodel.sensitivities, self.colors) } - self._initial_vector = ( + self.initial_vector = ( initial_vector if initial_vector and initial_vector in self._vectors else self._vectors[0] ) + self.initial_vector_selector_data = self.create_vector_selector_data( + self._vectors + ) def create_vectors_statistics_df(self, dframe: pd.DataFrame) -> pd.DataFrame: cols = [x for x in self._parameter_df.columns if x != "REAL"] @@ -70,6 +74,10 @@ def colors(self) -> list: def realizations(self) -> List[int]: return self._provider_set.all_realizations() + @property + def vectors(self) -> List[str]: + return self._vectors + @property def ensembles(self) -> List[str]: return self._provider_set.provider_names() @@ -84,6 +92,19 @@ def dates(self) -> List[datetime.datetime]: def sensname_colormap(self) -> dict: return self._senscolormap + def create_vector_selector_data(self, vector_names: list) -> list: + vector_selector_data: list = [] + for vector in self._get_non_historical_vector_names(vector_names): + add_vector_to_vector_selector_data(vector_selector_data, vector) + return vector_selector_data + + def _get_non_historical_vector_names(self, vector_names: list) -> list: + return [ + vector + for vector in vector_names + if historical_vector(vector, None, False) not in vector_names + ] + def get_sensitivity_dataframe_for_ensemble(self, ensemble: str) -> pd.DataFrame: return self._parameter_df[self._parameter_df["ENSEMBLE"] == ensemble] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py index a06dddb90..18fdee3fd 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py @@ -1 +1,2 @@ from ._selections import Selections +from ._vizualisation import Visualization diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py index 11640bbc4..3fb6193b8 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -9,7 +9,7 @@ from webviz_config.webviz_plugin_subclasses import SettingsGroupABC from ......_providers import Frequency -from .._utils import date_from_str, date_to_str +from ...._utils import date_from_str, date_to_str class Selections(SettingsGroupABC): @@ -20,12 +20,19 @@ class Ids(StrEnum): DATE_SLIDER = "date-slider" def __init__( - self, ensembles: List[str], vectors: List[str], dates: List[datetime.datetime] + self, + ensembles: List[str], + vectors: List[str], + vector_selector_data: dict, + dates: List[datetime.datetime], + initial_vector: str, ) -> None: super().__init__("Selections") self._ensembles = ensembles self._vectors = vectors + self._vector_selector_data = vector_selector_data self._dates = dates + self._initial_vector = initial_vector def layout(self) -> List[Component]: return [ @@ -41,10 +48,10 @@ def layout(self) -> List[Component]: label="Time Series", id=self.register_component_unique_id(self.Ids.VECTOR_SELECTOR), maxNumSelectedNodes=1, - data=self._vectors, + data=self._vector_selector_data, persistence=True, persistence_type="session", - selectedTags=["FOPT"] if "FOPT" in self._vectors else None, + selectedTags=[self._initial_vector], numSecondsUntilSuggestionsAreShown=0.5, lineBreakAfterTag=True, ), diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py index e69de29bb..0df526c54 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py @@ -0,0 +1,49 @@ +import datetime +from typing import List + +import webviz_core_components as wcc +import webviz_subsurface_components as wsc +from dash import dcc, html +from dash.development.base_component import Component +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ......_providers import Frequency +from ...._types import LineType +from ...._utils import date_from_str, date_to_str + + +class Visualization(SettingsGroupABC): + class Ids(StrEnum): + REALIZATION_OR_MEAN = "realization-or-mean" + BOTTOM_VISUALIZATION = "bottom-visualization" + + def __init__( + self, + ) -> None: + super().__init__("Visualization") + + def layout(self) -> List[Component]: + return [ + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.REALIZATION_OR_MEAN), + options=[ + {"label": "Individual realizations", "value": LineType.REALIZATION}, + {"label": "Mean over Sensitivities", "value": LineType.MEAN}, + ], + value="realizations", + ), + html.Div( + style={"margin-top": "10px"}, + children=wcc.RadioItems( + label="Bottom visualization:", + id=self.register_component_unique_id(self.Ids.BOTTOM_VISUALIZATION), + options=[ + {"label": "Table", "value": "table"}, + {"label": "Realization plot", "value": "realplot"}, + ], + vertical=False, + value="table", + ), + ), + ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index e6ea40a92..303f7310c 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -15,7 +15,7 @@ ) from ..._utils import SimulationTimeSeriesOneByOneDataModel -from ._settings import Selections +from ._settings import Selections, Visualization class OneByOneView(ViewABC): @@ -32,13 +32,19 @@ class Ids(StrEnum): def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: super().__init__("OneByOne View") self._data_model = data_model + for vec in self._data_model.vectors: + if vec == "undefined": + print(vec) self.add_settings_groups( { self.Ids.SELECTIONS: Selections( ensembles=self._data_model.ensembles, - vectors=self._data_model._vectors, + vectors=self._data_model.vectors, + vector_selector_data=self._data_model.initial_vector_selector_data, dates=self._data_model.dates, - ) + initial_vector=self._data_model.initial_vector, + ), + self.Ids.VIZUALISATION: Visualization(), } ) From 1e97e400dc060d6f7dc13497423fa6de55eafb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Fri, 5 May 2023 12:23:00 +0200 Subject: [PATCH 04/19] Implemented the rest of the settings groups --- .../_types.py | 12 +++ ...mulation_time_series_onebyone_datamodel.py | 4 + .../_onebyone_view/_settings/__init__.py | 2 + .../_settings/_general_settings.py | 74 +++++++++++++++++++ .../_onebyone_view/_settings/_selections.py | 5 +- .../_settings/_sensitivity_filter.py | 25 +++++++ .../_settings/_vizualisation.py | 6 +- .../_views/_onebyone_view/_view.py | 8 +- 8 files changed, 127 insertions(+), 9 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py index c2ffc4a4f..702e8e15c 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py @@ -4,3 +4,15 @@ class LineType(StrEnum): REALIZATION = "realization" MEAN = "mean" + + +class ScaleType(StrEnum): + PERCENTAGE = "percentage" + ABSOLUTE = "absolute" + TRUE_VALUE = "true-value" + + +class LabelOptions(StrEnum): + DETAILED = "detailed" + SIMPLE = "simple" + HIDE = "hide" diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index 6fd822340..2cd1d70ea 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -92,6 +92,10 @@ def dates(self) -> List[datetime.datetime]: def sensname_colormap(self) -> dict: return self._senscolormap + @property + def sensitivities(self) -> List[str]: + return self._pmodel.sensitivities + def create_vector_selector_data(self, vector_names: list) -> list: vector_selector_data: list = [] for vector in self._get_non_historical_vector_names(vector_names): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py index 18fdee3fd..0c99d9a8a 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/__init__.py @@ -1,2 +1,4 @@ +from ._general_settings import GeneralSettings from ._selections import Selections +from ._sensitivity_filter import SensitivityFilter from ._vizualisation import Visualization diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py index e69de29bb..c512d2bd3 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py @@ -0,0 +1,74 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ...._types import LabelOptions, ScaleType + + +class GeneralSettings(SettingsGroupABC): + class Ids(StrEnum): + SCALE_TYPE = "scale-type" + CHECKBOX_SETTINGS = "checkbox-settings" + LABEL_OPTIONS = "label-options" + REFERENCE = "reference" + + def __init__( + self, sensitivities: List[str], reference_sensname: str = "rms_seed" + ) -> None: + super().__init__("⚙️ Settings") + self._sensitivities = sensitivities + self._ref_sens = ( + reference_sensname + if reference_sensname in self._sensitivities + else self._sensitivities[0] + ) + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Scale:", + id=self.register_component_unique_id(self.Ids.SCALE_TYPE), + options=[ + {"label": "Relative value (%)", "value": ScaleType.PERCENTAGE}, + {"label": "Relative value", "value": ScaleType.ABSOLUTE}, + {"label": "True value", "value": ScaleType.TRUE_VALUE}, + ], + value=ScaleType.PERCENTAGE, + clearable=False, + ), + wcc.Checklist( + id=self.register_component_unique_id(self.Ids.CHECKBOX_SETTINGS), + style={"margin-top": "10px"}, + options=[ + {"label": "Color by sensitivity", "value": "color-by-sens"}, + {"label": "Show realization points", "value": "real-scatter"}, + {"label": "Show reference on tornado", "value": "show-tornado-ref"}, + { + "label": "Remove sensitivities with no impact", + "value": "remove-no-impact", + }, + ], + value=["color-by-sens", "show-tornado-ref", "remove-no-impact"], + ), + wcc.RadioItems( + label="Label options:", + id=self.register_component_unique_id(self.Ids.LABEL_OPTIONS), + options=[ + {"label": "Detailed", "value": LabelOptions.DETAILED}, + {"label": "Simple", "value": LabelOptions.SIMPLE}, + {"label": "Hide", "value": LabelOptions.HIDE}, + ], + vertical=False, + value=LabelOptions.SIMPLE, + ), + wcc.Dropdown( + label="Reference:", + id=self.register_component_unique_id(self.Ids.REFERENCE), + options=[{"label": elm, "value": elm} for elm in self._sensitivities], + value=self._ref_sens, + clearable=False, + ), + ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py index 3fb6193b8..68a0f7db5 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -3,13 +3,12 @@ import webviz_core_components as wcc import webviz_subsurface_components as wsc -from dash import dcc, html +from dash import html from dash.development.base_component import Component from webviz_config.utils import StrEnum from webviz_config.webviz_plugin_subclasses import SettingsGroupABC -from ......_providers import Frequency -from ...._utils import date_from_str, date_to_str +from ...._utils import date_to_str class Selections(SettingsGroupABC): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py index e69de29bb..4a624adcc 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_sensitivity_filter.py @@ -0,0 +1,25 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + + +class SensitivityFilter(SettingsGroupABC): + class Ids(StrEnum): + SENSITIVITY_FILTER = "sensitivity-filter" + + def __init__(self, sensitivities: List[str]) -> None: + super().__init__("Sensitivity Filter") + self._sensitivities = sensitivities + + def layout(self) -> List[Component]: + return [ + wcc.SelectWithLabel( + id=self.register_component_unique_id(self.Ids.SENSITIVITY_FILTER), + options=[{"label": i, "value": i} for i in self._sensitivities], + value=self._sensitivities, + size=min(20, len(self._sensitivities)), + ) + ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py index 0df526c54..b327ca159 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py @@ -1,16 +1,12 @@ -import datetime from typing import List import webviz_core_components as wcc -import webviz_subsurface_components as wsc -from dash import dcc, html +from dash import html from dash.development.base_component import Component from webviz_config.utils import StrEnum from webviz_config.webviz_plugin_subclasses import SettingsGroupABC -from ......_providers import Frequency from ...._types import LineType -from ...._utils import date_from_str, date_to_str class Visualization(SettingsGroupABC): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 303f7310c..d0fbe6ad9 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -15,7 +15,7 @@ ) from ..._utils import SimulationTimeSeriesOneByOneDataModel -from ._settings import Selections, Visualization +from ._settings import GeneralSettings, Selections, SensitivityFilter, Visualization class OneByOneView(ViewABC): @@ -46,5 +46,11 @@ def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: initial_vector=self._data_model.initial_vector, ), self.Ids.VIZUALISATION: Visualization(), + self.Ids.SENSITIVITY_FILTER: SensitivityFilter( + sensitivities=self._data_model.sensitivities + ), + self.Ids.SETTINGS: GeneralSettings( + sensitivities=self._data_model.sensitivities + ), } ) From f120c4c47d65378c07a571731c6d05f8fde9eb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 8 May 2023 08:30:35 +0200 Subject: [PATCH 05/19] New folder _view_elements --- .../_views/_onebyone_view/_view_elements/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py new file mode 100644 index 000000000..e69de29bb From 9c84521722114aa3442fcfb74e99ada9ca9bdadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 8 May 2023 09:16:10 +0200 Subject: [PATCH 06/19] New view element for the bottom visualization --- .../_bottom_visualization_view_element.py | 42 +++++++++++++++++++ .../_view_elements/_general_view_element.py | 17 ++++++++ 2 files changed, 59 insertions(+) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_bottom_visualization_view_element.py create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_bottom_visualization_view_element.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_bottom_visualization_view_element.py new file mode 100644 index 000000000..67b04ea47 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_bottom_visualization_view_element.py @@ -0,0 +1,42 @@ +import webviz_core_components as wcc +from dash import dash_table, html +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class BottomVisualizationViewElement(ViewElementABC): + class Ids(StrEnum): + TABLE_WRAPPER = "table-wrapper" + TABLE = "table" + REAL_GRAPH_WRAPPER = "real-graph-wrapper" + REAL_GRAPH = "real-graph" + + def __init__(self) -> None: + super().__init__() + + def inner_layout(self) -> html.Div: + return html.Div( + children=[ + html.Div( + id=self.register_component_unique_id(self.Ids.TABLE_WRAPPER), + style={"display": "block"}, + children=dash_table.DataTable( + id=self.register_component_unique_id(self.Ids.TABLE), + sort_action="native", + sort_mode="multi", + filter_action="native", + style_as_list_view=True, + style_table={"height": "35vh", "overflowY": "auto"}, + ), + ), + html.Div( + id=self.register_component_unique_id(self.Ids.REAL_GRAPH_WRAPPER), + style={"display": "none"}, + children=wcc.Graph( + config={"displayModeBar": False}, + style={"height": "35vh"}, + id=self.register_component_unique_id(self.Ids.REAL_GRAPH), + ), + ), + ] + ) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py new file mode 100644 index 000000000..0ad0b3446 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py @@ -0,0 +1,17 @@ +import webviz_core_components as wcc +from dash import html +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class GeneralViewElement(ViewElementABC): + class Ids(StrEnum): + GRAPH = "graph" + + def __init__(self) -> None: + super().__init__() + + def inner_layout(self) -> html.Div: + return html.Div( + children=[wcc.Graph(id=self.register_component_unique_id(self.Ids.GRAPH))] + ) From fda8e34de1c67178ab4cc1c73eee3d00d22d4748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 8 May 2023 09:18:43 +0200 Subject: [PATCH 07/19] Further work on converting the callbacks to WLF --- .../_settings/_general_settings.py | 20 +- .../_views/_onebyone_view/_view.py | 531 +++++++++++++++++- .../_onebyone_view/_view_elements/__init__.py | 2 + 3 files changed, 544 insertions(+), 9 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py index c512d2bd3..1efd5e87c 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py @@ -1,11 +1,14 @@ +import datetime from typing import List import webviz_core_components as wcc +from dash import dcc from dash.development.base_component import Component from webviz_config.utils import StrEnum from webviz_config.webviz_plugin_subclasses import SettingsGroupABC from ...._types import LabelOptions, ScaleType +from ...._utils import date_to_str class GeneralSettings(SettingsGroupABC): @@ -14,9 +17,16 @@ class Ids(StrEnum): CHECKBOX_SETTINGS = "checkbox-settings" LABEL_OPTIONS = "label-options" REFERENCE = "reference" + OPTIONS_STORE = "options-store" + REAL_STORE = "real-store" + DATE_STORE = "date-store" + VECTOR_STORE = "vector-store" def __init__( - self, sensitivities: List[str], reference_sensname: str = "rms_seed" + self, + sensitivities: List[str], + initial_date: datetime.datetime, + reference_sensname: str = "rms_seed", ) -> None: super().__init__("⚙️ Settings") self._sensitivities = sensitivities @@ -25,6 +35,7 @@ def __init__( if reference_sensname in self._sensitivities else self._sensitivities[0] ) + self._initial_date = initial_date def layout(self) -> List[Component]: return [ @@ -71,4 +82,11 @@ def layout(self) -> List[Component]: value=self._ref_sens, clearable=False, ), + dcc.Store(self.register_component_unique_id(self.Ids.OPTIONS_STORE)), + dcc.Store(self.register_component_unique_id(self.Ids.REAL_STORE)), + dcc.Store( + self.register_component_unique_id(self.Ids.DATE_STORE), + data=date_to_str(self._initial_date), + ), + dcc.Store(self.register_component_unique_id(self.Ids.VECTOR_STORE)), ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index d0fbe6ad9..623214295 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -1,9 +1,9 @@ import datetime from typing import Dict, List, Optional, Tuple, Union -import dash import pandas as pd -from dash import Input, Output, State, callback +import plotly.graph_objects as go +from dash import Input, Output, State, callback, ctx, html, no_update from dash.exceptions import PreventUpdate from webviz_config import EncodedFile, WebvizPluginABC from webviz_config._theme_class import WebvizConfigTheme @@ -14,15 +14,17 @@ EnsembleSummaryProviderSet, ) -from ..._utils import SimulationTimeSeriesOneByOneDataModel +from ..._types import LabelOptions, LineType +from ..._utils import SimulationTimeSeriesOneByOneDataModel, date_from_str, date_to_str from ._settings import GeneralSettings, Selections, SensitivityFilter, Visualization +from ._view_elements import BottomVisualizationViewElement, GeneralViewElement class OneByOneView(ViewABC): class Ids(StrEnum): TIMESERIES_PLOT = "time-series-plot" TORNADO_PLOT = "tornado-plot" - DATA_TABLE = "data-table" + BOTTOM_VISUALIZATION = "bottom-visualization" SELECTIONS = "selections" VIZUALISATION = "vizualisation" @@ -32,9 +34,6 @@ class Ids(StrEnum): def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: super().__init__("OneByOne View") self._data_model = data_model - for vec in self._data_model.vectors: - if vec == "undefined": - print(vec) self.add_settings_groups( { @@ -50,7 +49,523 @@ def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: sensitivities=self._data_model.sensitivities ), self.Ids.SETTINGS: GeneralSettings( - sensitivities=self._data_model.sensitivities + sensitivities=self._data_model.sensitivities, + initial_date=self._data_model.dates[-1], ), } ) + + first_row = self.add_row() + first_row.add_view_element(GeneralViewElement(), self.Ids.TIMESERIES_PLOT) + first_row.add_view_element(GeneralViewElement(), self.Ids.TORNADO_PLOT) + second_row = self.add_row() + second_row.add_view_element( + BottomVisualizationViewElement(), self.Ids.BOTTOM_VISUALIZATION + ) + + def set_callbacks(self) -> None: + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.CHECKBOX_SETTINGS + ), + "value", + ), + ) + def _update_options(selected_options: list) -> dict: + """Update graph with line coloring, vertical line and title""" + all_options = [ + "color-by-sens", + "real-scatter", + "show-tornado-ref", + "remove-no-impact", + ] + return {option: option in selected_options for option in all_options} + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REAL_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SENSITIVITY_FILTER, + SensitivityFilter.Ids.SENSITIVITY_FILTER, + ), + "value", + ), + State( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + ) + def _update_realization_store(sensitivites: list, ensemble: str) -> List[int]: + """Update graph with line coloring, vertical line and title""" + df = self._data_model.get_sensitivity_dataframe_for_ensemble(ensemble) + return list(df[df["SENSNAME"].isin(sensitivites)]["REAL"].unique()) + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SENSITIVITY_FILTER, + SensitivityFilter.Ids.SENSITIVITY_FILTER, + ), + "value", + ), + Input( + self.view_element(self.Ids.TORNADO_PLOT) + .component_unique_id(GeneralViewElement.Ids.GRAPH) + .to_string(), + "clickData", + ), + State( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE + ), + "value", + ), + prevent_initial_call=True, + ) + @callback_typecheck + def _update_sensitivity_filter( + tornado_click_data: dict, reference: str + ) -> List[str]: + """Update graph with line coloring, vertical line and title""" + clicked_data = tornado_click_data["points"][0] + return [clicked_data["y"], reference] + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SENSITIVITY_FILTER, + SensitivityFilter.Ids.SENSITIVITY_FILTER, + ), + "options", + ), + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE + ), + "options", + ), + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE + ), + "value", + ), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.VECTOR_SELECTOR + ), + "data", + ), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.VECTOR_SELECTOR + ), + "selectedTags", + ), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + State( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.VECTOR_SELECTOR + ), + "selectedNodes", + ), + State( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE + ), + "value", + ), + ) + @callback_typecheck + def _update_sensitivity_filter_and_reference( + ensemble: str, vector: list, reference: str + ) -> tuple: + """Update graph with line coloring, vertical line and title""" + sensitivities = self._data_model.get_unique_sensitivities_for_ensemble( + ensemble + ) + available_vectors = self._data_model._vmodel._provider_set[ + ensemble + ].vector_names_filtered_by_value( + exclude_all_values_zero=True, exclude_constant_values=True + ) + vector_selector_data = self._data_model.vmodel.create_vector_selector_data( + available_vectors + ) + + vector = ( + vector if vector[0] in available_vectors else [available_vectors[0]] + ) + return ( + [{"label": elm, "value": elm} for elm in sensitivities], + [{"label": elm, "value": elm} for elm in sensitivities], + self._data_model.get_tornado_reference(sensitivities, reference), + vector_selector_data, + vector, + ) + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.VECTOR_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.VECTOR_SELECTOR + ), + "selectedNodes", + ), + ) + @callback_typecheck + def _update_vector_store(vector: list) -> str: + """Unpack selected vector in vector selector""" + if not vector: + raise PreventUpdate + return vector[0] + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE + ), + "data", + ), + Output(get_uuid("date_selector_wrapper"), "children"), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + Input( + self.view_element(self.Ids.TIMESERIES_PLOT) + .component_unique_id(GeneralViewElement.Ids.GRAPH) + .to_string(), + "clickData", + ), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ), + "value", + ), + State( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE + ), + "data", + ), + ) + def _render_date_selector( + ensemble: str, + timeseries_clickdata: Union[None, dict], + dateidx: List[int], + date: str, + ) -> Tuple[str, html.Div]: + """Store selected date and tornado input. Write statistics + to table""" + + dates = self._data_model.vmodel.dates_for_ensemble(ensemble) + dateslider_drag = get_uuid("date-slider") in str(ctx.triggered_id) + + if timeseries_clickdata is not None and ctx.triggered_id == get_uuid( + "graph" + ): + date = timeseries_clickdata.get("points", [{}])[0]["x"] + elif dateslider_drag: + date = date_to_str(dates[dateidx[0]]) + + date_selected = ( + date_from_str(date) + if date_from_str(date) in dates + else self._data_model.vmodel.get_last_date(ensemble) + ) + + return ( + date_to_str(date_selected), + date_selector(get_uuid, date_selected=date_selected, dates=dates) + if not dateslider_drag + else no_update, + ) + + @callback( + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.SELECTED_DATE + ), + "children", + ), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ), + "drag_value", + ), + Input( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + prevent_initial_call=True, + ) + @callback_typecheck + def _update_date_text(dateidx: List[int], ensemble: str) -> List[str]: + """Update selected date text on date-slider drag""" + if ctx.triggered_id == self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ): + date = self._data_model.vmodel.get_last_date(ensemble) + else: + dates = self._data_model.vmodel.dates_for_ensemble(ensemble) + date = dates[dateidx[0]] + return [date_to_str(date)] + + @callback( + Output( + self.view_element(self.Ids.TIMESERIES_PLOT) + .component_unique_id(GeneralViewElement.Ids.GRAPH) + .to_string(), + "figure", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.VIZUALISATION, Visualization.Ids.REALIZATION_OR_MEAN + ), + "value", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.VECTOR_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.REAL_STORE + ), + "data", + ), + State( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + ) + @callback_typecheck + def _update_timeseries_figure( + date: str, + linetype: LineType, + vector: str, + realizations: list, + ensemble: str, + ) -> go.Figure: + # Get dataframe with vectors and dataframe with parameters and merge + vector_df = self._data_model.vmodel.get_vector_df( + ensemble=ensemble, vectors=[vector], realizations=realizations + ) + data = merge_dataframes_on_realization( + dframe1=vector_df, + dframe2=self._data_model.get_sensitivity_dataframe_for_ensemble( + ensemble + ), + ) + if linetype == LineType.MEAN: + data = self._data_model.create_vectors_statistics_df(data) + + return self._data_model.create_timeseries_figure( + data, vector, ensemble, date, linetype + ) + + @callback( + Output( + self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( + BottomVisualizationViewElement.Ids.TABLE + ), + "data", + ), + Output( + self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( + BottomVisualizationViewElement.Ids.TABLE + ), + "columns", + ), + Output( + self.view_element(self.Ids.TORNADO_PLOT).component_unique_id( + GeneralViewElement.Ids.GRAPH + ), + "figure", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.VECTOR_STORE + ), + "data", + ), + State( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + ) + @callback_typecheck + def _update_tornadoplot( + date: str, selections: dict, vector: str, ensemble: str + ) -> tuple: + if selections is None or selections[ + "Reference" + ] not in self._data_model.get_unique_sensitivities_for_ensemble(ensemble): + raise PreventUpdate + + # Get dataframe with vectors and dataframe with parameters and merge + vector_df = self._data_model.vmodel.get_vector_df( + ensemble=ensemble, vectors=[vector], date=date_from_str(date) + ) + data = merge_dataframes_on_realization( + dframe1=vector_df, + dframe2=self._data_model.get_sensitivity_dataframe_for_ensemble( + ensemble + ), + ) + + tornado_data = self._data_model.get_tornado_data(data, vector, selections) + use_si_format = tornado_data.reference_average > 1000 + tornadofig = self._data_model.create_tornado_figure( + tornado_data, selections, use_si_format + ) + table, columns = self._data_model.create_tornado_table( + tornado_data, use_si_format + ) + return table, columns, tornadofig + + @callback( + Output( + self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( + BottomVisualizationViewElement.Ids.REAL_GRAPH + ), + "figure", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE + ), + "data", + ), + State( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS_STORE + ), + "data", + ), + Input( + self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.VECTOR_STORE + ), + "data", + ), + State( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ), + "value", + ), + Input( + self.settings_group_unique_id( + self.Ids.VIZUALISATION, Visualization.Ids.BOTTOM_VISUALIZATION + ), + "value", + ), + ) + @callback_typecheck + def _update_realplot( + date: str, + selections: dict, + vector: str, + ensemble: str, + selected_vizualisation: str, + ) -> go.Figure: + if selections is None or selected_vizualisation == "table": + raise PreventUpdate + + # Get dataframe with vectors and dataframe with parameters and merge + vector_df = self._data_model.vmodel.get_vector_df( + ensemble=ensemble, vectors=[vector], date=date_from_str(date) + ) + data = merge_dataframes_on_realization( + dframe1=vector_df, + dframe2=self._data_model.get_sensitivity_dataframe_for_ensemble( + ensemble + ), + ) + tornado_data = self._data_model.get_tornado_data(data, vector, selections) + + return self.data_model.create_realplot(tornado_data) + + @callback( + Output( + self.view_element( + self.Ids.BOTTOM_VISUALIZATION, + BottomVisualizationViewElement.Ids.REAL_GRAPH_WRAPPER, + ), + "style", + ), + Output( + self.view_element( + self.Ids.BOTTOM_VISUALIZATION, + BottomVisualizationViewElement.Ids.TABLE_WRAPPER, + ), + "style", + ), + Input( + self.settings_group_unique_id( + self.Ids.VIZUALISATION, Visualization.Ids.BOTTOM_VISUALIZATION + ), + "value", + ), + ) + @callback_typecheck + def _display_table_or_realplot(selected_vizualisation: str) -> tuple: + return { + "display": "none" if selected_vizualisation == "table" else "block" + }, {"display": "block" if selected_vizualisation == "table" else "none"} diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py index e69de29bb..e858f26e0 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/__init__.py @@ -0,0 +1,2 @@ +from ._bottom_visualization_view_element import BottomVisualizationViewElement +from ._general_view_element import GeneralViewElement From 7d29124d32b648ab87a0b9c77a4c04789ed800d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Tue, 9 May 2023 15:43:25 +0200 Subject: [PATCH 08/19] First working version of callback, plenty of bugs still --- .../_plugin.py | 1 + .../_utils/__init__.py | 1 + ...mulation_time_series_onebyone_datamodel.py | 81 ++++-- .../_settings/_general_settings.py | 66 +++-- .../_onebyone_view/_settings/_selections.py | 2 +- .../_settings/_vizualisation.py | 2 +- .../_views/_onebyone_view/_view.py | 246 +++++++++--------- 7 files changed, 240 insertions(+), 159 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py index 2039a19f7..63d1497f2 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py @@ -119,6 +119,7 @@ def __init__( webviz_settings=webviz_settings, resampling_frequency=resampling_frequency, initial_vector=initial_vector, + line_shape_fallback=line_shape_fallback, ), ), self.Ids.ONEBYONE_VIEW, diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py index b21dec50b..09f7d4867 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py @@ -1,4 +1,5 @@ from ._datetime_utils import date_from_str, date_to_str from ._simulation_time_series_onebyone_datamodel import ( SimulationTimeSeriesOneByOneDataModel, + create_vector_selector_data, ) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index 2cd1d70ea..d1505a14e 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -1,5 +1,5 @@ import datetime -from typing import Dict, List, Optional, Tuple +from typing import List, Optional, Tuple import pandas as pd import plotly.graph_objects as go @@ -17,7 +17,7 @@ ) from webviz_subsurface._utils.vector_selector import add_vector_to_vector_selector_data -from ._datetime_utils import date_from_str, date_to_str +from ._datetime_utils import date_from_str class SimulationTimeSeriesOneByOneDataModel: @@ -33,6 +33,7 @@ def __init__( parametermodel: ParametersModel, webviz_settings: WebvizSettings, resampling_frequency: Frequency, + line_shape_fallback: str, initial_vector: Optional[str], ) -> None: self._theme = webviz_settings.theme @@ -40,6 +41,7 @@ def __init__( self._provider_set = provider_set self._vectors = self._provider_set.all_vector_names() self._resampling_frequency = resampling_frequency + self._line_shape_fallback = line_shape_fallback self._parameter_df = parametermodel.sens_df.copy() def test(x: pd.Series) -> pd.Series: @@ -58,9 +60,7 @@ def test(x: pd.Series) -> pd.Series: if initial_vector and initial_vector in self._vectors else self._vectors[0] ) - self.initial_vector_selector_data = self.create_vector_selector_data( - self._vectors - ) + self.initial_vector_selector_data = create_vector_selector_data(self._vectors) def create_vectors_statistics_df(self, dframe: pd.DataFrame) -> pd.DataFrame: cols = [x for x in self._parameter_df.columns if x != "REAL"] @@ -83,7 +83,7 @@ def ensembles(self) -> List[str]: return self._provider_set.provider_names() @property - def dates(self) -> List[datetime.datetime]: + def all_dates(self) -> List[datetime.datetime]: return self._provider_set.all_dates( resampling_frequency=self._resampling_frequency ) @@ -96,18 +96,44 @@ def sensname_colormap(self) -> dict: def sensitivities(self) -> List[str]: return self._pmodel.sensitivities - def create_vector_selector_data(self, vector_names: list) -> list: - vector_selector_data: list = [] - for vector in self._get_non_historical_vector_names(vector_names): - add_vector_to_vector_selector_data(vector_selector_data, vector) - return vector_selector_data + def ensemble_dates(self, ensemble: str) -> List[datetime.datetime]: + return self._provider_set.provider(ensemble).dates( + resampling_frequency=self._resampling_frequency + ) - def _get_non_historical_vector_names(self, vector_names: list) -> list: - return [ - vector - for vector in vector_names - if historical_vector(vector, None, False) not in vector_names - ] + def provider(self, ensemble: str) -> EnsembleSummaryProvider: + return self._provider_set.provider(ensemble) + + def get_vectors_df( + self, + ensemble: str, + vector_names: List[str], + realizations: Optional[List[int]] = None, + date: Optional[datetime.datetime] = None, + ) -> pd.DataFrame: + provider = self._provider_set.provider(ensemble) + if date is None: + return provider.get_vectors_df( + vector_names=vector_names, + realizations=realizations, + resampling_frequency=self._resampling_frequency, + ) + return provider.get_vectors_for_date_df( + date=date, + vector_names=vector_names, + realizations=realizations, + ) + + def get_historical_vector_df( + self, vector: str, ensemble: str + ) -> Optional[pd.DataFrame]: + hist_vecname = historical_vector(vector, smry_meta=None) + provider = self.provider(ensemble) + if hist_vecname and hist_vecname in provider.vector_names(): + return provider.get_vectors_df( + [hist_vecname], None, realizations=provider.realizations()[:1] + ).rename(columns={hist_vecname: vector}) + return None def get_sensitivity_dataframe_for_ensemble(self, ensemble: str) -> pd.DataFrame: return self._parameter_df[self._parameter_df["ENSEMBLE"] == ensemble] @@ -219,12 +245,27 @@ def create_timeseries_figure( vector=vector, ensemble=ensemble, dateline=date_from_str(date), - historical_vector_df=self.vmodel.get_historical_vector_df( - vector, ensemble + historical_vector_df=self.get_historical_vector_df( + vector=vector, ensemble=ensemble ), color_col="SENSNAME", - line_shape_fallback=self.vmodel.line_shape_fallback, + line_shape_fallback=self._line_shape_fallback, discrete_color_map=self.sensname_colormap, groupby="SENSNAME_CASE", ).figure ).update_layout({"title": f"{vector}, Date: {date}"}) + + +def create_vector_selector_data(vector_names: list) -> list: + vector_selector_data: list = [] + for vector in _get_non_historical_vector_names(vector_names): + add_vector_to_vector_selector_data(vector_selector_data, vector) + return vector_selector_data + + +def _get_non_historical_vector_names(vector_names: list) -> list: + return [ + vector + for vector in vector_names + if historical_vector(vector, None, False) not in vector_names + ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py index 1efd5e87c..60b2cac2f 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py @@ -2,7 +2,7 @@ from typing import List import webviz_core_components as wcc -from dash import dcc +from dash import dcc, html from dash.development.base_component import Component from webviz_config.utils import StrEnum from webviz_config.webviz_plugin_subclasses import SettingsGroupABC @@ -13,10 +13,12 @@ class GeneralSettings(SettingsGroupABC): class Ids(StrEnum): - SCALE_TYPE = "scale-type" - CHECKBOX_SETTINGS = "checkbox-settings" - LABEL_OPTIONS = "label-options" - REFERENCE = "reference" + # SCALE_TYPE = "scale-type" + # CHECKBOX_SETTINGS = "checkbox-settings" + # LABEL_OPTIONS = "label-options" + # REFERENCE = "reference" + + OPTIONS = "options" OPTIONS_STORE = "options-store" REAL_STORE = "real-store" DATE_STORE = "date-store" @@ -38,10 +40,12 @@ def __init__( self._initial_date = initial_date def layout(self) -> List[Component]: + options_id = self.register_component_unique_id(self.Ids.OPTIONS) return [ wcc.Dropdown( label="Scale:", - id=self.register_component_unique_id(self.Ids.SCALE_TYPE), + id={"id": options_id, "selector": "Scale"}, + # id=self.register_component_unique_id(self.Ids.SCALE_TYPE), options=[ {"label": "Relative value (%)", "value": ScaleType.PERCENTAGE}, {"label": "Relative value", "value": ScaleType.ABSOLUTE}, @@ -50,23 +54,44 @@ def layout(self) -> List[Component]: value=ScaleType.PERCENTAGE, clearable=False, ), - wcc.Checklist( - id=self.register_component_unique_id(self.Ids.CHECKBOX_SETTINGS), - style={"margin-top": "10px"}, - options=[ - {"label": "Color by sensitivity", "value": "color-by-sens"}, - {"label": "Show realization points", "value": "real-scatter"}, - {"label": "Show reference on tornado", "value": "show-tornado-ref"}, - { - "label": "Remove sensitivities with no impact", - "value": "remove-no-impact", - }, + # wcc.Checklist( + # id=self.register_component_unique_id(self.Ids.CHECKBOX_SETTINGS), + # style={"margin-top": "10px"}, + # options=[ + # {"label": "Color by sensitivity", "value": "color-by-sens"}, + # {"label": "Show realization points", "value": "real-scatter"}, + # {"label": "Show reference on tornado", "value": "show-tornado-ref"}, + # { + # "label": "Remove sensitivities with no impact", + # "value": "remove-no-impact", + # }, + # ], + # value=["color-by-sens", "show-tornado-ref", "remove-no-impact"], + # ), + html.Div( + style={"margin-top": "10px", "margin-bottom": "10px"}, + children=[ + wcc.Checklist( + id={"id": options_id, "selector": selector}, + options=[{"label": label, "value": "selected"}], + value=["selected"] if selected else [], + ) + for label, selector, selected in [ + ("Color by sensitivity", "color_by_sens", True), + ("Show realization points", "real_scatter", False), + ("Show reference on tornado", "torn_ref", True), + ( + "Remove sensitivities with no impact", + "Remove no impact", + True, + ), + ] ], - value=["color-by-sens", "show-tornado-ref", "remove-no-impact"], ), wcc.RadioItems( label="Label options:", - id=self.register_component_unique_id(self.Ids.LABEL_OPTIONS), + # id=self.register_component_unique_id(self.Ids.LABEL_OPTIONS), + id={"id": options_id, "selector": "labeloptions"}, options=[ {"label": "Detailed", "value": LabelOptions.DETAILED}, {"label": "Simple", "value": LabelOptions.SIMPLE}, @@ -77,7 +102,8 @@ def layout(self) -> List[Component]: ), wcc.Dropdown( label="Reference:", - id=self.register_component_unique_id(self.Ids.REFERENCE), + # id=self.register_component_unique_id(self.Ids.REFERENCE), + id={"id": options_id, "selector": "Reference"}, options=[{"label": elm, "value": elm} for elm in self._sensitivities], value=self._ref_sens, clearable=False, diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py index 68a0f7db5..e7e19b7dd 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -59,7 +59,7 @@ def layout(self) -> List[Component]: children=[ wcc.Label("Date:"), wcc.Label( - date_to_str(self._dates[-1]), + children=date_to_str(self._dates[-1]), id=self.register_component_unique_id(self.Ids.SELECTED_DATE), style={"margin-left": "10px"}, ), diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py index b327ca159..a830f0604 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py @@ -27,7 +27,7 @@ def layout(self) -> List[Component]: {"label": "Individual realizations", "value": LineType.REALIZATION}, {"label": "Mean over Sensitivities", "value": LineType.MEAN}, ], - value="realizations", + value=LineType.REALIZATION, ), html.Div( style={"margin-top": "10px"}, diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 623214295..3ac8ebf46 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -1,21 +1,20 @@ -import datetime -from typing import Dict, List, Optional, Tuple, Union +from typing import List, Tuple, Union -import pandas as pd import plotly.graph_objects as go -from dash import Input, Output, State, callback, ctx, html, no_update +from dash import ALL, Input, Output, State, callback, ctx, html from dash.exceptions import PreventUpdate -from webviz_config import EncodedFile, WebvizPluginABC -from webviz_config._theme_class import WebvizConfigTheme from webviz_config.utils import StrEnum, callback_typecheck from webviz_config.webviz_plugin_subclasses import ViewABC -from webviz_subsurface._utils.ensemble_summary_provider_set import ( - EnsembleSummaryProviderSet, -) +from webviz_subsurface._utils.dataframe_utils import merge_dataframes_on_realization from ..._types import LabelOptions, LineType -from ..._utils import SimulationTimeSeriesOneByOneDataModel, date_from_str, date_to_str +from ..._utils import ( + SimulationTimeSeriesOneByOneDataModel, + create_vector_selector_data, + date_from_str, + date_to_str, +) from ._settings import GeneralSettings, Selections, SensitivityFilter, Visualization from ._view_elements import BottomVisualizationViewElement, GeneralViewElement @@ -41,7 +40,7 @@ def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: ensembles=self._data_model.ensembles, vectors=self._data_model.vectors, vector_selector_data=self._data_model.initial_vector_selector_data, - dates=self._data_model.dates, + dates=self._data_model.all_dates, initial_vector=self._data_model.initial_vector, ), self.Ids.VIZUALISATION: Visualization(), @@ -50,15 +49,16 @@ def __init__(self, data_model: SimulationTimeSeriesOneByOneDataModel) -> None: ), self.Ids.SETTINGS: GeneralSettings( sensitivities=self._data_model.sensitivities, - initial_date=self._data_model.dates[-1], + initial_date=self._data_model.all_dates[-1], ), } ) - first_row = self.add_row() + main_column = self.add_column() + first_row = main_column.make_row() first_row.add_view_element(GeneralViewElement(), self.Ids.TIMESERIES_PLOT) first_row.add_view_element(GeneralViewElement(), self.Ids.TORNADO_PLOT) - second_row = self.add_row() + second_row = main_column.make_row() second_row.add_view_element( BottomVisualizationViewElement(), self.Ids.BOTTOM_VISUALIZATION ) @@ -72,21 +72,29 @@ def set_callbacks(self) -> None: "data", ), Input( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.CHECKBOX_SETTINGS - ), + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": ALL, + }, "value", ), + State( + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": ALL, + }, + "id", + ), ) - def _update_options(selected_options: list) -> dict: + def _update_options(option_values: list, options_id: List[dict]) -> dict: """Update graph with line coloring, vertical line and title""" - all_options = [ - "color-by-sens", - "real-scatter", - "show-tornado-ref", - "remove-no-impact", - ] - return {option: option in selected_options for option in all_options} + return { + opt["selector"]: value for opt, value in zip(options_id, option_values) + } @callback( Output( @@ -129,9 +137,12 @@ def _update_realization_store(sensitivites: list, ensemble: str) -> List[int]: "clickData", ), State( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE - ), + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": "Reference", + }, "value", ), prevent_initial_call=True, @@ -153,15 +164,21 @@ def _update_sensitivity_filter( "options", ), Output( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE - ), + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": "Reference", + }, "options", ), Output( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE - ), + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": "Reference", + }, "value", ), Output( @@ -189,9 +206,12 @@ def _update_sensitivity_filter( "selectedNodes", ), State( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.REFERENCE - ), + { + "id": self.settings_group_unique_id( + self.Ids.SETTINGS, GeneralSettings.Ids.OPTIONS + ), + "selector": "Reference", + }, "value", ), ) @@ -203,14 +223,12 @@ def _update_sensitivity_filter_and_reference( sensitivities = self._data_model.get_unique_sensitivities_for_ensemble( ensemble ) - available_vectors = self._data_model._vmodel._provider_set[ + available_vectors = self._data_model.provider( ensemble - ].vector_names_filtered_by_value( + ).vector_names_filtered_by_value( exclude_all_values_zero=True, exclude_constant_values=True ) - vector_selector_data = self._data_model.vmodel.create_vector_selector_data( - available_vectors - ) + vector_selector_data = create_vector_selector_data(available_vectors) vector = ( vector if vector[0] in available_vectors else [available_vectors[0]] @@ -251,7 +269,18 @@ def _update_vector_store(vector: list) -> str: ), "data", ), - Output(get_uuid("date_selector_wrapper"), "children"), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.SELECTED_DATE + ), + "children", + ), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ), + "value", + ), Input( self.settings_group_unique_id( self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE @@ -277,7 +306,7 @@ def _update_vector_store(vector: list) -> str: "data", ), ) - def _render_date_selector( + def _update_date( ensemble: str, timeseries_clickdata: Union[None, dict], dateidx: List[int], @@ -286,62 +315,40 @@ def _render_date_selector( """Store selected date and tornado input. Write statistics to table""" - dates = self._data_model.vmodel.dates_for_ensemble(ensemble) - dateslider_drag = get_uuid("date-slider") in str(ctx.triggered_id) + new_ensemble = self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE + ) in str(ctx.triggered_id) + dateslider_drag = self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ) in str(ctx.triggered_id) + timeseriesgraph_click = ( + timeseries_clickdata is not None + and self.view_element(self.Ids.TIMESERIES_PLOT) + .component_unique_id(GeneralViewElement.Ids.GRAPH) + .to_string() + in ctx.triggered_id + ) + dates = self._data_model.ensemble_dates(ensemble) - if timeseries_clickdata is not None and ctx.triggered_id == get_uuid( - "graph" - ): - date = timeseries_clickdata.get("points", [{}])[0]["x"] - elif dateslider_drag: + if new_ensemble: + date = dates[-1] + + if timeseriesgraph_click: + date = date_from_str(timeseries_clickdata.get("points", [{}])[0]["x"]) + + if dateslider_drag: date = date_to_str(dates[dateidx[0]]) date_selected = ( - date_from_str(date) - if date_from_str(date) in dates - else self._data_model.vmodel.get_last_date(ensemble) + date_from_str(date) if date_from_str(date) in dates else dates[-1] ) return ( date_to_str(date_selected), - date_selector(get_uuid, date_selected=date_selected, dates=dates) - if not dateslider_drag - else no_update, + date_to_str(date_selected), + dates.index(date_selected), ) - @callback( - Output( - self.settings_group_unique_id( - self.Ids.SELECTIONS, Selections.Ids.SELECTED_DATE - ), - "children", - ), - Input( - self.settings_group_unique_id( - self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER - ), - "drag_value", - ), - Input( - self.settings_group_unique_id( - self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE - ), - "value", - ), - prevent_initial_call=True, - ) - @callback_typecheck - def _update_date_text(dateidx: List[int], ensemble: str) -> List[str]: - """Update selected date text on date-slider drag""" - if ctx.triggered_id == self.settings_group_unique_id( - self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE - ): - date = self._data_model.vmodel.get_last_date(ensemble) - else: - dates = self._data_model.vmodel.dates_for_ensemble(ensemble) - date = dates[dateidx[0]] - return [date_to_str(date)] - @callback( Output( self.view_element(self.Ids.TIMESERIES_PLOT) @@ -389,8 +396,8 @@ def _update_timeseries_figure( ensemble: str, ) -> go.Figure: # Get dataframe with vectors and dataframe with parameters and merge - vector_df = self._data_model.vmodel.get_vector_df( - ensemble=ensemble, vectors=[vector], realizations=realizations + vector_df = self._data_model.get_vectors_df( + ensemble=ensemble, vector_names=[vector], realizations=realizations ) data = merge_dataframes_on_realization( dframe1=vector_df, @@ -407,21 +414,21 @@ def _update_timeseries_figure( @callback( Output( - self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( - BottomVisualizationViewElement.Ids.TABLE - ), + self.view_element(self.Ids.BOTTOM_VISUALIZATION) + .component_unique_id(BottomVisualizationViewElement.Ids.TABLE) + .to_string(), "data", ), Output( - self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( - BottomVisualizationViewElement.Ids.TABLE - ), + self.view_element(self.Ids.BOTTOM_VISUALIZATION) + .component_unique_id(BottomVisualizationViewElement.Ids.TABLE) + .to_string(), "columns", ), Output( - self.view_element(self.Ids.TORNADO_PLOT).component_unique_id( - GeneralViewElement.Ids.GRAPH - ), + self.view_element(self.Ids.TORNADO_PLOT) + .component_unique_id(GeneralViewElement.Ids.GRAPH) + .to_string(), "figure", ), Input( @@ -453,14 +460,17 @@ def _update_timeseries_figure( def _update_tornadoplot( date: str, selections: dict, vector: str, ensemble: str ) -> tuple: + print(selections) if selections is None or selections[ "Reference" ] not in self._data_model.get_unique_sensitivities_for_ensemble(ensemble): raise PreventUpdate # Get dataframe with vectors and dataframe with parameters and merge - vector_df = self._data_model.vmodel.get_vector_df( - ensemble=ensemble, vectors=[vector], date=date_from_str(date) + vector_df = self._data_model.get_vectors_df( + ensemble=ensemble, + date=date_from_str(date), + vector_names=[vector], ) data = merge_dataframes_on_realization( dframe1=vector_df, @@ -481,9 +491,9 @@ def _update_tornadoplot( @callback( Output( - self.view_element(self.Ids.BOTTOM_VISUALIZATION).component_unique_id( - BottomVisualizationViewElement.Ids.REAL_GRAPH - ), + self.view_element(self.Ids.BOTTOM_VISUALIZATION) + .component_unique_id(BottomVisualizationViewElement.Ids.REAL_GRAPH) + .to_string(), "figure", ), Input( @@ -529,8 +539,10 @@ def _update_realplot( raise PreventUpdate # Get dataframe with vectors and dataframe with parameters and merge - vector_df = self._data_model.vmodel.get_vector_df( - ensemble=ensemble, vectors=[vector], date=date_from_str(date) + vector_df = self._data_model.get_vectors_df( + ensemble=ensemble, + date=date_from_str(date), + vector_names=[vector], ) data = merge_dataframes_on_realization( dframe1=vector_df, @@ -540,21 +552,21 @@ def _update_realplot( ) tornado_data = self._data_model.get_tornado_data(data, vector, selections) - return self.data_model.create_realplot(tornado_data) + return self._data_model.create_realplot(tornado_data) @callback( Output( - self.view_element( - self.Ids.BOTTOM_VISUALIZATION, - BottomVisualizationViewElement.Ids.REAL_GRAPH_WRAPPER, - ), + self.view_element(self.Ids.BOTTOM_VISUALIZATION) + .component_unique_id( + BottomVisualizationViewElement.Ids.REAL_GRAPH_WRAPPER + ) + .to_string(), "style", ), Output( - self.view_element( - self.Ids.BOTTOM_VISUALIZATION, - BottomVisualizationViewElement.Ids.TABLE_WRAPPER, - ), + self.view_element(self.Ids.BOTTOM_VISUALIZATION) + .component_unique_id(BottomVisualizationViewElement.Ids.TABLE_WRAPPER) + .to_string(), "style", ), Input( From fe9e2cdb194be622a2f9c29ff1fe9b5e197a32db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Thu, 11 May 2023 15:11:55 +0200 Subject: [PATCH 09/19] Various improvements, made the time series figure local to the plugin to avoid problems with other plugins --- .../_components/tornado/_tornado_table.py | 2 + .../_utils/_onebyone_timeseries_figure.py | 364 ++++++++++++++++++ ...mulation_time_series_onebyone_datamodel.py | 17 +- .../_views/_onebyone_view/_view.py | 39 +- .../_view_elements/_general_view_element.py | 7 +- 5 files changed, 404 insertions(+), 25 deletions(-) create mode 100644 webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py diff --git a/webviz_subsurface/_components/tornado/_tornado_table.py b/webviz_subsurface/_components/tornado/_tornado_table.py index 8b0defad7..d80c07cee 100644 --- a/webviz_subsurface/_components/tornado/_tornado_table.py +++ b/webviz_subsurface/_components/tornado/_tornado_table.py @@ -23,6 +23,7 @@ def __init__( lambda x: str(len(x)) ) self._table["Response"] = tornado_data.response_name + self._table["Reference"] = tornado_data.reference_average self._table.rename( columns={ "sensname": "Sensitivity", @@ -64,6 +65,7 @@ def columns(self) -> List[Dict]: "True high", "Low #reals", "High #reals", + "Reference", ] ] diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py new file mode 100644 index 000000000..d80f6b168 --- /dev/null +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py @@ -0,0 +1,364 @@ +import datetime +from enum import Enum +from typing import Dict, List, Optional + +import numpy as np +import pandas as pd + +from webviz_subsurface._utils.colors import ( + find_intermediate_color, + hex_to_rgb, + rgb_to_str, + rgba_to_str, + scale_rgb_lightness, +) +from webviz_subsurface._utils.simulation_timeseries import ( + get_simulation_line_shape, + set_simulation_line_shape_fallback, +) + + +class Colors(str, Enum): + RED = rgba_to_str((255, 18, 67, 1)) + MID = rgba_to_str((220, 220, 220, 1)) + GREEN = rgba_to_str((62, 208, 62, 1)) + + +class OneByOneTimeSeriesFigure: + STAT_OPTIONS = ["Mean", "P10", "P90"] + + # pylint: disable=too-many-arguments + def __init__( + self, + dframe: pd.DataFrame, + visualization: str, + vector: str, + ensemble: str, + color_col: Optional[str], + line_shape_fallback: str, + observations: Optional[Dict] = None, + historical_vector_df: Optional[pd.DataFrame] = None, + dateline: Optional[datetime.datetime] = None, + discrete_color_map: Optional[Dict] = None, + groupby: Optional[str] = None, + ): + self.dframe = dframe + self.color_col = color_col + self.groupby = groupby + if color_col is not None and discrete_color_map is None: + self.dframe = self.normalize_parameter_value(self.dframe) + self.dframe = self.dframe.rename(columns={color_col: "VALUE"}) + self.vector = vector + self.ensemble = ensemble + self.visualization = visualization + self.historical_vector_df = historical_vector_df + self.date = dateline + self.observations = observations if observations is not None else {} + self.line_shape = self.get_line_shape(line_shape_fallback) + self.continous_color = self.color_col is not None and discrete_color_map is None + self.colormap = discrete_color_map if discrete_color_map is not None else {} + + self.create_traces() + + if self.observations: + self.create_vector_observation_traces() + + @property + def figure(self) -> dict: + title = self.vector + if self.color_col is not None: + title = f"{title} colored by {self.color_col}" + return { + "data": self.traces, + "layout": { + "margin": {"r": 40, "l": 20, "t": 60, "b": 20}, + "yaxis": {"automargin": True, "gridcolor": "#ECECEC"}, + "xaxis": {"range": self.daterange, "gridcolor": "#ECECEC"}, + "hovermode": "closest", + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "showlegend": False, + "uirevision": self.vector, + "title": {"text": title}, + "shapes": self.shapes, + "annotations": self.annotations, + }, + } + + def create_traces(self) -> None: + self.traces: List[dict] = [] + + if self.groupby == "SENSNAME_CASE": + if self.visualization == "realizations": + self._add_sensitivity_traces_real() + else: + self._add_sensitivity_traces_stat() + else: + if self.visualization == "realizations": + self._add_realization_traces() + if self.visualization == "statistics": + self._add_statistic_traces() + + self._add_history_trace() + + def _add_history_trace(self) -> None: + """Renders the history line""" + if self.historical_vector_df is None: + return + + self.traces.append( + { + "line": {"shape": self.line_shape, "color": "black"}, + "x": self.historical_vector_df["DATE"], + "y": self.historical_vector_df[self.vector], + "mode": "lines", + "hovertext": "History", + "hoverinfo": "y+x+text", + "name": "History", + } + ) + + def _add_statistic_traces(self) -> None: + """Renders the statistic lines""" + stat_df = self.create_vectors_statistics_df() + stat_df = pd.DataFrame(stat_df["DATE"]).join(stat_df[self.vector]) + self.traces.extend( + [ + { + "line": { + "width": 2, + "shape": self.line_shape, + "dash": False if stat == "Mean" else "dashdot", + "color": Colors.RED, + }, + "mode": "lines", + "x": stat_df["DATE"], + "y": stat_df[stat], + "name": stat, + "legendgroup": self.ensemble, + "showlegend": False, + } + for stat in self.STAT_OPTIONS + ] + ) + + def _add_realization_traces(self) -> None: + """Renders line trace for each realization""" + mean = self.dframe["VALUE_NORM"].mean() if self.continous_color else None + self.traces.extend( + [ + { + "line": { + "shape": self.line_shape, + "color": self.set_real_color( + real_df["VALUE_NORM"].iloc[0], mean + ) + if self.visualization == "realizations" and self.continous_color + else self.colormap.get(real_df[self.color_col].iloc[0], "grey"), + }, + "mode": "lines", + "x": real_df["DATE"], + "y": real_df[self.vector], + "name": self.ensemble, + "legendgroup": self.ensemble, + "hovertext": self.create_hovertext(real_df["VALUE"].iloc[0], real) + if self.continous_color + else f"Real: {real} {real_df[self.color_col].iloc[0]}", + "showlegend": real_idx == 0, + } + for real_idx, (real, real_df) in enumerate(self.dframe.groupby("REAL")) + ] + ) + + @property + def daterange(self) -> list: + active_dates = self.dframe["DATE"][self.dframe[self.vector] != 0] + if len(active_dates) == 0: + return [self.dframe["DATE"].min(), self.dframe["DATE"].max()] + if self.date is None: + return [active_dates.min(), active_dates.max()] + # Ensure xaxis covers selected date + return [min(active_dates.min(), self.date), max(active_dates.max(), self.date)] + + @property + def annotations(self) -> List[dict]: + return ( + [ + { + "bgcolor": "white", + "showarrow": False, + "text": self.date.strftime("%Y-%m-%d"), + "x": self.date, + "y": 1, + "yref": "y domain", + } + ] + if self.date is not None + else [] + ) + + @property + def shapes(self) -> List[dict]: + return ( + [ + { + "line": {"color": "#243746", "dash": "dot", "width": 4}, + "type": "line", + "x0": self.date, + "x1": self.date, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain", + } + ] + if self.date is not None + else [] + ) + + def create_hovertext(self, color_value: float, real: int) -> str: + return f"Real: {real}, {self.color_col}: {color_value}" + + def get_line_shape(self, line_shape_fallback: str) -> str: + return get_simulation_line_shape( + line_shape_fallback=set_simulation_line_shape_fallback(line_shape_fallback), + vector=self.vector, + smry_meta=None, + ) + + def create_vectors_statistics_df(self) -> pd.DataFrame: + return ( + self.dframe[["DATE", self.vector]] + .groupby(["DATE"]) + .agg( + [ + ("Mean", np.nanmean), + ("P10", lambda x: np.nanpercentile(x, q=90)), + ("P90", lambda x: np.nanpercentile(x, q=10)), + ] + ) + .reset_index() + ) + + def create_vector_observation_traces(self) -> None: + """Adds observations to the plot""" + + legend_group = "Observation" + name = "Observation" + color = "black" + show_legend = False + + for observation in self.observations.get("observations", []): + hovertext = observation.get("comment") + hovertemplate = ( + "(%{x}, %{y})
" + hovertext if hovertext else "(%{x}, %{y})
" + ) + self.traces.append( + { + "name": name, + "legendgroup": legend_group, + "x": [observation.get("date"), []], + "y": [observation.get("value"), []], + "marker": {"color": color}, + "hovertemplate": hovertemplate, + "showlegend": show_legend, + "error_y": { + "type": "data", + "array": [observation.get("error"), []], + "visible": True, + }, + } + ) + + def _add_sensitivity_traces_stat(self) -> None: + """Renders line trace for each realization""" + + self.dframe["dash"] = np.where( + self.dframe["t"] == 1, "dashdot", "solid" + ) # dot, dashdot + + self.traces.extend( + [ + { + "line": { + "dash": "dash" if real_df["t"].iloc[0] == 1 else "solid", + "shape": self.line_shape, + "color": self.colormap.get( + real_df[self.color_col].iloc[0], "grey" + ), + "width": 3 if real_df["t"].iloc[0] == 1 else 2, + }, + "mode": "lines", + "x": real_df["DATE"], + "y": real_df[self.vector], + "name": sens, + "legendgroup": sens, + "hovertext": f"Sens: {sens}", + } + for real_idx, (sens, real_df) in enumerate( + self.dframe.groupby("SENSNAME_CASE") + ) + ] + ) + + def _add_sensitivity_traces_real(self) -> None: + """Renders line trace for each realization""" + + self.dframe["dash"] = np.where( + self.dframe["t"] == 1, "longdash", "solid" + ) # dot, dashdot + + self.traces.extend( + [ + { + "line": { + "dash": "dash" if real_df["t"].iloc[0] == 1 else "solid", + "shape": self.line_shape, + "color": rgb_to_str( + scale_rgb_lightness( + hex_to_rgb( + self.colormap.get( + real_df[self.color_col].iloc[0], "grey" + ) + ), + 130 if real_df["t"].iloc[0] == 1 else 90, + ) + ), + "width": 3 if real_df["t"].iloc[0] == 1 else 2, + }, + "mode": "lines", + "x": real_df["DATE"], + "y": real_df[self.vector], + "name": sens, + "legendgroup": sens, + "hovertext": f"Real: {real}, Sens: {sens}", + "showlegend": real_idx == 0, + } + for real_idx, ((sens, real), real_df) in enumerate( + self.dframe.groupby(["SENSNAME_CASE", "REAL"]) + ) + ] + ) + + def normalize_parameter_value(self, df: pd.DataFrame) -> pd.DataFrame: + df["VALUE_NORM"] = (df[self.color_col] - df[self.color_col].min()) / ( + df[self.color_col].max() - df[self.color_col].min() + ) + return df + + @staticmethod + def set_real_color(norm_value: float, mean_param_value: float) -> str: + """ + Return color for trace based on normalized color_col value. + Midpoint for the colorscale is set on the average value + """ + + if norm_value <= mean_param_value: + intermed = norm_value / mean_param_value + return find_intermediate_color(Colors.RED, Colors.MID, intermed) + + if norm_value > mean_param_value: + intermed = (norm_value - mean_param_value) / (1 - mean_param_value) + return find_intermediate_color(Colors.MID, Colors.GREEN, intermed) + + return Colors.MID diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index d1505a14e..f4c07ad72 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -9,7 +9,7 @@ from webviz_subsurface._components.tornado._tornado_bar_chart import TornadoBarChart from webviz_subsurface._components.tornado._tornado_data import TornadoData from webviz_subsurface._components.tornado._tornado_table import TornadoTable -from webviz_subsurface._figures import TimeSeriesFigure, create_figure +from webviz_subsurface._figures import create_figure from webviz_subsurface._models.parameter_model import ParametersModel from webviz_subsurface._providers import EnsembleSummaryProvider, Frequency from webviz_subsurface._utils.ensemble_summary_provider_set import ( @@ -18,6 +18,7 @@ from webviz_subsurface._utils.vector_selector import add_vector_to_vector_selector_data from ._datetime_utils import date_from_str +from ._onebyone_timeseries_figure import OneByOneTimeSeriesFigure class SimulationTimeSeriesOneByOneDataModel: @@ -163,7 +164,11 @@ def get_tornado_data( ) def create_tornado_figure( - self, tornado_data: TornadoData, selections: dict, use_si_format: bool + self, + tornado_data: TornadoData, + selections: dict, + use_si_format: bool, + title: Optional[str], ) -> tuple: return ( TornadoBarChart( @@ -180,7 +185,9 @@ def create_tornado_figure( ) .figure.update_xaxes(side="bottom", title=None) .update_layout( - title_text=f"Tornadoplot for {tornado_data.response_name}
", + title_text=title + if title is not None + else f"Tornadoplot for {tornado_data.response_name}
", margin={"t": 70}, ) ) @@ -239,7 +246,7 @@ def create_timeseries_figure( visualization: str, ) -> go.Figure: return go.Figure( - TimeSeriesFigure( + OneByOneTimeSeriesFigure( dframe=dframe, visualization=visualization, vector=vector, @@ -253,7 +260,7 @@ def create_timeseries_figure( discrete_color_map=self.sensname_colormap, groupby="SENSNAME_CASE", ).figure - ).update_layout({"title": f"{vector}, Date: {date}"}) + ).update_layout({"title": f"{vector}"}) def create_vector_selector_data(vector_names: list) -> list: diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 3ac8ebf46..b0f94bd1e 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import plotly.graph_objects as go from dash import ALL, Input, Output, State, callback, ctx, html @@ -8,7 +8,7 @@ from webviz_subsurface._utils.dataframe_utils import merge_dataframes_on_realization -from ..._types import LabelOptions, LineType +from ..._types import LineType from ..._utils import ( SimulationTimeSeriesOneByOneDataModel, create_vector_selector_data, @@ -149,9 +149,12 @@ def _update_realization_store(sensitivites: list, ensemble: str) -> List[int]: ) @callback_typecheck def _update_sensitivity_filter( - tornado_click_data: dict, reference: str + tornado_click_data: Optional[Dict], reference: str ) -> List[str]: """Update graph with line coloring, vertical line and title""" + if tornado_click_data is None: + raise PreventUpdate + clicked_data = tornado_click_data["points"][0] return [clicked_data["y"], reference] @@ -309,7 +312,7 @@ def _update_vector_store(vector: list) -> str: def _update_date( ensemble: str, timeseries_clickdata: Union[None, dict], - dateidx: List[int], + dateidx: int, date: str, ) -> Tuple[str, html.Div]: """Store selected date and tornado input. Write statistics @@ -332,21 +335,17 @@ def _update_date( if new_ensemble: date = dates[-1] - - if timeseriesgraph_click: + elif timeseriesgraph_click: date = date_from_str(timeseries_clickdata.get("points", [{}])[0]["x"]) - - if dateslider_drag: - date = date_to_str(dates[dateidx[0]]) - - date_selected = ( - date_from_str(date) if date_from_str(date) in dates else dates[-1] - ) + elif dateslider_drag: + date = dates[dateidx] + else: + date = dates[-1] return ( - date_to_str(date_selected), - date_to_str(date_selected), - dates.index(date_selected), + date_to_str(date), + date_to_str(date), + dates.index(date), ) @callback( @@ -460,7 +459,6 @@ def _update_timeseries_figure( def _update_tornadoplot( date: str, selections: dict, vector: str, ensemble: str ) -> tuple: - print(selections) if selections is None or selections[ "Reference" ] not in self._data_model.get_unique_sensitivities_for_ensemble(ensemble): @@ -482,7 +480,10 @@ def _update_tornadoplot( tornado_data = self._data_model.get_tornado_data(data, vector, selections) use_si_format = tornado_data.reference_average > 1000 tornadofig = self._data_model.create_tornado_figure( - tornado_data, selections, use_si_format + tornado_data=tornado_data, + selections=selections, + use_si_format=use_si_format, + title=f"Tornadoplot for {tornado_data.response_name} at {date}
", ) table, columns = self._data_model.create_tornado_table( tornado_data, use_si_format @@ -530,7 +531,7 @@ def _update_tornadoplot( @callback_typecheck def _update_realplot( date: str, - selections: dict, + selections: Optional[Dict], vector: str, ensemble: str, selected_vizualisation: str, diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py index 0ad0b3446..42c44ed52 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view_elements/_general_view_element.py @@ -13,5 +13,10 @@ def __init__(self) -> None: def inner_layout(self) -> html.Div: return html.Div( - children=[wcc.Graph(id=self.register_component_unique_id(self.Ids.GRAPH))] + children=[ + wcc.Graph( + id=self.register_component_unique_id(self.Ids.GRAPH), + style={"height": "43.5vh"}, + ) + ] ) From c7e88d774c864025956aa2d5100b572368867473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Thu, 11 May 2023 15:39:12 +0200 Subject: [PATCH 10/19] pylint and mypy fixes --- .../_plugin.py | 3 +- ...mulation_time_series_onebyone_datamodel.py | 5 ++-- .../_onebyone_view/_settings/_selections.py | 4 +-- .../_views/_onebyone_view/_view.py | 29 ++++++++----------- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py index 63d1497f2..d4b694ce9 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py @@ -7,7 +7,6 @@ from webviz_subsurface._models.parameter_model import ParametersModel from webviz_subsurface._providers import ( - EnsembleSummaryProviderFactory, EnsembleTableProvider, EnsembleTableProviderFactory, Frequency, @@ -104,7 +103,7 @@ def __init__( parameterproviderset = { ens_name: table_provider.create_from_per_realization_parameter_file( - ens_path + str(ens_path) ) for ens_name, ens_path in ensemble_paths.items() } diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index f4c07ad72..965107c78 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -53,9 +53,8 @@ def test(x: pd.Series) -> pd.Series: ].transform(test) self._smry_meta = None - self._senscolormap = { - sens: color for sens, color in zip(self._pmodel.sensitivities, self.colors) - } + self._senscolormap = dict(zip(self._pmodel.sensitivities, self.colors)) + self.initial_vector = ( initial_vector if initial_vector and initial_vector in self._vectors diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py index e7e19b7dd..81c60d6b8 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_selections.py @@ -1,5 +1,5 @@ import datetime -from typing import List +from typing import Any, Dict, List import webviz_core_components as wcc import webviz_subsurface_components as wsc @@ -22,7 +22,7 @@ def __init__( self, ensembles: List[str], vectors: List[str], - vector_selector_data: dict, + vector_selector_data: List[Dict[str, Any]], dates: List[datetime.datetime], initial_vector: str, ) -> None: diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index b0f94bd1e..05cfebc44 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional, Tuple, Union import plotly.graph_objects as go -from dash import ALL, Input, Output, State, callback, ctx, html +from dash import ALL, Input, Output, State, callback, ctx from dash.exceptions import PreventUpdate from webviz_config.utils import StrEnum, callback_typecheck from webviz_config.webviz_plugin_subclasses import ViewABC @@ -302,19 +302,12 @@ def _update_vector_store(vector: list) -> str: ), "value", ), - State( - self.settings_group_unique_id( - self.Ids.SETTINGS, GeneralSettings.Ids.DATE_STORE - ), - "data", - ), ) def _update_date( ensemble: str, timeseries_clickdata: Union[None, dict], dateidx: int, - date: str, - ) -> Tuple[str, html.Div]: + ) -> Tuple[str, str, int]: """Store selected date and tornado input. Write statistics to table""" @@ -334,18 +327,20 @@ def _update_date( dates = self._data_model.ensemble_dates(ensemble) if new_ensemble: - date = dates[-1] - elif timeseriesgraph_click: - date = date_from_str(timeseries_clickdata.get("points", [{}])[0]["x"]) + date_selected = dates[-1] + elif timeseriesgraph_click and timeseries_clickdata is not None: + date_selected = date_from_str( + timeseries_clickdata.get("points", [{}])[0]["x"] + ) elif dateslider_drag: - date = dates[dateidx] + date_selected = dates[dateidx] else: - date = dates[-1] + date_selected = dates[-1] return ( - date_to_str(date), - date_to_str(date), - dates.index(date), + date_to_str(date_selected), + date_to_str(date_selected), + dates.index(date_selected), ) @callback( From 72668d1b652ec1fc6aa076791beb3b0755faefaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Fri, 12 May 2023 10:26:13 +0200 Subject: [PATCH 11/19] Fixed scale bug --- .../_types.py | 6 ++--- .../_settings/_general_settings.py | 22 ------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py index 702e8e15c..b97e9db00 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py @@ -7,9 +7,9 @@ class LineType(StrEnum): class ScaleType(StrEnum): - PERCENTAGE = "percentage" - ABSOLUTE = "absolute" - TRUE_VALUE = "true-value" + PERCENTAGE = "Percentage" + ABSOLUTE = "Absolute" + TRUE_VALUE = "True" class LabelOptions(StrEnum): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py index 60b2cac2f..9308ac6bf 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_general_settings.py @@ -13,11 +13,6 @@ class GeneralSettings(SettingsGroupABC): class Ids(StrEnum): - # SCALE_TYPE = "scale-type" - # CHECKBOX_SETTINGS = "checkbox-settings" - # LABEL_OPTIONS = "label-options" - # REFERENCE = "reference" - OPTIONS = "options" OPTIONS_STORE = "options-store" REAL_STORE = "real-store" @@ -45,7 +40,6 @@ def layout(self) -> List[Component]: wcc.Dropdown( label="Scale:", id={"id": options_id, "selector": "Scale"}, - # id=self.register_component_unique_id(self.Ids.SCALE_TYPE), options=[ {"label": "Relative value (%)", "value": ScaleType.PERCENTAGE}, {"label": "Relative value", "value": ScaleType.ABSOLUTE}, @@ -54,20 +48,6 @@ def layout(self) -> List[Component]: value=ScaleType.PERCENTAGE, clearable=False, ), - # wcc.Checklist( - # id=self.register_component_unique_id(self.Ids.CHECKBOX_SETTINGS), - # style={"margin-top": "10px"}, - # options=[ - # {"label": "Color by sensitivity", "value": "color-by-sens"}, - # {"label": "Show realization points", "value": "real-scatter"}, - # {"label": "Show reference on tornado", "value": "show-tornado-ref"}, - # { - # "label": "Remove sensitivities with no impact", - # "value": "remove-no-impact", - # }, - # ], - # value=["color-by-sens", "show-tornado-ref", "remove-no-impact"], - # ), html.Div( style={"margin-top": "10px", "margin-bottom": "10px"}, children=[ @@ -90,7 +70,6 @@ def layout(self) -> List[Component]: ), wcc.RadioItems( label="Label options:", - # id=self.register_component_unique_id(self.Ids.LABEL_OPTIONS), id={"id": options_id, "selector": "labeloptions"}, options=[ {"label": "Detailed", "value": LabelOptions.DETAILED}, @@ -102,7 +81,6 @@ def layout(self) -> List[Component]: ), wcc.Dropdown( label="Reference:", - # id=self.register_component_unique_id(self.Ids.REFERENCE), id={"id": options_id, "selector": "Reference"}, options=[{"label": elm, "value": elm} for elm in self._sensitivities], value=self._ref_sens, From 21401eb62efe445f0f4ffe98362de8f70878ec13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 15 May 2023 10:10:40 +0200 Subject: [PATCH 12/19] Various improvements --- .../_types.py | 4 +- .../_settings/_vizualisation.py | 2 +- .../_views/_onebyone_view/_view.py | 37 +++++++++++++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py index b97e9db00..41d35fc99 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_types.py @@ -2,8 +2,8 @@ class LineType(StrEnum): - REALIZATION = "realization" - MEAN = "mean" + REALIZATION = "realizations" + STATISTICS = "statistics" class ScaleType(StrEnum): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py index a830f0604..cfae13eff 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_settings/_vizualisation.py @@ -25,7 +25,7 @@ def layout(self) -> List[Component]: id=self.register_component_unique_id(self.Ids.REALIZATION_OR_MEAN), options=[ {"label": "Individual realizations", "value": LineType.REALIZATION}, - {"label": "Mean over Sensitivities", "value": LineType.MEAN}, + {"label": "Mean over Sensitivities", "value": LineType.STATISTICS}, ], value=LineType.REALIZATION, ), diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 05cfebc44..555bd3ad6 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -1,4 +1,5 @@ -from typing import Dict, List, Optional, Tuple, Union +import datetime +from typing import Any, Dict, List, Optional, Tuple, Union import plotly.graph_objects as go from dash import ALL, Input, Output, State, callback, ctx @@ -284,6 +285,18 @@ def _update_vector_store(vector: list) -> str: ), "value", ), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ), + "max", + ), + Output( + self.settings_group_unique_id( + self.Ids.SELECTIONS, Selections.Ids.DATE_SLIDER + ), + "marks", + ), Input( self.settings_group_unique_id( self.Ids.SELECTIONS, Selections.Ids.ENSEMBLE @@ -307,7 +320,7 @@ def _update_date( ensemble: str, timeseries_clickdata: Union[None, dict], dateidx: int, - ) -> Tuple[str, str, int]: + ) -> Tuple[str, str, int, int, Dict[int, Dict[str, Any]]]: """Store selected date and tornado input. Write statistics to table""" @@ -332,6 +345,9 @@ def _update_date( date_selected = date_from_str( timeseries_clickdata.get("points", [{}])[0]["x"] ) + if date_selected not in dates: + date_selected = get_closest_date(dates, date_selected) + elif dateslider_drag: date_selected = dates[dateidx] else: @@ -341,6 +357,14 @@ def _update_date( date_to_str(date_selected), date_to_str(date_selected), dates.index(date_selected), + len(dates) - 1, + { + idx: { + "label": date_to_str(dates[idx]), + "style": {"white-space": "nowrap"}, + } + for idx in [0, len(dates) - 1] + }, ) @callback( @@ -399,7 +423,7 @@ def _update_timeseries_figure( ensemble ), ) - if linetype == LineType.MEAN: + if linetype == LineType.STATISTICS: data = self._data_model.create_vectors_statistics_df(data) return self._data_model.create_timeseries_figure( @@ -577,3 +601,10 @@ def _display_table_or_realplot(selected_vizualisation: str) -> tuple: return { "display": "none" if selected_vizualisation == "table" else "block" }, {"display": "block" if selected_vizualisation == "table" else "none"} + + +def get_closest_date( + dates: List[datetime.datetime], date: datetime.datetime +) -> datetime.datetime: + # Returns the closest date to the input date in the dates list. + return min(dates, key=lambda dte: abs(dte - date)) From 6bcff25c1e12d470bdbc65aa91916676fd9e4c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 15 May 2023 10:33:23 +0200 Subject: [PATCH 13/19] Changelog entry --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca037d62..8a634ad23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] - YYYY-MM-DD -## [0.2.19] - 2023-05-05 +### Added +- [#1217](https://github.com/equinor/webviz-subsurface/pull/1207) - New plugin `SimulationTimeSeriesOneByOne`, meant to replace the old `ReservoirSimulationTimeSeriesOneByOne`. Uses the `.arrow` summary provider and is implemented with WLF (Webviz Layout Framework). + + +## [0.2.19] - YYYY-MM-DD ### Changed From 17a48772c9be16d01cafc4beea1620d6f5af4c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 15 May 2023 10:45:01 +0200 Subject: [PATCH 14/19] Fixed pylint issue --- .../_utils/__init__.py | 2 + ...mulation_time_series_onebyone_datamodel.py | 49 ++++++++++--------- .../_views/_onebyone_view/_view.py | 10 ++-- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py index 09f7d4867..19168c7b2 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/__init__.py @@ -1,5 +1,7 @@ from ._datetime_utils import date_from_str, date_to_str from ._simulation_time_series_onebyone_datamodel import ( SimulationTimeSeriesOneByOneDataModel, + create_tornado_table, create_vector_selector_data, + get_tornado_data, ) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index 965107c78..f22792005 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -150,18 +150,6 @@ def get_tornado_reference(sensitivities: List[str], existing_reference: str) -> return "rms_seed" return sensitivities[0] - def get_tornado_data( - self, dframe: pd.DataFrame, response: str, selections: dict - ) -> TornadoData: - dframe.rename(columns={response: "VALUE"}, inplace=True) - return TornadoData( - dframe=dframe, - reference=selections["Reference"], - response_name=response, - scale=selections["Scale"], - cutbyref=bool(selections["Remove no impact"]), - ) - def create_tornado_figure( self, tornado_data: TornadoData, @@ -224,18 +212,6 @@ def create_realplot(self, tornado_data: TornadoData) -> go.Figure: ) ) - def create_tornado_table( - self, - tornado_data: TornadoData, - use_si_format: bool, - ) -> Tuple[List[dict], List[dict]]: - tornado_table = TornadoTable( - tornado_data=tornado_data, - use_si_format=use_si_format, - precision=4 if use_si_format else 3, - ) - return tornado_table.as_plotly_table, tornado_table.columns - def create_timeseries_figure( self, dframe: pd.DataFrame, @@ -262,6 +238,31 @@ def create_timeseries_figure( ).update_layout({"title": f"{vector}"}) +def get_tornado_data( + dframe: pd.DataFrame, response: str, selections: dict +) -> TornadoData: + dframe.rename(columns={response: "VALUE"}, inplace=True) + return TornadoData( + dframe=dframe, + reference=selections["Reference"], + response_name=response, + scale=selections["Scale"], + cutbyref=bool(selections["Remove no impact"]), + ) + + +def create_tornado_table( + tornado_data: TornadoData, + use_si_format: bool, +) -> Tuple[List[dict], List[dict]]: + tornado_table = TornadoTable( + tornado_data=tornado_data, + use_si_format=use_si_format, + precision=4 if use_si_format else 3, + ) + return tornado_table.as_plotly_table, tornado_table.columns + + def create_vector_selector_data(vector_names: list) -> list: vector_selector_data: list = [] for vector in _get_non_historical_vector_names(vector_names): diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py index 555bd3ad6..43e7eea72 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_views/_onebyone_view/_view.py @@ -12,9 +12,11 @@ from ..._types import LineType from ..._utils import ( SimulationTimeSeriesOneByOneDataModel, + create_tornado_table, create_vector_selector_data, date_from_str, date_to_str, + get_tornado_data, ) from ._settings import GeneralSettings, Selections, SensitivityFilter, Visualization from ._view_elements import BottomVisualizationViewElement, GeneralViewElement @@ -496,7 +498,7 @@ def _update_tornadoplot( ), ) - tornado_data = self._data_model.get_tornado_data(data, vector, selections) + tornado_data = get_tornado_data(data, vector, selections) use_si_format = tornado_data.reference_average > 1000 tornadofig = self._data_model.create_tornado_figure( tornado_data=tornado_data, @@ -504,9 +506,7 @@ def _update_tornadoplot( use_si_format=use_si_format, title=f"Tornadoplot for {tornado_data.response_name} at {date}
", ) - table, columns = self._data_model.create_tornado_table( - tornado_data, use_si_format - ) + table, columns = create_tornado_table(tornado_data, use_si_format) return table, columns, tornadofig @callback( @@ -570,7 +570,7 @@ def _update_realplot( ensemble ), ) - tornado_data = self._data_model.get_tornado_data(data, vector, selections) + tornado_data = get_tornado_data(data, vector, selections) return self._data_model.create_realplot(tornado_data) From aa489d6cbb7f5467299ebc65c1b2f3c894bd1030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 15 May 2023 10:59:16 +0200 Subject: [PATCH 15/19] fixed changelog issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a634ad23..c8eb049ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1217](https://github.com/equinor/webviz-subsurface/pull/1207) - New plugin `SimulationTimeSeriesOneByOne`, meant to replace the old `ReservoirSimulationTimeSeriesOneByOne`. Uses the `.arrow` summary provider and is implemented with WLF (Webviz Layout Framework). -## [0.2.19] - YYYY-MM-DD +## [0.2.19] - 2023-05-05 ### Changed From 3d63091aaa895adcc0d556b0e0a1bb3224f8f140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 15 May 2023 12:00:29 +0200 Subject: [PATCH 16/19] Added test --- .../test_simulation_timeseries_onebyone.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/integration_tests/plugin_tests/test_simulation_timeseries_onebyone.py diff --git a/tests/integration_tests/plugin_tests/test_simulation_timeseries_onebyone.py b/tests/integration_tests/plugin_tests/test_simulation_timeseries_onebyone.py new file mode 100644 index 000000000..5c4f23344 --- /dev/null +++ b/tests/integration_tests/plugin_tests/test_simulation_timeseries_onebyone.py @@ -0,0 +1,23 @@ +import warnings + +# pylint: disable=no-name-in-module +from webviz_config.plugins import SimulationTimeSeriesOneByOne +from webviz_config.testing import WebvizComposite + + +def test_simulation_timeseries_onebyone( + _webviz_duo: WebvizComposite, shared_settings: dict +) -> None: + plugin = SimulationTimeSeriesOneByOne( + webviz_settings=shared_settings["SENS_SETTINGS"], + ensembles=shared_settings["SENS_ENSEMBLES"], + initial_vector="FOPT", + ) + _webviz_duo.start_server(plugin) + logs = [] + for log in _webviz_duo.get_logs(): + if "dash_renderer" in log.get("message"): + warnings.warn(log.get("message")) + else: + logs.append(log) + assert not logs From 2ad685ecb0935adeef08cc9d29554e145b01c5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Tue, 16 May 2023 15:26:06 +0200 Subject: [PATCH 17/19] Deprecated ReservoirSimulationTimeseriesOnyByOne plugin --- .../plugins/_reservoir_simulation_timeseries_onebyone.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py b/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py index 467e83487..5d4cb846f 100644 --- a/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py +++ b/webviz_subsurface/plugins/_reservoir_simulation_timeseries_onebyone.py @@ -11,6 +11,7 @@ from dash.exceptions import PreventUpdate from webviz_config import WebvizPluginABC, WebvizSettings from webviz_config.common_cache import CACHE +from webviz_config.deprecation_decorators import deprecated_plugin from webviz_config.webviz_store import webvizstore from webviz_subsurface._components import TornadoWidget @@ -33,6 +34,9 @@ # pylint: disable=too-many-instance-attributes +@deprecated_plugin( + "This plugin has been replaced by the plugin `SimulationTimeSeriesOneByOne`" +) class ReservoirSimulationTimeSeriesOneByOne(WebvizPluginABC): """Visualizes reservoir simulation time series data for sensitivity studies based \ on a design matrix. From 1281963ee4603d22415b180d7b890fc70e9a702d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 22 May 2023 08:45:09 +0200 Subject: [PATCH 18/19] Review related updates --- .../_plugin.py | 20 ++--- .../_utils/_onebyone_timeseries_figure.py | 84 ++++--------------- ...mulation_time_series_onebyone_datamodel.py | 8 +- 3 files changed, 23 insertions(+), 89 deletions(-) diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py index d4b694ce9..f6579b9b0 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_plugin.py @@ -27,13 +27,12 @@ class SimulationTimeSeriesOneByOne(WebvizPluginABC): After selecting a date individual sensitivities can be selected to highlight the realizations run with that sensitivity. --- -**Using simulation time series data directly from `UNSMRY` files** +**Input arguments** * **`ensembles`:** Which ensembles in `shared_settings` to visualize. -* **`column_keys`:** List of vectors to extract. If not given, all vectors \ - from the simulations will be extracted. Wild card asterisk `*` can be used. +* **`rel_file_pattern`:** Path to `.arrow` files with summary data. * **`sampling`:** Time separation between extracted values. Can be e.g. `monthly` (default) or \ `yearly`. -**Common optional settings for both input options** +* **`perform_presampling`:** Presample summary data instead of lazy sampling. * **`initial_vector`:** Initial vector to display * **`line_shape_fallback`:** Fallback interpolation method between points. Vectors identified as \ rates or phase ratios are always backfilled, vectors identified as cumulative (totals) are \ @@ -42,15 +41,6 @@ class SimulationTimeSeriesOneByOne(WebvizPluginABC): * `linear` (default) * `backfilled` * `hv`, `vh`, `hvh`, `vhv` and `spline` (regular Plotly options). -**Using simulation time series data directly from `.UNSMRY` files** -Time series data are extracted automatically from the `UNSMRY` files in the individual -realizations, using the `fmu-ensemble` library. The `SENSNAME` and `SENSCASE` values are read -directly from the `parameters.txt` files of the individual realizations, assuming that these -exist. If the `SENSCASE` of a realization is `p10_p90`, the sensitivity case is regarded as a -**Monte Carlo** style sensitivity, otherwise the case is evaluated as a **scalar** sensitivity. -?> Using the `UNSMRY` method will also extract metadata like units, and whether the vector is a \ -rate, a cumulative, or historical. Units are e.g. added to the plot titles, while rates and \ -cumulatives are used to decide the line shapes in the plot. """ class Ids(StrEnum): @@ -60,8 +50,8 @@ def __init__( self, webviz_settings: WebvizSettings, ensembles: list, - time_index: str = "monthly", rel_file_pattern: str = "share/results/unsmry/*.arrow", + sampling: str = Frequency.MONTHLY.value, perform_presampling: bool = False, initial_vector: str = None, line_shape_fallback: str = "linear", @@ -70,7 +60,7 @@ def __init__( # vectormodel: ProviderTimeSeriesDataModel table_provider = EnsembleTableProviderFactory.instance() - resampling_frequency = Frequency(time_index) + resampling_frequency = Frequency(sampling) if ensembles is not None: ensemble_paths: Dict[str, Path] = { diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py index d80f6b168..29445b679 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_onebyone_timeseries_figure.py @@ -88,16 +88,10 @@ def figure(self) -> dict: def create_traces(self) -> None: self.traces: List[dict] = [] - if self.groupby == "SENSNAME_CASE": - if self.visualization == "realizations": - self._add_sensitivity_traces_real() - else: - self._add_sensitivity_traces_stat() + if self.visualization == "realizations": + self._add_sensitivity_traces_real() else: - if self.visualization == "realizations": - self._add_realization_traces() - if self.visualization == "statistics": - self._add_statistic_traces() + self._add_sensitivity_traces_stat() self._add_history_trace() @@ -118,61 +112,9 @@ def _add_history_trace(self) -> None: } ) - def _add_statistic_traces(self) -> None: - """Renders the statistic lines""" - stat_df = self.create_vectors_statistics_df() - stat_df = pd.DataFrame(stat_df["DATE"]).join(stat_df[self.vector]) - self.traces.extend( - [ - { - "line": { - "width": 2, - "shape": self.line_shape, - "dash": False if stat == "Mean" else "dashdot", - "color": Colors.RED, - }, - "mode": "lines", - "x": stat_df["DATE"], - "y": stat_df[stat], - "name": stat, - "legendgroup": self.ensemble, - "showlegend": False, - } - for stat in self.STAT_OPTIONS - ] - ) - - def _add_realization_traces(self) -> None: - """Renders line trace for each realization""" - mean = self.dframe["VALUE_NORM"].mean() if self.continous_color else None - self.traces.extend( - [ - { - "line": { - "shape": self.line_shape, - "color": self.set_real_color( - real_df["VALUE_NORM"].iloc[0], mean - ) - if self.visualization == "realizations" and self.continous_color - else self.colormap.get(real_df[self.color_col].iloc[0], "grey"), - }, - "mode": "lines", - "x": real_df["DATE"], - "y": real_df[self.vector], - "name": self.ensemble, - "legendgroup": self.ensemble, - "hovertext": self.create_hovertext(real_df["VALUE"].iloc[0], real) - if self.continous_color - else f"Real: {real} {real_df[self.color_col].iloc[0]}", - "showlegend": real_idx == 0, - } - for real_idx, (real, real_df) in enumerate(self.dframe.groupby("REAL")) - ] - ) - @property def daterange(self) -> list: - active_dates = self.dframe["DATE"][self.dframe[self.vector] != 0] + active_dates = self.dframe["DATE"][self.dframe[self.vector].round(4) != 0] if len(active_dates) == 0: return [self.dframe["DATE"].min(), self.dframe["DATE"].max()] if self.date is None: @@ -274,19 +216,21 @@ def _add_sensitivity_traces_stat(self) -> None: """Renders line trace for each realization""" self.dframe["dash"] = np.where( - self.dframe["t"] == 1, "dashdot", "solid" + self.dframe["SENSCASEID"] == 1, "dashdot", "solid" ) # dot, dashdot self.traces.extend( [ { "line": { - "dash": "dash" if real_df["t"].iloc[0] == 1 else "solid", + "dash": "dash" + if real_df["SENSCASEID"].iloc[0] == 1 + else "solid", "shape": self.line_shape, "color": self.colormap.get( real_df[self.color_col].iloc[0], "grey" ), - "width": 3 if real_df["t"].iloc[0] == 1 else 2, + "width": 3 if real_df["SENSCASEID"].iloc[0] == 1 else 2, }, "mode": "lines", "x": real_df["DATE"], @@ -305,14 +249,16 @@ def _add_sensitivity_traces_real(self) -> None: """Renders line trace for each realization""" self.dframe["dash"] = np.where( - self.dframe["t"] == 1, "longdash", "solid" + self.dframe["SENSCASEID"] == 1, "longdash", "solid" ) # dot, dashdot self.traces.extend( [ { "line": { - "dash": "dash" if real_df["t"].iloc[0] == 1 else "solid", + "dash": "dash" + if real_df["SENSCASEID"].iloc[0] == 1 + else "solid", "shape": self.line_shape, "color": rgb_to_str( scale_rgb_lightness( @@ -321,10 +267,10 @@ def _add_sensitivity_traces_real(self) -> None: real_df[self.color_col].iloc[0], "grey" ) ), - 130 if real_df["t"].iloc[0] == 1 else 90, + 130 if real_df["SENSCASEID"].iloc[0] == 1 else 90, ) ), - "width": 3 if real_df["t"].iloc[0] == 1 else 2, + "width": 3 if real_df["SENSCASEID"].iloc[0] == 1 else 2, }, "mode": "lines", "x": real_df["DATE"], diff --git a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py index f22792005..360c9a09b 100644 --- a/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py +++ b/webviz_subsurface/plugins/_simulation_time_series_onebyone/_utils/_simulation_time_series_onebyone_datamodel.py @@ -26,8 +26,6 @@ class SimulationTimeSeriesOneByOneDataModel: data providing methods. """ - SELECTORS = ["QC_FLAG", "SATNUM", "EQLNUM", "FIPNUM"] - def __init__( self, provider_set: EnsembleSummaryProviderSet, @@ -45,12 +43,12 @@ def __init__( self._line_shape_fallback = line_shape_fallback self._parameter_df = parametermodel.sens_df.copy() - def test(x: pd.Series) -> pd.Series: + def create_senscase_id(x: pd.Series) -> pd.Series: return x.apply(lambda v: list(x.unique()).index(v)) - self._parameter_df["t"] = self._parameter_df.groupby("SENSNAME")[ + self._parameter_df["SENSCASEID"] = self._parameter_df.groupby("SENSNAME")[ "SENSCASE" - ].transform(test) + ].transform(create_senscase_id) self._smry_meta = None self._senscolormap = dict(zip(self._pmodel.sensitivities, self.colors)) From 6e36334d9943ec599136684b45d52a5fadd27d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= Date: Mon, 22 May 2023 09:41:03 +0200 Subject: [PATCH 19/19] Fixed link in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8eb049ad..1f338cbb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] - YYYY-MM-DD ### Added -- [#1217](https://github.com/equinor/webviz-subsurface/pull/1207) - New plugin `SimulationTimeSeriesOneByOne`, meant to replace the old `ReservoirSimulationTimeSeriesOneByOne`. Uses the `.arrow` summary provider and is implemented with WLF (Webviz Layout Framework). +- [#1217](https://github.com/equinor/webviz-subsurface/pull/1217) - New plugin `SimulationTimeSeriesOneByOne`, meant to replace the old `ReservoirSimulationTimeSeriesOneByOne`. Uses the `.arrow` summary provider and is implemented with WLF (Webviz Layout Framework). ## [0.2.19] - 2023-05-05