diff --git a/CHANGELOG.md b/CHANGELOG.md index 1daa40832..f1fdde263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1195](https://github.com/equinor/webviz-subsurface/pull/1195) - `RftPlotter` faultlines argument can now use fault polygons csv file with *X, Y, ID* header (fmu-dataio default) - [#1196](https://github.com/equinor/webviz-subsurface/pull/1196) - `SwatinitQC` faultlines argument can now use fault polygons csv file with *X, Y, ID* header (fmu-dataio default) +### Added +- [#1199](https://github.com/equinor/webviz-subsurface/pull/1199) - Added more statistical options to the WellOverview tab in `WellAnalysis`, and the possibility to see injection rates. + ## [0.2.17] - 2023-01-18 ### Changed diff --git a/webviz_subsurface/plugins/_well_analysis/_types.py b/webviz_subsurface/plugins/_well_analysis/_types.py index 42d34fad6..7fea8accd 100644 --- a/webviz_subsurface/plugins/_well_analysis/_types.py +++ b/webviz_subsurface/plugins/_well_analysis/_types.py @@ -16,3 +16,13 @@ class ChartType(StrEnum): BAR = "bar" PIE = "pie" AREA = "area" + + +class StatType(StrEnum): + MEAN = "mean" + P10 = "p10" + P50 = "p50" + P90 = "p90" + MAX = "max" + MIN = "min" + P10_MINUS_P90 = "p10-p90" diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py index bb0be2f9d..8539f5e89 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py @@ -145,7 +145,7 @@ def add_network_pressure_traces( "type": "scatter", "x": [], "y": [], - "line": dict(color=next(color_iterator)), + "line": {"color": next(color_iterator)}, "name": label, "showlegend": True, "hovertext": (f"{label}"), @@ -249,7 +249,7 @@ def add_area_trace( mode="lines", # fill="tonexty", # fillcolor=color, - line=dict(width=linewidth, color=color), + line={"width": linewidth, "color": color}, name=name, text=name, stackgroup="one", diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/__init__.py index d3ef722ed..3326940ee 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/__init__.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/__init__.py @@ -2,3 +2,4 @@ from ._filters import WellOverviewFilters from ._layout_options import WellOverviewLayoutOptions from ._selections import WellOverviewSelections +from ._statistical_options import WellOverviewStatisticalOptions diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_layout_options.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_layout_options.py index 48db9745d..a933f10fb 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_layout_options.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_layout_options.py @@ -38,6 +38,11 @@ def layout(self) -> List[Component]: "label": "White background", "value": "white_background", }, + { + "label": "Error bars (P10/P90)", + "value": "errorbars", + "disabled": False, + }, ], value=["legend"], ), diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_selections.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_selections.py index 749c7cd97..ce25c2683 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_selections.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_selections.py @@ -36,6 +36,8 @@ def layout(self) -> List[Component]: {"label": "Oil production", "value": "WOPT"}, {"label": "Gas production", "value": "WGPT"}, {"label": "Water production", "value": "WWPT"}, + {"label": "Water injection", "value": "WWIT"}, + {"label": "Gas injection", "value": "WGIT"}, ], value="WOPT", multi=False, diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_statistical_options.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_statistical_options.py new file mode 100644 index 000000000..b672d66f9 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_settings/_statistical_options.py @@ -0,0 +1,41 @@ +from typing import List + +import webviz_core_components as wcc +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 ...._types import StatType + + +class WellOverviewStatisticalOptions(SettingsGroupABC): + class Ids(StrEnum): + STATISTICS = "statistics" + + def __init__(self) -> None: + super().__init__("Statistics") + + def layout(self) -> List[Component]: + return [ + html.Div( + children=[ + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.STATISTICS), + options=[ + {"label": "Mean", "value": StatType.MEAN}, + {"label": "P10 (high)", "value": StatType.P10}, + { + "label": "P50 (median)", + "value": StatType.P50, + }, + {"label": "P90 (low)", "value": StatType.P90}, + {"label": "Maximum", "value": StatType.MAX}, + {"label": "Minimum", "value": StatType.MIN}, + {"label": "P10 - P90", "value": StatType.P10_MINUS_P90}, + ], + value=StatType.MEAN, + ) + ], + ) + ] diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py index add63d24d..229a8c4e4 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py @@ -8,7 +8,7 @@ from plotly.subplots import make_subplots from webviz_config import WebvizConfigTheme -from ...._types import ChartType +from ...._types import ChartType, StatType from ...._utils import EnsembleWellAnalysisData @@ -21,6 +21,7 @@ def __init__( prod_from_date: Union[datetime.datetime, None], prod_until_date: Union[datetime.datetime, None], charttype: ChartType, + stattype: StatType, wells: List[str], theme: WebvizConfigTheme, ) -> None: @@ -31,6 +32,7 @@ def __init__( self._prod_from_date = prod_from_date self._prod_until_date = prod_until_date self._charttype = charttype + self._stattype = stattype self._wells = wells self._colors = theme.plotly_theme["layout"]["colorway"] self._rows, self._cols = self.get_subplot_dim() @@ -78,8 +80,7 @@ def _get_ensemble_charttype_data(self, ensemble: str) -> pd.DataFrame: prod_until_date=self._prod_until_date, ) df = df[df["WELL"].isin(self._wells)] - df_mean = df.groupby("WELL").mean().reset_index() - return df_mean[df_mean[self._sumvec] > 0] + return df # else chart type == area df = self._data_models[ensemble].get_summary_data( @@ -87,7 +88,36 @@ def _get_ensemble_charttype_data(self, ensemble: str) -> pd.DataFrame: prod_from_date=self._prod_from_date, prod_until_date=self._prod_until_date, ) - return df.groupby("DATE").mean().reset_index() + return df + + def _calc_statistics( + self, df: pd.DataFrame, groupby: str, stattype: StatType + ) -> pd.DataFrame: + """Calculates statistics from input dataframe""" + df_grouped = df.groupby(groupby) + + if stattype == StatType.MEAN: + df_out = df_grouped.mean(numeric_only=True).reset_index() + if stattype == StatType.P50: + df_out = df_grouped.quantile(0.5, numeric_only=True).reset_index() + if stattype == StatType.P10: + df_out = df_grouped.quantile(0.9, numeric_only=True).reset_index() + if stattype == StatType.P90: + df_out = df_grouped.quantile(0.1, numeric_only=True).reset_index() + if stattype == StatType.MAX: + df_out = df_grouped.max(0.1).reset_index() + if stattype == StatType.MIN: + df_out = df_grouped.min(0.1).reset_index() + if stattype == StatType.P10_MINUS_P90: + df_p10 = self._calc_statistics(df, groupby, StatType.P10) + df_p90 = self._calc_statistics(df, groupby, StatType.P90) + df_merged = df_p10.merge(df_p90, on=groupby) + for col in df_p10.columns: + if col in ["DATE", "REAL", groupby]: + continue + df_merged[col] = df_merged[f"{col}_x"] - df_merged[f"{col}_y"] + df_out = df_merged + return df_out def _add_traces(self) -> None: """Add all traces for the currently selected chart type.""" @@ -97,10 +127,12 @@ def _add_traces(self) -> None: df = self._get_ensemble_charttype_data(ensemble) if self._charttype == ChartType.PIE: + df_stat = self._calc_statistics(df, "WELL", self._stattype) + df_stat = df_stat[df_stat[self._sumvec] > 0] self._figure.add_trace( go.Pie( - values=df[self._sumvec], - labels=df["WELL"], + values=df_stat[self._sumvec], + labels=df_stat["WELL"], marker_colors=self._colors, textposition="inside", texttemplate="%{label}", @@ -110,24 +142,44 @@ def _add_traces(self) -> None: ) elif self._charttype == ChartType.BAR: + df_stat = self._calc_statistics(df, "WELL", self._stattype) + df_stat = df_stat[df_stat[self._sumvec] > 0] + trace = { - "x": df["WELL"], - "y": df[self._sumvec], + "x": df_stat["WELL"], + "y": df_stat[self._sumvec], "orientation": "v", "type": "bar", "name": ensemble, "marker": {"color": self._colors[i]}, - "text": df[self._sumvec], + "text": df_stat[self._sumvec], "textposition": "none", "texttemplate": "%{text:.2s}", } + if self._stattype is not StatType.P10_MINUS_P90: + # Add error bars + wells = df_stat["WELL"].unique() + df_p10 = self._calc_statistics(df, "WELL", StatType.P10) + df_p10 = df_p10[df_p10["WELL"].isin(wells)] + df_p90 = self._calc_statistics(df, "WELL", StatType.P90) + df_p90 = df_p90[df_p90["WELL"].isin(wells)] + trace["error_y"] = { + "type": "data", + "symmetric": False, + "array": df_p10[self._sumvec] - df_stat[self._sumvec], + "arrayminus": df_stat[self._sumvec] - df_p90[self._sumvec], + "visible": False, + } + self._figure.add_trace( trace, row=1, col=1, ) + elif self._charttype == ChartType.AREA: + df_stat = self._calc_statistics(df, "DATE", self._stattype) color_iterator = itertools.cycle(self._colors) for well in self._data_models[ensemble].wells: @@ -139,14 +191,14 @@ def _add_traces(self) -> None: self._figure.add_trace( go.Scatter( - x=df["DATE"], - y=df[f"{self._sumvec}:{well}"], + x=df_stat["DATE"], + y=df_stat[f"{self._sumvec}:{well}"], hoverinfo="text+x+y", hoveron="fills", mode="lines", stackgroup="one", name=well, - line=dict(width=0.1, color=next(color_iterator)), + line={"width": 0.1, "color": next(color_iterator)}, legendgroup="Wells", showlegend=showlegend, ), @@ -158,6 +210,7 @@ def _add_traces(self) -> None: def format_well_overview_figure( figure: go.Figure, charttype: ChartType, + stattype: StatType, settings: List[str], sumvec: str, prod_from_date: Union[str, None], @@ -186,6 +239,11 @@ def format_well_overview_figure( figure.update_traces( textposition=("auto" if "show_prod_text" in settings else "none") ) + if stattype == StatType.P10_MINUS_P90: + # Error bars doesn't make sens for the P10 - P90 option. + figure.update_traces(error_y={"visible": False}) + else: + figure.update_traces(error_y={"visible": "errorbars" in settings}) # These are valid for all chart types figure.update_layout( @@ -193,12 +251,28 @@ def format_well_overview_figure( ) # Make title - phase = {"WOPT": "Oil", "WGPT": "Gas", "WWPT": "Water"}[sumvec] - title = f"Cumulative Well {phase} Production (Sm3)" + phase = { + "WOPT": "Oil Production", + "WGPT": "Gas Production", + "WWPT": "Water Production", + "WWIT": "Water Injection", + "WGIT": "Gas Injection", + }[sumvec] + title = f"Cumulative Well {phase} (Sm3)" if prod_from_date is not None: title += f" from {prod_from_date}" if prod_until_date is not None: title += f" until {prod_until_date}" + stattype_text = { + StatType.MEAN: "Mean", + StatType.P10: "P10", + StatType.P50: "P50", + StatType.P90: "P90", + StatType.MAX: "Maximum", + StatType.MIN: "Minimum", + StatType.P10_MINUS_P90: "P10-P90", + } + title += f" - {stattype_text[stattype]} of all realizations" figure.update( layout_title_text=title, diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py index bf3488c14..adf54fcaf 100644 --- a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union import plotly.graph_objects as go from dash import ALL, Input, Output, State, callback, callback_context @@ -8,13 +8,14 @@ from webviz_config.utils import StrEnum, callback_typecheck from webviz_config.webviz_plugin_subclasses import ViewABC -from ..._types import ChartType +from ..._types import ChartType, StatType from ..._utils import EnsembleWellAnalysisData from ._settings import ( WellOverviewChartType, WellOverviewFilters, WellOverviewLayoutOptions, WellOverviewSelections, + WellOverviewStatisticalOptions, ) from ._utils import WellOverviewFigure, format_well_overview_figure from ._view_element import WellOverviewViewElement @@ -24,6 +25,7 @@ class WellOverviewView(ViewABC): class Ids(StrEnum): CHART_TYPE = "chart-type" SELECTIONS = "selections" + STATISTICS = "statistics" LAYOUT_OPTIONS = "layout-options" FILTERS = "filters" CURRENT_FIGURE = "current-figure" @@ -56,6 +58,7 @@ def __init__( { self.Ids.CHART_TYPE: WellOverviewChartType(), self.Ids.SELECTIONS: WellOverviewSelections(ensembles, sorted_dates), + self.Ids.STATISTICS: WellOverviewStatisticalOptions(), self.Ids.LAYOUT_OPTIONS: WellOverviewLayoutOptions(), self.Ids.FILTERS: WellOverviewFilters(wells), } @@ -112,6 +115,16 @@ def _display_charttype_settings( .to_string(), "figure", ), + Output( + { + "id": self.settings_group_unique_id( + self.Ids.LAYOUT_OPTIONS, + WellOverviewLayoutOptions.Ids.CHARTTYPE_CHECKLIST, + ), + "charttype": ChartType.BAR, + }, + "options", + ), Input( self.settings_group_unique_id( self.Ids.SELECTIONS, WellOverviewSelections.Ids.ENSEMBLES @@ -160,6 +173,12 @@ def _display_charttype_settings( ), "value", ), + Input( + self.settings_group_unique_id( + self.Ids.STATISTICS, WellOverviewStatisticalOptions.Ids.STATISTICS + ), + "value", + ), State( { "id": self.settings_group_unique_id( @@ -176,6 +195,16 @@ def _display_charttype_settings( .to_string(), "figure", ), + State( + { + "id": self.settings_group_unique_id( + self.Ids.LAYOUT_OPTIONS, + WellOverviewLayoutOptions.Ids.CHARTTYPE_CHECKLIST, + ), + "charttype": ChartType.BAR, + }, + "options", + ), ) @callback_typecheck def _update_graph( @@ -186,10 +215,12 @@ def _update_graph( prod_until_date: Union[str, None], charttype_selected: ChartType, wells_selected: List[str], + stattype_selected: StatType, checklist_ids: List[Dict[str, str]], current_fig_dict: Optional[Dict[str, Any]], - ) -> Component: - # pylint: disable=too-many-arguments + barchart_layout_options: List[Dict[str, Any]], + ) -> Tuple[Component, List[Dict[str, Any]]]: + # pylint: disable=too-many-arguments, too-many-locals """Updates the well overview graph with selected input (f.ex chart type)""" ctx = callback_context.triggered[0]["prop_id"].split(".")[0] @@ -210,6 +241,7 @@ def _update_graph( fig_dict = format_well_overview_figure( figure=go.Figure(current_fig_dict), charttype=charttype_selected, + stattype=stattype_selected, settings=settings[charttype_selected], sumvec=sumvec, prod_from_date=prod_from_date, @@ -231,6 +263,7 @@ def _update_graph( if prod_until_date is not None else None, charttype=charttype_selected, + stattype=stattype_selected, wells=wells_selected, theme=self._theme, ) @@ -238,10 +271,16 @@ def _update_graph( fig_dict = format_well_overview_figure( figure=figure.figure, charttype=charttype_selected, + stattype=stattype_selected, settings=settings[charttype_selected], sumvec=sumvec, prod_from_date=prod_from_date, prod_until_date=prod_until_date, ) - return fig_dict + # Disable error bars option if stat type is P10 minus P90. + for elem in barchart_layout_options: + if elem["value"] == "errorbars": + elem["disabled"] = stattype_selected == StatType.P10_MINUS_P90 + + return fig_dict, barchart_layout_options