From ea2f9550c2ff42685195213646146374b368fd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Lind-Johansen?= <47847084+lindjoha@users.noreply.github.com> Date: Thu, 29 Sep 2022 16:15:44 +0200 Subject: [PATCH] `RftPlotter` to WLF (#1113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Øyvind Lind-Johansen --- CHANGELOG.md | 1 + .../plugin_tests/test_rft_plotter.py | 25 +- .../_rft_plotter/_callbacks/__init__.py | 2 - .../_rft_plotter/_callbacks/callbacks.py | 222 ------- .../parameter_response_callbacks.py | 254 -------- .../plugins/_rft_plotter/_layout.py | 580 ------------------ .../plugins/_rft_plotter/_plugin.py | 38 +- .../_rft_plotter/_reusable_settings.py | 47 ++ .../_rft_plotter/_reusable_view_element.py | 14 + .../plugins/_rft_plotter/_types.py | 22 + .../plugins/_rft_plotter/_utils/__init__.py | 7 + .../{_figures => _utils}/_formation_figure.py | 114 ++-- .../_rft_plotter_data_model.py} | 0 .../{_figures => _views}/__init__.py | 0 .../_rft_plotter/_views/_map_view/__init__.py | 1 + .../_views/_map_view/_settings/__init__.py | 2 + .../_settings/_formation_plot_settings.py | 160 +++++ .../_map_view/_settings/_map_settings.py | 100 +++ .../_views/_map_view/_utils/__init__.py | 1 + .../_map_view/_utils}/_map_figure.py | 41 +- .../_rft_plotter/_views/_map_view/_view.py | 221 +++++++ .../_views/_misfit_per_real_view/__init__.py | 1 + .../_views/_misfit_per_real_view/_settings.py | 27 + .../_misfit_per_real_view/_utils/__init__.py | 1 + .../_utils/_misfit_per_real_figure.py} | 4 +- .../_views/_misfit_per_real_view/_view.py | 81 +++ .../_parameter_response_view/__init__.py | 1 + .../_settings/__init__.py | 3 + .../_settings/_options.py | 51 ++ .../_settings/_parameter_filter.py | 27 + .../_settings/_selections.py | 100 +++ .../_views/_parameter_response_view/_view.py | 323 ++++++++++ .../_views/_sim_vs_obs_view/__init__.py | 1 + .../_sim_vs_obs_view/_settings/__init__.py | 2 + .../_sim_vs_obs_view/_settings/_selections.py | 48 ++ .../_settings/_size_color_settings.py | 53 ++ .../_sim_vs_obs_view/_utils/__init__.py | 2 + .../_utils}/_crossplot_figure.py | 18 +- .../_utils}/_errorplot_figure.py | 0 .../_views/_sim_vs_obs_view/_view.py | 113 ++++ 40 files changed, 1548 insertions(+), 1160 deletions(-) delete mode 100644 webviz_subsurface/plugins/_rft_plotter/_callbacks/__init__.py delete mode 100644 webviz_subsurface/plugins/_rft_plotter/_callbacks/callbacks.py delete mode 100644 webviz_subsurface/plugins/_rft_plotter/_callbacks/parameter_response_callbacks.py delete mode 100644 webviz_subsurface/plugins/_rft_plotter/_layout.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_reusable_settings.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_reusable_view_element.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_types.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_utils/__init__.py rename webviz_subsurface/plugins/_rft_plotter/{_figures => _utils}/_formation_figure.py (77%) rename webviz_subsurface/plugins/_rft_plotter/{_business_logic.py => _utils/_rft_plotter_data_model.py} (100%) rename webviz_subsurface/plugins/_rft_plotter/{_figures => _views}/__init__.py (100%) create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_formation_plot_settings.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_map_settings.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/__init__.py rename webviz_subsurface/plugins/_rft_plotter/{_figures => _views/_map_view/_utils}/_map_figure.py (70%) create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_view.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_settings.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/__init__.py rename webviz_subsurface/plugins/_rft_plotter/{_figures/_misfit_figure.py => _views/_misfit_per_real_view/_utils/_misfit_per_real_figure.py} (95%) create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_view.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_options.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_parameter_filter.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_selections.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_view.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/__init__.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_selections.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_size_color_settings.py create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/__init__.py rename webviz_subsurface/plugins/_rft_plotter/{_figures => _views/_sim_vs_obs_view/_utils}/_crossplot_figure.py (88%) rename webviz_subsurface/plugins/_rft_plotter/{_figures => _views/_sim_vs_obs_view/_utils}/_errorplot_figure.py (100%) create mode 100644 webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_view.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 179c66950..685d8b1ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1078](https://github.com/equinor/webviz-subsurface/pull/1078) - Converted the `PvtPlot` plugin to WLF (Webviz Layout Framework). - [#1092](https://github.com/equinor/webviz-subsurface/pull/1092) - Converted the `TornadoPlotterFMU` plugin to WLF (Webviz Layout Framework). - [#1085](https://github.com/equinor/webviz-subsurface/pull/1085) - Converted the `ProdMisfit` plugin to WLF (Webviz Layout Framework). +- [#1113](https://github.com/equinor/webviz-subsurface/pull/1113) - Converted the `RftPlotter` plugin to WLF (Webviz Layout Framework). Combined the crossplot and misfit per observation to one new view: `Sim vs Obs`. ## [0.2.14] - 2022-06-28 diff --git a/tests/integration_tests/plugin_tests/test_rft_plotter.py b/tests/integration_tests/plugin_tests/test_rft_plotter.py index 67aa4f275..94848b283 100644 --- a/tests/integration_tests/plugin_tests/test_rft_plotter.py +++ b/tests/integration_tests/plugin_tests/test_rft_plotter.py @@ -1,10 +1,12 @@ # pylint: disable=no-name-in-module from webviz_config.plugins import RftPlotter +from webviz_config.testing import WebvizComposite -def test_rft_plotter(dash_duo, app, shared_settings, testdata_folder) -> None: +def test_rft_plotter( + _webviz_duo: WebvizComposite, shared_settings, testdata_folder +) -> None: plugin = RftPlotter( - app, shared_settings["HM_SETTINGS"], ensembles=shared_settings["HM_ENSEMBLES"], formations=testdata_folder @@ -24,9 +26,18 @@ def test_rft_plotter(dash_duo, app, shared_settings, testdata_folder) -> None: / "polygons" / "toptherys--gl_faultlines_extract_postprocess.csv", ) - app.layout = plugin.layout - dash_duo.start_server(app) - # This assert is commented out because it causes problem that are - # seemingly random. - # assert dash_duo.get_logs() == [] + _webviz_duo.start_server(plugin) + + _webviz_duo.toggle_webviz_settings_drawer() + _webviz_duo.toggle_webviz_settings_group( + plugin.view("map-view").settings_group_unique_id("map-settings") + ) + # Using str literals directly, not IDs from the plugin as intended because + # the run test did not accept the imports + + my_component_id = _webviz_duo.view_settings_group_unique_component_id( + "map-view", "map-settings", "map-ensemble" + ) + _webviz_duo.wait_for_contains_text(my_component_id, "iter-0") + assert _webviz_duo.get_logs() == [] diff --git a/webviz_subsurface/plugins/_rft_plotter/_callbacks/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_callbacks/__init__.py deleted file mode 100644 index 44cc4d766..000000000 --- a/webviz_subsurface/plugins/_rft_plotter/_callbacks/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .callbacks import plugin_callbacks -from .parameter_response_callbacks import paramresp_callbacks diff --git a/webviz_subsurface/plugins/_rft_plotter/_callbacks/callbacks.py b/webviz_subsurface/plugins/_rft_plotter/_callbacks/callbacks.py deleted file mode 100644 index 11ac74469..000000000 --- a/webviz_subsurface/plugins/_rft_plotter/_callbacks/callbacks.py +++ /dev/null @@ -1,222 +0,0 @@ -from typing import Any, Callable, Dict, List, Tuple, Union - -import webviz_core_components as wcc -from dash import Dash, Input, Output, State -from dash.exceptions import PreventUpdate - -from .._business_logic import RftPlotterDataModel, filter_frame -from .._figures._crossplot_figure import update_crossplot -from .._figures._errorplot_figure import update_errorplot -from .._figures._formation_figure import FormationFigure -from .._figures._map_figure import MapFigure -from .._figures._misfit_figure import update_misfit_plot -from .._layout import LayoutElements - - -def plugin_callbacks( - app: Dash, get_uuid: Callable, datamodel: RftPlotterDataModel -) -> None: - @app.callback( - Output(get_uuid(LayoutElements.FORMATIONS_WELL), "value"), - Input(get_uuid(LayoutElements.MAP_GRAPH), "clickData"), - State(get_uuid(LayoutElements.FORMATIONS_WELL), "value"), - ) - def _get_clicked_well( - click_data: Dict[str, List[Dict[str, Any]]], well: str - ) -> str: - if not click_data: - return well - for layer in click_data["points"]: - try: - return layer["customdata"] - except KeyError: - pass - raise PreventUpdate - - @app.callback( - Output(get_uuid(LayoutElements.MAP), "children"), - Input(get_uuid(LayoutElements.MAP_ENSEMBLE), "value"), - Input(get_uuid(LayoutElements.MAP_SIZE_BY), "value"), - Input(get_uuid(LayoutElements.MAP_COLOR_BY), "value"), - Input(get_uuid(LayoutElements.MAP_DATE_RANGE), "value"), - Input(get_uuid(LayoutElements.MAP_ZONES), "value"), - ) - def _update_map( - ensemble: str, sizeby: str, colorby: str, dates: List[float], zones: List[str] - ) -> Union[str, List[wcc.Graph]]: - figure = MapFigure(datamodel.ertdatadf, ensemble, zones) - if datamodel.faultlinesdf is not None: - figure.add_fault_lines(datamodel.faultlinesdf) - figure.add_misfit_plot(sizeby, colorby, dates) - - return [ - wcc.Graph( - style={"height": "84vh"}, - figure={"data": figure.traces, "layout": figure.layout}, - id=get_uuid(LayoutElements.MAP_GRAPH), - ) - ] - - @app.callback( - Output(get_uuid(LayoutElements.FORMATIONS_GRAPH), "children"), - Input(get_uuid(LayoutElements.FORMATIONS_WELL), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_DATE), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_ENSEMBLE), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_LINETYPE), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_DEPTHOPTION), "value"), - ) - def _update_formation_plot( - well: str, date: str, ensembles: List[str], linetype: str, depth_option: str - ) -> Union[str, List[wcc.Graph]]: - if not ensembles: - return "No ensembles selected" - - if date not in datamodel.date_in_well(well): - raise PreventUpdate - - figure = FormationFigure( - well=well, - ertdf=datamodel.ertdatadf, - enscolors=datamodel.enscolors, - depth_option=depth_option, - date=date, - ensembles=ensembles, - simdf=datamodel.simdf, - obsdf=datamodel.obsdatadf, - ) - if figure.ertdf.empty: - return ["No data matching the given filter criterias."] - - if datamodel.formations is not None: - figure.add_formation(datamodel.formationdf) - - figure.add_simulated_lines(linetype) - figure.add_additional_observations() - figure.add_ert_observed() - - return [ - wcc.Graph( - style={"height": "84vh"}, - figure=figure.figure, - ) - ] - - @app.callback( - Output(get_uuid(LayoutElements.FORMATIONS_LINETYPE), "options"), - Output(get_uuid(LayoutElements.FORMATIONS_LINETYPE), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_DEPTHOPTION), "value"), - State(get_uuid(LayoutElements.FORMATIONS_LINETYPE), "value"), - State(get_uuid(LayoutElements.FORMATIONS_WELL), "value"), - State(get_uuid(LayoutElements.FORMATIONS_DATE), "value"), - ) - def _update_linetype( - depth_option: str, - current_linetype: str, - current_well: str, - current_date: str, - ) -> Tuple[List[Dict[str, str]], str]: - if datamodel.simdf is not None: - df = filter_frame( - datamodel.simdf, - {"WELL": current_well, "DATE": current_date}, - ) - if depth_option == "TVD" or ( - depth_option == "MD" - and "CONMD" in datamodel.simdf - and len(df["CONMD"].unique()) == len(df["DEPTH"].unique()) - ): - - return ( - [ - { - "label": "Realization lines", - "value": "realization", - }, - { - "label": "Statistical fanchart", - "value": "fanchart", - }, - ], - current_linetype, - ) - - return [ - { - "label": "Realization lines", - "value": "realization", - }, - ], "realization" - - @app.callback( - Output(get_uuid(LayoutElements.FORMATIONS_DATE), "options"), - Output(get_uuid(LayoutElements.FORMATIONS_DATE), "value"), - Input(get_uuid(LayoutElements.FORMATIONS_WELL), "value"), - State(get_uuid(LayoutElements.FORMATIONS_DATE), "value"), - ) - def _update_date(well: str, current_date: str) -> Tuple[List[Dict[str, str]], str]: - dates = datamodel.date_in_well(well) - available_dates = [{"label": date, "value": date} for date in dates] - date = current_date if current_date in dates else dates[0] - return available_dates, date - - @app.callback( - Output(get_uuid(LayoutElements.MISFITPLOT_GRAPH), "children"), - Input(get_uuid(LayoutElements.FILTER_WELLS["misfitplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ZONES["misfitplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_DATES["misfitplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ENSEMBLES["misfitplot"]), "value"), - ) - def _misfit_plot( - wells: List[str], zones: List[str], dates: List[str], ensembles: List[str] - ) -> Union[str, List[wcc.Graph]]: - df = filter_frame( - datamodel.ertdatadf, - {"WELL": wells, "ZONE": zones, "DATE": dates, "ENSEMBLE": ensembles}, - ) - if df.empty: - return "No data matching the given filter criterias" - - return update_misfit_plot(df, datamodel.enscolors) - - @app.callback( - Output(get_uuid(LayoutElements.CROSSPLOT_GRAPH), "children"), - Input(get_uuid(LayoutElements.FILTER_WELLS["crossplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ZONES["crossplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_DATES["crossplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ENSEMBLES["crossplot"]), "value"), - Input(get_uuid(LayoutElements.CROSSPLOT_SIZE_BY), "value"), - Input(get_uuid(LayoutElements.CROSSPLOT_COLOR_BY), "value"), - ) - def _crossplot( - wells: List[str], - zones: List[str], - dates: List[str], - ensembles: List[str], - sizeby: str, - colorby: str, - ) -> Union[str, List[wcc.Graph]]: - df = filter_frame( - datamodel.ertdatadf, - {"WELL": wells, "ZONE": zones, "DATE": dates, "ENSEMBLE": ensembles}, - ) - if df.empty: - return "No data matching the given filter criterias" - return update_crossplot(df, sizeby, colorby) - - @app.callback( - Output(get_uuid(LayoutElements.ERRORPLOT_GRAPH), "children"), - Input(get_uuid(LayoutElements.FILTER_WELLS["errorplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ZONES["errorplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_DATES["errorplot"]), "value"), - Input(get_uuid(LayoutElements.FILTER_ENSEMBLES["errorplot"]), "value"), - ) - def _errorplot( - wells: List[str], zones: List[str], dates: List[str], ensembles: List[str] - ) -> Union[str, List[wcc.Graph]]: - df = filter_frame( - datamodel.ertdatadf, - {"WELL": wells, "ZONE": zones, "DATE": dates, "ENSEMBLE": ensembles}, - ) - if df.empty: - return "No data matching the given filter criterias" - return [update_errorplot(df, datamodel.enscolors)] diff --git a/webviz_subsurface/plugins/_rft_plotter/_callbacks/parameter_response_callbacks.py b/webviz_subsurface/plugins/_rft_plotter/_callbacks/parameter_response_callbacks.py deleted file mode 100644 index af7f97be8..000000000 --- a/webviz_subsurface/plugins/_rft_plotter/_callbacks/parameter_response_callbacks.py +++ /dev/null @@ -1,254 +0,0 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Union - -import webviz_core_components as wcc -from dash import Dash, Input, Output, State -from dash.exceptions import PreventUpdate - -from ...._figures import BarChart, ScatterPlot -from .._business_logic import RftPlotterDataModel, correlate -from .._figures._formation_figure import FormationFigure -from .._layout import LayoutElements - - -def paramresp_callbacks( - app: Dash, get_uuid: Callable, datamodel: RftPlotterDataModel -) -> None: - @app.callback( - Output(get_uuid(LayoutElements.PARAMRESP_PARAM), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_CORR_BARCHART_FIGURE), "clickData"), - State(get_uuid(LayoutElements.PARAMRESP_CORRTYPE), "value"), - prevent_initial_call=True, - ) - def _update_param_from_clickdata( - corr_vector_clickdata: Union[None, dict], - corrtype: str, - ) -> str: - """Update the selected parameter from clickdata""" - if corr_vector_clickdata is None or corrtype == "param_vs_sim": - raise PreventUpdate - return corr_vector_clickdata.get("points", [{}])[0].get("y") - - @app.callback( - Output(get_uuid(LayoutElements.PARAMRESP_WELL), "value"), - Output(get_uuid(LayoutElements.PARAMRESP_DATE_DROPDOWN), "children"), - Output(get_uuid(LayoutElements.PARAMRESP_ZONE_DROPDOWN), "children"), - Input(get_uuid(LayoutElements.PARAMRESP_CORR_BARCHART_FIGURE), "clickData"), - State(get_uuid(LayoutElements.PARAMRESP_CORRTYPE), "value"), - State(get_uuid(LayoutElements.PARAMRESP_WELL), "value"), - State(get_uuid(LayoutElements.PARAMRESP_DATE), "value"), - State(get_uuid(LayoutElements.PARAMRESP_ZONE), "value"), - prevent_initial_call=True, - ) - def _update_selections_from_clickdata( - corr_vector_clickdata: Union[None, dict], - corrtype: str, - well: str, - date: str, - zone: str, - ) -> Tuple[str, wcc.Dropdown, wcc.Dropdown]: - """Update well, date and zone from clickdata""" - if corr_vector_clickdata is None or corrtype == "sim_vs_param": - raise PreventUpdate - - clickdata = corr_vector_clickdata.get("points", [{}])[0].get("y") - ls_clickdata = clickdata.split() - - dates_in_well, zones_in_well = datamodel.well_dates_and_zones(well) - dates_dropdown = wcc.Dropdown( - label="Date", - id=get_uuid(LayoutElements.PARAMRESP_DATE), - options=[{"label": date, "value": date} for date in dates_in_well], - value=ls_clickdata[1], - clearable=False, - ) - zones_dropdown = wcc.Dropdown( - label="Zone", - id=get_uuid(LayoutElements.PARAMRESP_ZONE), - options=[{"label": zone, "value": zone} for zone in zones_in_well], - value=ls_clickdata[2], - clearable=False, - ) - - return ls_clickdata[0], dates_dropdown, zones_dropdown - - @app.callback( - Output(get_uuid(LayoutElements.PARAMRESP_DATE), "options"), - Output(get_uuid(LayoutElements.PARAMRESP_DATE), "value"), - Output(get_uuid(LayoutElements.PARAMRESP_ZONE), "options"), - Output(get_uuid(LayoutElements.PARAMRESP_ZONE), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_WELL), "value"), - State(get_uuid(LayoutElements.PARAMRESP_ZONE), "value"), - ) - def _update_date_and_zone( - well: str, zone_state: str - ) -> Tuple[List[Dict[str, str]], str, List[Dict[str, str]], str]: - """Update dates and zones when selecting well. If the current - selected zone is also present in the new well it will be kept as value. - """ - dates_in_well, zones_in_well = datamodel.well_dates_and_zones(well) - return ( - [{"label": date, "value": date} for date in dates_in_well], - dates_in_well[0], - [{"label": zone, "value": zone} for zone in zones_in_well], - zone_state if zone_state in zones_in_well else zones_in_well[0], - ) - - @app.callback( - Output(get_uuid(LayoutElements.PARAMRESP_CORR_BARCHART), "children"), - Output(get_uuid(LayoutElements.PARAMRESP_SCATTERPLOT), "children"), - Output(get_uuid(LayoutElements.PARAMRESP_FORMATIONS), "children"), - Input(get_uuid(LayoutElements.PARAMRESP_ENSEMBLE), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_WELL), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_DATE), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_ZONE), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_PARAM), "value"), - Input(get_uuid(LayoutElements.PARAMRESP_CORRTYPE), "value"), - Input( - {"id": get_uuid(LayoutElements.PARAM_FILTER), "type": "data-store"}, "data" - ), - Input(get_uuid(LayoutElements.PARAMRESP_DEPTHOPTION), "value"), - ) - # pylint: disable=too-many-locals - def _update_paramresp_graphs( - ensemble: str, - well: str, - date: str, - zone: str, - param: Optional[str], - corrtype: str, - real_filter: Dict[str, List[int]], - depth_option: str, - ) -> List[Optional[Any]]: - """Main callback to update the graphs: - - * ranked correlations bar chart - * response vs param scatter plot - * formations chart RFT pressure vs depth, colored by parameter value - """ - ( - df, - obs, - obs_err, - ens_params, - ens_rfts, - ) = datamodel.create_rft_and_param_pivot_table( - ensemble=ensemble, - well=well, - date=date, - zone=zone, - reals=real_filter[ensemble], - keep_all_rfts=(corrtype == "param_vs_sim"), - ) - current_key = f"{well} {date} {zone}" - - if df is None: - # This happens if the filtering criterias returns no data - # Could f.ex happen when there are ensembles with different well names - return ["No data matching the given filter criterias."] * 3 - if param is not None and param not in ens_params: - # This happens if the selected parameter does not exist in the - # selected ensemble - return ["The selected parameter not valid for selected ensemble."] * 3 - if not ens_params: - # This happens if there are multiple ensembles and one of the ensembles - # doesn't have non-constant parameters. - return ["The selected ensemble has no non-constant parameters."] * 3 - - if corrtype == "sim_vs_param" or param is None: - corrseries = correlate(df[ens_params + [current_key]], current_key) - param = param if param is not None else corrseries.abs().idxmax() - corr_title = f"{current_key} vs parameters" - scatter_x, scatter_y, highlight_bar = param, current_key, param - - if corrtype == "param_vs_sim": - corrseries = correlate(df[ens_rfts + [param]], param) - corr_title = f"{param} vs simulated RFTs" - scatter_x, scatter_y, highlight_bar = param, current_key, current_key - - # Correlation bar chart - corrfig = BarChart(corrseries, n_rows=15, title=corr_title, orientation="h") - corrfig.color_bars(highlight_bar, "#007079", 0.5) - corr_graph = wcc.Graph( - style={"height": "40vh"}, - config={"displayModeBar": False}, - figure=corrfig.figure, - id=get_uuid(LayoutElements.PARAMRESP_CORR_BARCHART_FIGURE), - ) - - # Scatter plot - scatterplot = ScatterPlot( - df, scatter_y, scatter_x, "#007079", f"{current_key} vs {param}" - ) - scatterplot.add_vertical_line_with_error( - obs, - obs_err, - df[param].min(), - df[param].max(), - ) - scatter_graph = ( - wcc.Graph( - style={"height": "40vh"}, - config={"displayModeBar": False}, - figure=scatterplot.figure, - ), - ) - - # Formations plot - formations_figure = FormationFigure( - well=well, - ertdf=datamodel.ertdatadf, - enscolors=datamodel.enscolors, - depth_option=depth_option, - date=date, - ensembles=[ensemble], - reals=real_filter[ensemble], - simdf=datamodel.simdf, - obsdf=datamodel.obsdatadf, - ) - - if formations_figure.use_ertdf: - return [ - corr_graph, - scatter_graph, - f"Realization lines not available for depth option {depth_option}", - ] - - if datamodel.formations is not None: - formations_figure.add_formation(datamodel.formationdf, fill_color=False) - - formations_figure.add_simulated_lines("realization") - formations_figure.add_additional_observations() - formations_figure.add_ert_observed() - - df_value_norm = datamodel.get_param_real_and_value_df( - ensemble, parameter=param, normalize=True - ) - formations_figure.color_by_param_value(df_value_norm, param) - - return [ - corr_graph, - scatter_graph, - wcc.Graph( - style={"height": "87vh"}, - figure=formations_figure.figure, - ), - ] - - @app.callback( - Output(get_uuid(LayoutElements.PARAM_FILTER_WRAPPER), "style"), - Input(get_uuid(LayoutElements.DISPLAY_PARAM_FILTER), "value"), - ) - def _show_hide_parameter_filter(display_param_filter: list) -> Dict[str, Any]: - """Display/hide parameter filter""" - return {"display": "block" if display_param_filter else "none", "flex": 1} - - @app.callback( - Output( - {"id": get_uuid(LayoutElements.PARAM_FILTER), "type": "ensemble-update"}, - "data", - ), - Input(get_uuid(LayoutElements.PARAMRESP_ENSEMBLE), "value"), - ) - def _update_parameter_filter_selection(ensemble: str) -> List[str]: - """Update ensemble in parameter filter""" - return [ensemble] diff --git a/webviz_subsurface/plugins/_rft_plotter/_layout.py b/webviz_subsurface/plugins/_rft_plotter/_layout.py deleted file mode 100644 index ce1733573..000000000 --- a/webviz_subsurface/plugins/_rft_plotter/_layout.py +++ /dev/null @@ -1,580 +0,0 @@ -from typing import Callable, List - -import webviz_core_components as wcc -from dash import html - -from ..._components.parameter_filter import ParameterFilter -from ._business_logic import RftPlotterDataModel - - -# pylint: disable = too-few-public-methods -class LayoutElements: - MAP = "map-wrapper" - MAP_GRAPH = "map-graph" - FORMATIONS_GRAPH = "formations-graph-wrapper" - MISFITPLOT_GRAPH = "misfit-graph-wrapper" - CROSSPLOT_GRAPH = "crossplot-graph-wrapper" - ERRORPLOT_GRAPH = "errorplot-graph-wrapper" - FORMATIONS_ENSEMBLE = "formations-ensemble" - FORMATIONS_WELL = "formations-well" - FORMATIONS_DATE = "formations-date" - FORMATIONS_LINETYPE = "formations-linetype" - FORMATIONS_DEPTHOPTION = "formations-depthoption" - MAP_ENSEMBLE = "map-ensemble" - MAP_SIZE_BY = "map-size-by" - MAP_COLOR_BY = "map-color-by" - MAP_DATE_RANGE = "map-date-range" - MAP_ZONES = "map-zones" - FILTER_ENSEMBLES = { - "misfitplot": "ensembles-misfit", - "crossplot": "ensembles-crossplot", - "errorplot": "ensembles-errorplot", - } - FILTER_WELLS = { - "misfitplot": "well-misfit", - "crossplot": "well-crossplot", - "errorplot": "well-errorplot", - } - FILTER_ZONES = { - "misfitplot": "zones-misfit", - "crossplot": "zones-crossplot", - "errorplot": "zones-errorplot", - } - FILTER_DATES = { - "misfitplot": "dates-misfit", - "crossplot": "dates-crossplot", - "errorplot": "dates-errorplot", - } - CROSSPLOT_COLOR_BY = "crossplot-color-by" - CROSSPLOT_SIZE_BY = "crossplot-size-by" - PARAMRESP_ENSEMBLE = "param-response-ensemble" - PARAMRESP_WELL = "param-response-well" - PARAMRESP_DATE = "param-response-date" - PARAMRESP_ZONE = "param-response-zone" - PARAMRESP_PARAM = "param-response-param" - PARAMRESP_CORRTYPE = "param-response-corrtype" - PARAMRESP_CORR_BARCHART = "paramresp-corr-barchart" - PARAMRESP_CORR_BARCHART_FIGURE = "paramresp-corr-barchart-figure" - PARAMRESP_SCATTERPLOT = "paramresp-scatterplot" - PARAMRESP_FORMATIONS = "paramresp-formations" - PARAMRESP_DATE_DROPDOWN = "paramresp-well-dropdown" - PARAMRESP_ZONE_DROPDOWN = "paramresp-zone-dropdown" - PARAMRESP_DEPTHOPTION = "paramresp-depthoption" - PARAM_FILTER = "param-filter" - PARAM_FILTER_WRAPPER = "param-filter-wrapper" - DISPLAY_PARAM_FILTER = "display-param-filter" - - -def main_layout(get_uuid: Callable, datamodel: RftPlotterDataModel) -> wcc.Tabs: - - tabs = [ - wcc.Tab( - label="RFT Map", - children=[ - wcc.FlexBox( - children=[ - wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=[ - map_plot_selectors(get_uuid, datamodel), - formation_plot_selectors(get_uuid, datamodel), - ], - ), - wcc.Frame( - style={"flex": 3, "height": "87vh"}, - color="white", - highlight=False, - id=get_uuid(LayoutElements.MAP), - children=[], - ), - wcc.Frame( - style={"flex": 3, "height": "87vh"}, - color="white", - highlight=False, - id=get_uuid(LayoutElements.FORMATIONS_GRAPH), - children=[], - ), - ] - ) - ], - ), - wcc.Tab( - label="RFT misfit per real", - children=[ - wcc.FlexBox( - children=[ - wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=filter_layout(get_uuid, datamodel, "misfitplot"), - ), - wcc.Frame( - style={"flex": 6, "height": "87vh"}, - color="white", - highlight=False, - id=get_uuid(LayoutElements.MISFITPLOT_GRAPH), - children=[], - ), - ] - ) - ], - ), - wcc.Tab( - label="RFT crossplot - sim vs obs", - children=[ - wcc.FlexBox( - children=[ - wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=[ - filter_layout(get_uuid, datamodel, "crossplot"), - size_color_layout(get_uuid), - ], - ), - wcc.Frame( - style={"flex": 6, "height": "87vh"}, - color="white", - highlight=False, - children=[ - html.Div(id=get_uuid(LayoutElements.CROSSPLOT_GRAPH)), - ], - ), - ], - ), - ], - ), - wcc.Tab( - label="RFT misfit per observation", - children=[ - wcc.FlexBox( - children=[ - wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=filter_layout(get_uuid, datamodel, "errorplot"), - ), - wcc.Frame( - color="white", - highlight=False, - style={"flex": 6, "height": "87vh"}, - id=get_uuid(LayoutElements.ERRORPLOT_GRAPH), - children=[], - ), - ], - ), - ], - ), - ] - - # It this is not a sensitivity run, add the parameter response tab - if not datamodel.param_model.sensrun: - tabs.append( - wcc.Tab( - label="RFT parameter response", - children=parameter_response_layout( - get_uuid=get_uuid, datamodel=datamodel - ), - ) - ) - - return wcc.Tabs(children=tabs) - - -def parameter_response_selector_layout( - get_uuid: Callable, datamodel: RftPlotterDataModel -) -> wcc.Frame: - ensembles = datamodel.ensembles - well_names = datamodel.well_names - params = datamodel.parameters if not datamodel.parameters is None else [] - return wcc.Frame( - style={ - "height": "87vh", - "overflowY": "auto", - "font-size": "15px", - }, - children=[ - wcc.Selectors( - label="Selections", - children=[ - wcc.Dropdown( - label="Ensemble", - id=get_uuid(LayoutElements.PARAMRESP_ENSEMBLE), - options=[{"label": ens, "value": ens} for ens in ensembles], - value=ensembles[0], - clearable=False, - ), - wcc.Dropdown( - label="Well", - id=get_uuid(LayoutElements.PARAMRESP_WELL), - options=[{"label": well, "value": well} for well in well_names], - value=well_names[0] if well_names else "", - clearable=False, - ), - html.Div( - id=get_uuid(LayoutElements.PARAMRESP_DATE_DROPDOWN), - children=wcc.Dropdown( - label="Date", - id=get_uuid(LayoutElements.PARAMRESP_DATE), - options=None, - value=None, - clearable=False, - ), - ), - html.Div( - id=get_uuid(LayoutElements.PARAMRESP_ZONE_DROPDOWN), - children=wcc.Dropdown( - label="Zone", - id=get_uuid(LayoutElements.PARAMRESP_ZONE), - options=None, - clearable=False, - value=None, - ), - ), - wcc.Dropdown( - label="Parameter", - id=get_uuid(LayoutElements.PARAMRESP_PARAM), - options=[{"label": param, "value": param} for param in params], - clearable=False, - value=None, - ), - ], - ), - wcc.Selectors( - label="Options", - children=[ - wcc.Checklist( - id=get_uuid(LayoutElements.DISPLAY_PARAM_FILTER), - options=[{"label": "Show parameter filter", "value": "Show"}], - value=[], - ), - wcc.RadioItems( - label="Correlation options", - id=get_uuid(LayoutElements.PARAMRESP_CORRTYPE), - options=[ - { - "label": "Simulated vs parameters", - "value": "sim_vs_param", - }, - { - "label": "Parameter vs simulated", - "value": "param_vs_sim", - }, - ], - value="sim_vs_param", - ), - wcc.RadioItems( - label="Depth option", - id=get_uuid(LayoutElements.PARAMRESP_DEPTHOPTION), - options=[ - { - "label": "TVD", - "value": "TVD", - }, - { - "label": "MD", - "value": "MD", - }, - ], - value="TVD", - ), - ], - ), - ], - ) - - -def parameter_response_layout( - get_uuid: Callable, datamodel: RftPlotterDataModel -) -> wcc.FlexBox: - df = datamodel.param_model.dataframe - parameter_filter = ParameterFilter( - uuid=get_uuid(LayoutElements.PARAM_FILTER), - dframe=df[df["ENSEMBLE"].isin(datamodel.param_model.mc_ensembles)].copy(), - reset_on_ensemble_update=True, - ) - return wcc.FlexBox( - children=[ - wcc.FlexColumn( - flex=1, children=parameter_response_selector_layout(get_uuid, datamodel) - ), - wcc.FlexColumn( - flex=4, - children=wcc.FlexBox( - children=[ - wcc.FlexColumn( - flex=2, - children=[ - wcc.Frame( - style={"height": "41.5vh"}, - id=get_uuid(LayoutElements.PARAMRESP_CORR_BARCHART), - color="white", - highlight=False, - children=[], - ), - wcc.Frame( - style={"height": "41.5vh"}, - id=get_uuid(LayoutElements.PARAMRESP_SCATTERPLOT), - color="white", - highlight=False, - children=[], - ), - ], - ), - wcc.FlexColumn( - flex=2, - children=[ - wcc.Frame( - id=get_uuid(LayoutElements.PARAMRESP_FORMATIONS), - color="white", - highlight=False, - style={"height": "87vh"}, - children=[], - ) - ], - ), - ], - ), - ), - wcc.FlexColumn( - id=get_uuid(LayoutElements.PARAM_FILTER_WRAPPER), - style={"display": "none"}, - flex=1, - children=wcc.Frame( - style={"height": "87vh"}, - children=parameter_filter.layout, - ), - ), - ] - ) - - -def formation_plot_selectors( - get_uuid: Callable, datamodel: RftPlotterDataModel -) -> List[html.Div]: - ensembles = datamodel.ensembles - well_names = datamodel.well_names - date_in_well = datamodel.date_in_well - return wcc.Selectors( - label="Formation plot settings", - children=[ - wcc.Dropdown( - label="Ensemble", - id=get_uuid(LayoutElements.FORMATIONS_ENSEMBLE), - options=[{"label": ens, "value": ens} for ens in ensembles], - value=ensembles[0], - multi=True, - clearable=False, - ), - wcc.Dropdown( - label="Well", - id=get_uuid(LayoutElements.FORMATIONS_WELL), - options=[{"label": well, "value": well} for well in well_names], - value=well_names[0], - clearable=False, - ), - wcc.Dropdown( - label="Date", - id=get_uuid(LayoutElements.FORMATIONS_DATE), - options=[ - {"label": date, "value": date} - for date in date_in_well(well_names[0]) - ], - clearable=False, - value=date_in_well(well_names[0])[0], - ), - wcc.RadioItems( - label="Plot simulations as", - id=get_uuid(LayoutElements.FORMATIONS_LINETYPE), - options=[ - { - "label": "Realization lines", - "value": "realization", - }, - { - "label": "Statistical fanchart", - "value": "fanchart", - }, - ], - value="realization", - ), - wcc.RadioItems( - label="Depth option", - id=get_uuid(LayoutElements.FORMATIONS_DEPTHOPTION), - options=[ - { - "label": "TVD", - "value": "TVD", - }, - { - "label": "MD", - "value": "MD", - }, - ], - value="TVD", - ), - ], - ) - - -def map_plot_selectors( - get_uuid: Callable, datamodel: RftPlotterDataModel -) -> List[html.Div]: - ensembles = datamodel.ensembles - zone_names = datamodel.zone_names - return wcc.Selectors( - label="Map plot settings", - children=[ - wcc.Dropdown( - label="Ensemble", - id=get_uuid(LayoutElements.MAP_ENSEMBLE), - options=[{"label": ens, "value": ens} for ens in ensembles], - value=ensembles[0], - clearable=False, - ), - wcc.Dropdown( - label="Size points by", - id=get_uuid(LayoutElements.MAP_SIZE_BY), - options=[ - { - "label": "Standard Deviation", - "value": "STDDEV", - }, - { - "label": "Misfit", - "value": "ABSDIFF", - }, - ], - value="ABSDIFF", - clearable=False, - ), - wcc.Dropdown( - label="Color points by", - id=get_uuid(LayoutElements.MAP_COLOR_BY), - options=[ - { - "label": "Misfit", - "value": "ABSDIFF", - }, - { - "label": "Standard Deviation", - "value": "STDDEV", - }, - { - "label": "Year", - "value": "YEAR", - }, - ], - value="STDDEV", - clearable=False, - ), - wcc.RangeSlider( - label="Filter date range", - id=get_uuid(LayoutElements.MAP_DATE_RANGE), - min=datamodel.ertdatadf["DATE_IDX"].min(), - max=datamodel.ertdatadf["DATE_IDX"].max(), - value=[ - datamodel.ertdatadf["DATE_IDX"].min(), - datamodel.ertdatadf["DATE_IDX"].max(), - ], - marks=datamodel.date_marks, - ), - wcc.Selectors( - label="Zone filter", - open_details=False, - children=[ - wcc.SelectWithLabel( - size=min(10, len(zone_names)), - id=get_uuid(LayoutElements.MAP_ZONES), - options=[{"label": name, "value": name} for name in zone_names], - value=zone_names, - multi=True, - ), - ], - ), - ], - ) - - -def filter_layout( - get_uuid: Callable, datamodel: RftPlotterDataModel, tab: str -) -> List[wcc.Selectors]: - """Layout for shared filters""" - ensembles = datamodel.ensembles - well_names = datamodel.well_names - zone_names = datamodel.zone_names - dates = datamodel.dates - return wcc.Selectors( - label="Selectors", - children=[ - wcc.SelectWithLabel( - label="Ensembles", - size=min(4, len(ensembles)), - id=get_uuid(LayoutElements.FILTER_ENSEMBLES[tab]), - options=[{"label": name, "value": name} for name in ensembles], - value=ensembles, - multi=True, - ), - wcc.SelectWithLabel( - label="Wells", - size=min(20, len(well_names)), - id=get_uuid(LayoutElements.FILTER_WELLS[tab]), - options=[{"label": name, "value": name} for name in well_names], - value=well_names, - multi=True, - ), - wcc.SelectWithLabel( - label="Zones", - size=min(10, len(zone_names)), - id=get_uuid(LayoutElements.FILTER_ZONES[tab]), - options=[{"label": name, "value": name} for name in zone_names], - value=zone_names, - multi=True, - ), - wcc.SelectWithLabel( - label="Dates", - size=min(10, len(dates)), - id=get_uuid(LayoutElements.FILTER_DATES[tab]), - options=[{"label": name, "value": name} for name in dates], - value=dates, - multi=True, - ), - ], - ) - - -def size_color_layout(get_uuid: Callable) -> List[html.Div]: - return wcc.Selectors( - label="Plot settings", - children=[ - wcc.Dropdown( - label="Color by", - id=get_uuid(LayoutElements.CROSSPLOT_COLOR_BY), - options=[ - { - "label": "Misfit", - "value": "ABSDIFF", - }, - { - "label": "Standard Deviation", - "value": "STDDEV", - }, - ], - value="STDDEV", - clearable=False, - ), - wcc.Dropdown( - label="Size by", - id=get_uuid(LayoutElements.CROSSPLOT_SIZE_BY), - options=[ - { - "label": "Standard Deviation", - "value": "STDDEV", - }, - { - "label": "Misfit", - "value": "ABSDIFF", - }, - ], - value="ABSDIFF", - clearable=False, - ), - ], - ) diff --git a/webviz_subsurface/plugins/_rft_plotter/_plugin.py b/webviz_subsurface/plugins/_rft_plotter/_plugin.py index a7c3112b6..8ad350af4 100644 --- a/webviz_subsurface/plugins/_rft_plotter/_plugin.py +++ b/webviz_subsurface/plugins/_rft_plotter/_plugin.py @@ -1,13 +1,14 @@ from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple -import webviz_core_components as wcc -from dash import Dash from webviz_config import WebvizPluginABC, WebvizSettings +from webviz_config.utils import StrEnum -from ._business_logic import RftPlotterDataModel -from ._callbacks import paramresp_callbacks, plugin_callbacks -from ._layout import main_layout +from ._utils._rft_plotter_data_model import RftPlotterDataModel +from ._views._map_view import MapView +from ._views._misfit_per_real_view import MisfitPerRealView +from ._views._parameter_response_view import ParameterResponseView +from ._views._sim_vs_obs_view import SimVsObsView class RftPlotter(WebvizPluginABC): @@ -90,10 +91,14 @@ class RftPlotter(WebvizPluginABC): """ - # pylint: disable=too-many-arguments + class Ids(StrEnum): + MAP_VIEW = "map-view" + MISFIT_PER_REAL_VIEW = "misfit-per-real-view" + SIM_VS_OBS_VIEW = "sim-vs-obs-view" + PARAMETER_RESPONSE_VIEW = "parameter-response-view" + def __init__( self, - app: Dash, webviz_settings: WebvizSettings, csvfile_rft: Path = None, csvfile_rft_ert: Path = None, @@ -114,18 +119,13 @@ def __init__( csvfile_rft_ert, ) - self.set_callbacks(app) + self.add_view(MapView(self._datamodel), self.Ids.MAP_VIEW) + self.add_view(MisfitPerRealView(self._datamodel), self.Ids.MISFIT_PER_REAL_VIEW) + self.add_view(SimVsObsView(self._datamodel), self.Ids.SIM_VS_OBS_VIEW) + if not self._datamodel.param_model.sensrun: + self.add_view( + ParameterResponseView(self._datamodel), self.Ids.PARAMETER_RESPONSE_VIEW + ) def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]: return self._datamodel.webviz_store - - @property - def layout(self) -> wcc.Tabs: - return main_layout(self.uuid, self._datamodel) - - def set_callbacks(self, app: Dash) -> None: - plugin_callbacks(app, self.uuid, self._datamodel) - - # It this is not a sensitivity run, add the parameter response callbacks - if not self._datamodel.param_model.sensrun: - paramresp_callbacks(app, self.uuid, self._datamodel) diff --git a/webviz_subsurface/plugins/_rft_plotter/_reusable_settings.py b/webviz_subsurface/plugins/_rft_plotter/_reusable_settings.py new file mode 100644 index 000000000..bfaf974e0 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_reusable_settings.py @@ -0,0 +1,47 @@ +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 FilterLayout(SettingsGroupABC): + class Ids(StrEnum): + FILTER_WELLS = "filter-wells" + FILTER_ZONES = "filter-zones" + FILTER_DATES = "filter-dates" + + def __init__(self, wells: List[str], zones: List[str], dates: List[str]) -> None: + super().__init__("Filters") + self._wells = wells + self._zones = zones + self._dates = dates + + def layout(self) -> List[Component]: + return [ + wcc.SelectWithLabel( + label="Wells", + size=min(10, len(self._wells)), + id=self.register_component_unique_id(self.Ids.FILTER_WELLS), + options=[{"label": name, "value": name} for name in self._wells], + value=self._wells, + multi=True, + ), + wcc.SelectWithLabel( + label="Zones", + size=min(10, len(self._zones)), + id=self.register_component_unique_id(self.Ids.FILTER_ZONES), + options=[{"label": name, "value": name} for name in self._zones], + value=self._zones, + multi=True, + ), + wcc.SelectWithLabel( + label="Dates", + size=min(10, len(self._dates)), + id=self.register_component_unique_id(self.Ids.FILTER_DATES), + options=[{"label": name, "value": name} for name in self._dates], + value=self._dates, + multi=True, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_reusable_view_element.py b/webviz_subsurface/plugins/_rft_plotter/_reusable_view_element.py new file mode 100644 index 000000000..3f4e5d553 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_reusable_view_element.py @@ -0,0 +1,14 @@ +from dash import html +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class GeneralViewElement(ViewElementABC): + class Ids(StrEnum): + CHART = "chart" + + def __init__(self) -> None: + super().__init__() + + def inner_layout(self) -> html.Div: + return html.Div(id=self.register_component_unique_id(self.Ids.CHART)) diff --git a/webviz_subsurface/plugins/_rft_plotter/_types.py b/webviz_subsurface/plugins/_rft_plotter/_types.py new file mode 100644 index 000000000..1fe3fc6b2 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_types.py @@ -0,0 +1,22 @@ +from webviz_config.utils import StrEnum + + +class LineType(StrEnum): + REALIZATION = "realization" + FANCHART = "fanchart" + + +class DepthType(StrEnum): + TVD = "TVD" + MD = "MD" + + +class ColorAndSizeByType(StrEnum): + MISFIT = "ABSDIFF" + STDDEV = "STDDEV" + YEAR = "YEAR" + + +class CorrType(StrEnum): + SIM_VS_PARAM = "sim_vs_param" + PARAM_VS_SIM = "param_vs_sim" diff --git a/webviz_subsurface/plugins/_rft_plotter/_utils/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_utils/__init__.py new file mode 100644 index 000000000..ebe3cb652 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_utils/__init__.py @@ -0,0 +1,7 @@ +from ._formation_figure import FormationFigure +from ._rft_plotter_data_model import ( + RftPlotterDataModel, + correlate, + filter_frame, + interpolate_depth, +) diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/_formation_figure.py b/webviz_subsurface/plugins/_rft_plotter/_utils/_formation_figure.py similarity index 77% rename from webviz_subsurface/plugins/_rft_plotter/_figures/_formation_figure.py rename to webviz_subsurface/plugins/_rft_plotter/_utils/_formation_figure.py index 4d042ca6e..15d55138a 100644 --- a/webviz_subsurface/plugins/_rft_plotter/_figures/_formation_figure.py +++ b/webviz_subsurface/plugins/_rft_plotter/_utils/_formation_figure.py @@ -12,7 +12,8 @@ TraceDirection, get_fanchart_traces, ) -from .._business_logic import filter_frame, interpolate_depth +from .._types import DepthType, LineType +from ._rft_plotter_data_model import filter_frame, interpolate_depth class FormationFigure: @@ -22,30 +23,30 @@ def __init__( well: str, ertdf: pd.DataFrame, enscolors: dict, - depth_option: str, + depthtype: DepthType, date: str, ensembles: List[str], simdf: Optional[pd.DataFrame] = None, obsdf: Optional[pd.DataFrame] = None, reals: Optional[Sequence] = None, ) -> None: - self.well = well + self._well = well rft_filter = {"WELL": well, "DATE": date, "ENSEMBLE": ensembles} if reals is not None: rft_filter.update({"REAL": reals}) - self.simdf = filter_frame(simdf, rft_filter) if simdf is not None else None - self.obsdf = ( + self._simdf = filter_frame(simdf, rft_filter) if simdf is not None else None + self._obsdf = ( filter_frame(obsdf, {"WELL": well, "DATE": date}) if obsdf is not None else None ) - self.ertdf = filter_frame(ertdf, rft_filter) - self.depth_option = depth_option - self.enscolors = enscolors - self.set_depth_columns() + self._ertdf = filter_frame(ertdf, rft_filter) + self._depthtype = depthtype + self._enscolors = enscolors + self._set_depth_columns() - self.traces: List[Dict[str, Any]] = [] + self._traces: List[Dict[str, Any]] = [] self._layout = { "yaxis": {"autorange": "reversed", "title": "Depth", "showgrid": False}, "xaxis": { @@ -59,13 +60,13 @@ def __init__( "legend": {"orientation": "h"}, "margin": {"t": 50}, "hovermode": "closest", - "uirevision": f"{well}{depth_option}", + "uirevision": f"{well}{depthtype.value}", "title": f"{well} at {date}", } @property def figure(self) -> Dict[str, Any]: - return {"data": self.traces, "layout": self._layout} + return {"data": self._traces, "layout": self._layout} @property def xaxis_extension(self) -> float: @@ -75,17 +76,17 @@ def xaxis_extension(self) -> float: @property def pressure_range(self) -> List[float]: min_sim = ( - self.ertdf["SIMULATED"].min() + self._ertdf["SIMULATED"].min() if self.use_ertdf - else self.simdf["PRESSURE"].min() + else self._simdf["PRESSURE"].min() ) max_sim = ( - self.ertdf["SIMULATED"].max() + self._ertdf["SIMULATED"].max() if self.use_ertdf - else self.simdf["PRESSURE"].max() + else self._simdf["PRESSURE"].max() ) - min_obs = (self.ertdf["OBSERVED"] - self.ertdf["OBSERVED_ERR"]).min() - max_obs = (self.ertdf["OBSERVED"] + self.ertdf["OBSERVED_ERR"]).max() + min_obs = (self._ertdf["OBSERVED"] - self._ertdf["OBSERVED_ERR"]).min() + max_obs = (self._ertdf["OBSERVED"] + self._ertdf["OBSERVED_ERR"]).max() return [ min( @@ -101,29 +102,33 @@ def pressure_range(self) -> List[float]: @property def simdf_has_md(self) -> bool: if ( - self.simdf is not None - and "CONMD" in self.simdf - and len(self.simdf["CONMD"].unique()) == len(self.simdf["DEPTH"].unique()) + self._simdf is not None + and "CONMD" in self._simdf + and len(self._simdf["CONMD"].unique()) == len(self._simdf["DEPTH"].unique()) ): return True return False @property def use_ertdf(self) -> bool: - return self.simdf is None or ( - self.depth_option == "MD" and not self.simdf_has_md + return self._simdf is None or ( + self._depthtype == DepthType.MD.value and not self.simdf_has_md ) - def set_depth_columns(self) -> None: + @property + def ertdf_empty(self) -> bool: + return self._ertdf.empty + + def _set_depth_columns(self) -> None: """Set depth columns (md vs tvd)""" - self.ertdf["DEPTH"] = self.ertdf["TVD"] + self._ertdf["DEPTH"] = self._ertdf["TVD"] - if self.depth_option == "MD": - self.ertdf["DEPTH"] = self.ertdf["MD"] + if self._depthtype == DepthType.MD: + self._ertdf["DEPTH"] = self._ertdf[DepthType.MD.value] if self.simdf_has_md: - self.simdf["DEPTH"] = self.simdf["CONMD"] - if self.obsdf is not None and "MD" in self.obsdf: - self.obsdf["DEPTH"] = self.obsdf["MD"] + self._simdf["DEPTH"] = self._simdf["CONMD"] + if self._obsdf is not None and DepthType.MD.value in self._obsdf: + self._obsdf["DEPTH"] = self._obsdf[DepthType.MD.value] def add_formation(self, df: pd.DataFrame, fill_color: bool = True) -> None: """Plot zonation""" @@ -133,10 +138,10 @@ def add_formation(self, df: pd.DataFrame, fill_color: bool = True) -> None: for x in np.linspace(0, 1, len(df["ZONE"].unique())) ] - top_col = "TOP_TVD" if self.depth_option == "TVD" else "TOP_MD" - base_col = "BASE_TVD" if self.depth_option == "TVD" else "BASE_MD" + top_col = "TOP_TVD" if self._depthtype == DepthType.TVD else "TOP_MD" + base_col = "BASE_TVD" if self._depthtype == DepthType.TVD else "BASE_MD" - df = filter_frame(df, {"WELL": self.well}) + df = filter_frame(df, {"WELL": self._well}) df = df[df["TOP_MD"] != df["BASE_MD"]] for (_index, row) in df.iterrows(): @@ -163,7 +168,7 @@ def add_formation(self, df: pd.DataFrame, fill_color: bool = True) -> None: } ) # Add formation names - self.traces.append( + self._traces.append( { "showlegend": _index - 1 == df.index.min(), "name": "Formations", @@ -181,11 +186,11 @@ def add_formation(self, df: pd.DataFrame, fill_color: bool = True) -> None: self._layout.update({"shapes": formation_names}) def add_additional_observations(self) -> None: - if self.obsdf is not None: - self.traces.append( + if self._obsdf is not None: + self._traces.append( { - "x": self.obsdf["PRESSURE"], - "y": self.obsdf["DEPTH"], + "x": self._obsdf["PRESSURE"], + "y": self._obsdf["DEPTH"], "type": "scatter", "mode": "markers", "name": "Observations", @@ -194,7 +199,7 @@ def add_additional_observations(self) -> None: ) def add_ert_observed(self) -> None: - df = self.ertdf + df = self._ertdf if not df.empty: df = filter_frame( df, @@ -203,7 +208,7 @@ def add_ert_observed(self) -> None: "ENSEMBLE": df["ENSEMBLE"].unique()[0], }, ) - self.traces.append( + self._traces.append( { "x": df["OBSERVED"], "y": df["DEPTH"], @@ -223,10 +228,10 @@ def add_ert_observed(self) -> None: } ) - def add_simulated_lines(self, linetype: str) -> None: + def add_simulated_lines(self, linetype: LineType) -> None: if self.use_ertdf: - for ensemble, ensdf in self.ertdf.groupby("ENSEMBLE"): - self.traces.append( + for ensemble, ensdf in self._ertdf.groupby("ENSEMBLE"): + self._traces.append( { "x": ensdf["SIMULATED"], "y": ensdf["DEPTH"], @@ -234,17 +239,17 @@ def add_simulated_lines(self, linetype: str) -> None: "mode": "markers", "name": ensemble, "marker": { - "color": self.enscolors[ensemble], + "color": self._enscolors[ensemble], "size": 20, "line": {"width": 1, "color": "grey"}, }, } ) else: - if linetype == "realization": - for ensemble, ensdf in self.simdf.groupby("ENSEMBLE"): + if linetype == LineType.REALIZATION: + for ensemble, ensdf in self._simdf.groupby("ENSEMBLE"): for i, (real, realdf) in enumerate(ensdf.groupby("REAL")): - self.traces.append( + self._traces.append( { "x": realdf["PRESSURE"], "y": realdf["DEPTH"], @@ -252,15 +257,15 @@ def add_simulated_lines(self, linetype: str) -> None: "hovertext": f"Realization: {real}, Ensemble: {ensemble}", "type": "scatter", "mode": "lines", - "line": {"color": self.enscolors[ensemble]}, + "line": {"color": self._enscolors[ensemble]}, "name": ensemble, "showlegend": i == 0, "legendgroup": ensemble, "customdata": real, } ) - if linetype == "fanchart": - for ensemble, ensdf in self.simdf.groupby("ENSEMBLE"): + if linetype == LineType.FANCHART: + for ensemble, ensdf in self._simdf.groupby("ENSEMBLE"): quantiles = [10, 90] dframe = ( interpolate_depth(ensdf).drop(columns=["REAL"]).groupby("DEPTH") @@ -273,12 +278,12 @@ def add_simulated_lines(self, linetype: str) -> None: dframes[quantile_str] = dframe.quantile(q=quantile / 100.0) dframes["maximum"] = dframe.max() dframes["minimum"] = dframe.min() - self.traces.extend( + self._traces.extend( _get_fanchart_traces( pd.concat(dframes, names=["STATISTIC"], sort=False)[ "PRESSURE" ], - self.enscolors[ensemble], + self._enscolors[ensemble], ensemble, ) ) @@ -287,7 +292,7 @@ def color_by_param_value(self, df_norm: pd.DataFrame, selected_param: str) -> No """This method colors the RFT traces according to the value of the input selected_param """ - for trace in self.traces: + for trace in self._traces: if "customdata" in trace: trace["line"]["color"] = set_real_color( real_no=trace["customdata"], df_norm=df_norm @@ -298,7 +303,7 @@ def color_by_param_value(self, df_norm: pd.DataFrame, selected_param: str) -> No ) self._layout["title"] = { - "text": f"{self.well} colored by {selected_param}", + "text": f"{self._well} colored by {selected_param}", } @@ -306,7 +311,6 @@ def set_real_color(df_norm: pd.DataFrame, real_no: str) -> str: """ Return color for trace based on normalized parameter value. Midpoint for the colorscale is set on the average value - Same function as in Parameter Analysis. Shout be generalized """ red = rgba_to_str((255, 18, 67, 1)) diff --git a/webviz_subsurface/plugins/_rft_plotter/_business_logic.py b/webviz_subsurface/plugins/_rft_plotter/_utils/_rft_plotter_data_model.py similarity index 100% rename from webviz_subsurface/plugins/_rft_plotter/_business_logic.py rename to webviz_subsurface/plugins/_rft_plotter/_utils/_rft_plotter_data_model.py diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/__init__.py similarity index 100% rename from webviz_subsurface/plugins/_rft_plotter/_figures/__init__.py rename to webviz_subsurface/plugins/_rft_plotter/_views/__init__.py diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/__init__.py new file mode 100644 index 000000000..e500f7095 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/__init__.py @@ -0,0 +1 @@ +from ._view import MapView diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/__init__.py new file mode 100644 index 000000000..8e15114fa --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/__init__.py @@ -0,0 +1,2 @@ +from ._formation_plot_settings import FormationPlotSettings +from ._map_settings import MapSettings diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_formation_plot_settings.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_formation_plot_settings.py new file mode 100644 index 000000000..a55a52123 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_formation_plot_settings.py @@ -0,0 +1,160 @@ +from typing import Dict, List, Optional, Tuple + +import webviz_core_components as wcc +from dash import Input, Output, State, callback +from dash.development.base_component import Component +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ...._types import DepthType, LineType +from ...._utils import RftPlotterDataModel, filter_frame + + +class FormationPlotSettings(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLE = "ensemble" + WELL = "well" + DATE = "date" + LINETYPE = "linetype" + DEPTH_OPTION = "depth-option" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Formation plot settings") + self._datamodel = datamodel + self._ensembles = datamodel.ensembles + self._well_names = datamodel.well_names + + def layout(self) -> List[Component]: + ensemble = self._ensembles[0] if len(self._ensembles) > 0 else None + well = self._well_names[0] if len(self._well_names) > 0 else None + dates_in_well = self._datamodel.date_in_well(well) if well is not None else [] + return [ + wcc.Dropdown( + label="Ensemble", + id=self.register_component_unique_id(self.Ids.ENSEMBLE), + options=[{"label": ens, "value": ens} for ens in self._ensembles], + value=[ensemble], + multi=True, + clearable=False, + ), + wcc.Dropdown( + label="Well", + id=self.register_component_unique_id(self.Ids.WELL), + options=[{"label": well, "value": well} for well in self._well_names], + value=well, + clearable=False, + ), + wcc.Dropdown( + label="Date", + id=self.register_component_unique_id(self.Ids.DATE), + options=[{"label": date, "value": date} for date in dates_in_well], + clearable=False, + value=dates_in_well[0] if len(dates_in_well) > 0 else None, + ), + wcc.RadioItems( + label="Plot simulations as", + id=self.register_component_unique_id(self.Ids.LINETYPE), + options=[ + { + "label": "Realization lines", + "value": LineType.REALIZATION, + }, + { + "label": "Statistical fanchart", + "value": LineType.FANCHART, + }, + ], + value=LineType.REALIZATION, + ), + wcc.RadioItems( + label="Depth option", + id=self.register_component_unique_id(self.Ids.DEPTH_OPTION), + options=[ + { + "label": "TVD", + "value": DepthType.TVD, + }, + { + "label": "MD", + "value": DepthType.MD, + }, + ], + value=DepthType.TVD, + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output( + self.component_unique_id(self.Ids.LINETYPE).to_string(), + "options", + ), + Output( + self.component_unique_id(self.Ids.LINETYPE).to_string(), + "value", + ), + Input( + self.component_unique_id(self.Ids.DEPTH_OPTION).to_string(), + "value", + ), + State( + self.component_unique_id(self.Ids.LINETYPE).to_string(), + "value", + ), + State(self.component_unique_id(self.Ids.WELL).to_string(), "value"), + State(self.component_unique_id(self.Ids.DATE).to_string(), "value"), + ) + @callback_typecheck + def _update_linetype( + depthtype: DepthType, + current_linetype: LineType, + current_well: str, + current_date: str, + ) -> Tuple[List[Dict[str, str]], str]: + if self._datamodel.simdf is not None: + df = filter_frame( + self._datamodel.simdf, + {"WELL": current_well, "DATE": current_date}, + ) + if depthtype == DepthType.TVD or ( + depthtype == DepthType.MD + and "CONMD" in self._datamodel.simdf + and len(df["CONMD"].unique()) == len(df["DEPTH"].unique()) + ): + + return [ + { + "label": "Realization lines", + "value": LineType.REALIZATION, + }, + { + "label": "Statistical fanchart", + "value": LineType.FANCHART, + }, + ], current_linetype + + return [ + { + "label": "Realization lines", + "value": LineType.REALIZATION, + }, + ], LineType.REALIZATION + + @callback( + Output( + self.component_unique_id(self.Ids.DATE).to_string(), + "options", + ), + Output(self.component_unique_id(self.Ids.DATE).to_string(), "value"), + Input(self.component_unique_id(self.Ids.WELL).to_string(), "value"), + State(self.component_unique_id(self.Ids.DATE).to_string(), "value"), + ) + @callback_typecheck + def _update_date( + well: str, current_date: str + ) -> Tuple[List[Dict[str, str]], Optional[str]]: + dates = self._datamodel.date_in_well(well) + first_date = dates[0] if len(dates) > 0 else None + available_dates = [{"label": date, "value": date} for date in dates] + date = current_date if current_date in dates else first_date + return available_dates, date diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_map_settings.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_map_settings.py new file mode 100644 index 000000000..b4249a7e2 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_settings/_map_settings.py @@ -0,0 +1,100 @@ +from typing import Any, Dict, 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 ColorAndSizeByType + + +class MapSettings(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLE = "map-ensemble" + SIZE_BY = "map-size-by" + COLOR_BY = "map-color-by" + DATE_RANGE = "map-date-range" + ZONES = "map-zones" + + def __init__( + self, + ensembles: List[str], + zones: List[str], + date_marks: Dict[str, Dict[str, Any]], + date_range_min: int, + date_range_max: int, + ) -> None: + super().__init__("Map settings") + self._ensembles = ensembles + self._zone_names = zones + self._date_marks = date_marks + self._date_range_min = date_range_min + self._date_range_max = date_range_max + + 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], + value=self._ensembles[0], + clearable=False, + ), + wcc.Dropdown( + label="Size points by", + id=self.register_component_unique_id(self.Ids.SIZE_BY), + options=[ + { + "label": "Standard Deviation", + "value": ColorAndSizeByType.STDDEV, + }, + { + "label": "Misfit", + "value": ColorAndSizeByType.MISFIT, + }, + ], + value=ColorAndSizeByType.MISFIT, + clearable=False, + ), + wcc.Dropdown( + label="Color points by", + id=self.register_component_unique_id(self.Ids.COLOR_BY), + options=[ + { + "label": "Misfit", + "value": ColorAndSizeByType.MISFIT, + }, + { + "label": "Standard Deviation", + "value": ColorAndSizeByType.STDDEV, + }, + { + "label": "Year", + "value": ColorAndSizeByType.YEAR, + }, + ], + value=ColorAndSizeByType.STDDEV, + clearable=False, + ), + wcc.RangeSlider( + label="Filter date range", + id=self.register_component_unique_id(self.Ids.DATE_RANGE), + min=self._date_range_min, + max=self._date_range_max, + value=[ + self._date_range_min, + self._date_range_max, + ], + marks=self._date_marks, + ), + wcc.Label( + "Zone filter", + ), + wcc.SelectWithLabel( + size=min(10, len(self._zone_names)), + id=self.register_component_unique_id(self.Ids.ZONES), + options=[{"label": name, "value": name} for name in self._zone_names], + value=self._zone_names, + multi=True, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/__init__.py new file mode 100644 index 000000000..e9fb52e90 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/__init__.py @@ -0,0 +1 @@ +from ._map_figure import MapFigure diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/_map_figure.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/_map_figure.py similarity index 70% rename from webviz_subsurface/plugins/_rft_plotter/_figures/_map_figure.py rename to webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/_map_figure.py index 671e17056..d3b3b7a69 100644 --- a/webviz_subsurface/plugins/_rft_plotter/_figures/_map_figure.py +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_utils/_map_figure.py @@ -2,24 +2,32 @@ import pandas as pd +from ...._types import ColorAndSizeByType + class MapFigure: def __init__(self, ertdf: pd.DataFrame, ensemble: str, zones: List[str]) -> None: - self.ertdf = ( + self._ertdf = ( ertdf.loc[(ertdf["ENSEMBLE"] == ensemble) & (ertdf["ZONE"].isin(zones))] .groupby(["WELL", "DATE", "ENSEMBLE"]) .aggregate("mean") .reset_index() ) - self.traces: List[Dict[str, Any]] = [] - def add_misfit_plot(self, sizeby: str, colorby: str, dates: List[float]) -> None: - df = self.ertdf.loc[ - (self.ertdf["DATE_IDX"] >= dates[0]) & (self.ertdf["DATE_IDX"] <= dates[1]) - ] + self._traces: List[Dict[str, Any]] = [] - self.traces.append( + def add_misfit_plot( + self, + sizeby: ColorAndSizeByType, + colorby: ColorAndSizeByType, + dates: List[float], + ) -> None: + df = self._ertdf.loc[ + (self._ertdf["DATE_IDX"] >= dates[0]) + & (self._ertdf["DATE_IDX"] <= dates[1]) + ] + self._traces.append( { "x": df["EAST"], "y": df["NORTH"], @@ -39,13 +47,15 @@ def add_misfit_plot(self, sizeby: str, colorby: str, dates: List[float]) -> None # "name": date, "showlegend": False, "marker": { - "size": df[sizeby], - "sizeref": 2.0 * self.ertdf[sizeby].quantile(0.9) / (40.0**2), + "size": df[sizeby.value], + "sizeref": 2.0 + * self._ertdf[sizeby.value].quantile(0.9) + / (40.0**2), "sizemode": "area", "sizemin": 6, - "color": df[colorby], - "cmin": self.ertdf[colorby].min(), - "cmax": self.ertdf[colorby].quantile(0.9), + "color": df[colorby.value], + "cmin": self._ertdf[colorby.value].min(), + "cmax": self._ertdf[colorby.value].quantile(0.9), "colorscale": [[0, "#2584DE"], [1, "#E50000"]], "showscale": True, }, @@ -54,7 +64,7 @@ def add_misfit_plot(self, sizeby: str, colorby: str, dates: List[float]) -> None def add_fault_lines(self, df: pd.DataFrame) -> None: for _fault, faultdf in df.groupby("POLY_ID"): - self.traces.append( + self._traces.append( { "x": faultdf["X_UTME"], "y": faultdf["Y_UTMN"], @@ -77,3 +87,8 @@ def layout(self) -> Dict[str, Any]: "xaxis": {"constrain": "domain", "showgrid": False}, "yaxis": {"scaleanchor": "x", "showgrid": False}, } + + @property + def traces(self) -> List[Dict[str, Any]]: + """Returns the list of traces""" + return self._traces diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_view.py b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_view.py new file mode 100644 index 000000000..1d3744372 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_map_view/_view.py @@ -0,0 +1,221 @@ +from typing import Any, Dict, List, Union + +import webviz_core_components as wcc +from dash import Input, Output, State, callback +from dash.exceptions import PreventUpdate +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import ViewABC + +from ..._reusable_view_element import GeneralViewElement +from ..._types import ColorAndSizeByType, DepthType, LineType +from ..._utils import FormationFigure, RftPlotterDataModel +from ._settings import FormationPlotSettings, MapSettings +from ._utils import MapFigure + + +class MapView(ViewABC): + class Ids(StrEnum): + MAP_SETTINGS = "map-settings" + FORMATION_PLOT_SETTINGS = "formation-plot-settings" + MAP_VIEW_ELEMENT = "map-view-element" + FORMATION_PLOT_VIEW_ELEMENT = "formation-plot-view-element" + MAP_FIGURE = "map-figure" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Map") + self._datamodel = datamodel + + self.add_settings_group( + MapSettings( + ensembles=self._datamodel.ensembles, + zones=self._datamodel.zone_names, + date_marks=self._datamodel.date_marks, + date_range_min=self._datamodel.ertdatadf["DATE_IDX"].min(), + date_range_max=self._datamodel.ertdatadf["DATE_IDX"].max(), + ), + self.Ids.MAP_SETTINGS, + ) + self.add_settings_group( + FormationPlotSettings(self._datamodel), + self.Ids.FORMATION_PLOT_SETTINGS, + ) + + map_column = self.add_column() + map_column.add_view_element(GeneralViewElement(), self.Ids.MAP_VIEW_ELEMENT) + formation_plot_column = self.add_column() + formation_plot_column.add_view_element( + GeneralViewElement(), self.Ids.FORMATION_PLOT_VIEW_ELEMENT + ) + + def set_callbacks(self) -> None: + map_figure_id = self.view_element( + self.Ids.MAP_VIEW_ELEMENT + ).register_component_unique_id(self.Ids.MAP_FIGURE) + + @callback( + Output( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.WELL) + .to_string(), + "value", + ), + Input( + map_figure_id, + "clickData", + ), + State( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.WELL) + .to_string(), + "value", + ), + ) + def _get_clicked_well( + click_data: Dict[str, List[Dict[str, Any]]], well: str + ) -> str: + if not click_data: + return well + for layer in click_data["points"]: + try: + return layer["customdata"] + except KeyError: + pass + raise PreventUpdate + + @callback( + Output( + self.view_element(self.Ids.MAP_VIEW_ELEMENT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.MAP_SETTINGS) + .component_unique_id(MapSettings.Ids.ENSEMBLE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_SETTINGS) + .component_unique_id(MapSettings.Ids.SIZE_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_SETTINGS) + .component_unique_id(MapSettings.Ids.COLOR_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_SETTINGS) + .component_unique_id(MapSettings.Ids.DATE_RANGE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_SETTINGS) + .component_unique_id(MapSettings.Ids.ZONES) + .to_string(), + "value", + ), + ) + @callback_typecheck + def _update_map( + ensemble: str, + sizeby: ColorAndSizeByType, + colorby: ColorAndSizeByType, + dates: List[float], + zones: List[str], + ) -> Union[str, List[wcc.Graph]]: + figure = MapFigure(self._datamodel.ertdatadf, ensemble, zones) + if self._datamodel.faultlinesdf is not None: + figure.add_fault_lines(self._datamodel.faultlinesdf) + figure.add_misfit_plot(sizeby, colorby, dates) + + return [ + wcc.Graph( + style={"height": "84vh"}, + id=map_figure_id, + figure={"data": figure.traces, "layout": figure.layout}, + ) + ] + + @callback( + Output( + self.view_element(self.Ids.FORMATION_PLOT_VIEW_ELEMENT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.WELL) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.DATE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.ENSEMBLE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.LINETYPE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FORMATION_PLOT_SETTINGS) + .component_unique_id(FormationPlotSettings.Ids.DEPTH_OPTION) + .to_string(), + "value", + ), + ) + @callback_typecheck + def _update_formation_plot( + well: str, + date: str, + ensembles: List[str], + linetype: LineType, + depthtype: DepthType, + ) -> Union[str, List[wcc.Graph]]: + if not ensembles: + return "No ensembles selected" + + if date not in self._datamodel.date_in_well(well): + raise PreventUpdate + + figure = FormationFigure( + well=well, + ertdf=self._datamodel.ertdatadf, + enscolors=self._datamodel.enscolors, + depthtype=depthtype, + date=date, + ensembles=ensembles, + simdf=self._datamodel.simdf, + obsdf=self._datamodel.obsdatadf, + ) + if figure.ertdf_empty: + return ["No data matching the given filter criterias."] + + if self._datamodel.formations is not None: + figure.add_formation(self._datamodel.formationdf) + + figure.add_simulated_lines(linetype) + figure.add_additional_observations() + figure.add_ert_observed() + + return [ + wcc.Graph( + style={"height": "84vh"}, + figure=figure.figure, + ) + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/__init__.py new file mode 100644 index 000000000..57db05d24 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/__init__.py @@ -0,0 +1 @@ +from ._view import MisfitPerRealView diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_settings.py b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_settings.py new file mode 100644 index 000000000..f6c37f460 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_settings.py @@ -0,0 +1,27 @@ +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 Selections(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLES = "ensembles" + + def __init__(self, ensembles: List[str]) -> None: + super().__init__("Selections") + self._ensembles = ensembles + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Ensembles", + id=self.register_component_unique_id(self.Ids.ENSEMBLES), + options=[{"label": ens, "value": ens} for ens in self._ensembles], + value=[self._ensembles[0] if len(self._ensembles) > 0 else None], + clearable=False, + multi=True, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/__init__.py new file mode 100644 index 000000000..bcbaffa23 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/__init__.py @@ -0,0 +1 @@ +from ._misfit_per_real_figure import update_misfit_per_real_plot diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/_misfit_figure.py b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/_misfit_per_real_figure.py similarity index 95% rename from webviz_subsurface/plugins/_rft_plotter/_figures/_misfit_figure.py rename to webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/_misfit_per_real_figure.py index 8fce1a793..9487e800d 100644 --- a/webviz_subsurface/plugins/_rft_plotter/_figures/_misfit_figure.py +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_utils/_misfit_per_real_figure.py @@ -5,7 +5,9 @@ import webviz_core_components as wcc -def update_misfit_plot(df: pd.DataFrame, enscolors: Dict[str, Any]) -> List[wcc.Graph]: +def update_misfit_per_real_plot( + df: pd.DataFrame, enscolors: Dict[str, Any] +) -> List[wcc.Graph]: max_diff = find_max_diff(df) figures = [] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_view.py b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_view.py new file mode 100644 index 000000000..42617d116 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_misfit_per_real_view/_view.py @@ -0,0 +1,81 @@ +from typing import List, Union + +import webviz_core_components as wcc +from dash import Input, Output, callback +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewABC + +from ..._reusable_settings import FilterLayout +from ..._reusable_view_element import GeneralViewElement +from ..._utils import RftPlotterDataModel, filter_frame +from ._settings import Selections +from ._utils import update_misfit_per_real_plot + + +class MisfitPerRealView(ViewABC): + class Ids(StrEnum): + SELECTIONS = "selections" + FILTERS = "filters" + VIEW_ELEMENT = "view-element" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Misfit per real") + self._datamodel = datamodel + + self.add_settings_groups( + { + self.Ids.SELECTIONS: Selections(self._datamodel.ensembles), + self.Ids.FILTERS: FilterLayout( + wells=self._datamodel.well_names, + zones=self._datamodel.zone_names, + dates=self._datamodel.dates, + ), + } + ) + + self.add_view_element(GeneralViewElement(), self.Ids.VIEW_ELEMENT) + + def set_callbacks(self) -> None: + @callback( + Output( + self.view_element(self.Ids.VIEW_ELEMENT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.ENSEMBLES) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_WELLS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_ZONES) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_DATES) + .to_string(), + "value", + ), + ) + def _misfit_plot( + ensembles: List[str], wells: List[str], zones: List[str], dates: List[str] + ) -> Union[str, List[wcc.Graph]]: + df = filter_frame( + self._datamodel.ertdatadf, + {"WELL": wells, "ZONE": zones, "DATE": dates, "ENSEMBLE": ensembles}, + ) + if df.empty: + return "No data matching the given filter criterias" + + return update_misfit_per_real_plot(df, self._datamodel.enscolors) diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/__init__.py new file mode 100644 index 000000000..e13d481ae --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/__init__.py @@ -0,0 +1 @@ +from ._view import ParameterResponseView diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/__init__.py new file mode 100644 index 000000000..164e2f8af --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/__init__.py @@ -0,0 +1,3 @@ +from ._options import Options +from ._parameter_filter import ParameterFilterSettings +from ._selections import Selections diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_options.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_options.py new file mode 100644 index 000000000..e40aa89b1 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_options.py @@ -0,0 +1,51 @@ +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 CorrType, DepthType + + +class Options(SettingsGroupABC): + class Ids(StrEnum): + CORRTYPE = "corrtype" + DEPTHTYPE = "depthtype" + + def __init__(self) -> None: + super().__init__("Options") + + def layout(self) -> List[Component]: + return [ + wcc.RadioItems( + label="Correlation options", + id=self.register_component_unique_id(self.Ids.CORRTYPE), + options=[ + { + "label": "Simulated vs parameters", + "value": CorrType.SIM_VS_PARAM, + }, + { + "label": "Parameter vs simulated", + "value": CorrType.PARAM_VS_SIM, + }, + ], + value="sim_vs_param", + ), + wcc.RadioItems( + label="Depth option", + id=self.register_component_unique_id(self.Ids.DEPTHTYPE), + options=[ + { + "label": "TVD", + "value": DepthType.TVD, + }, + { + "label": "MD", + "value": DepthType.MD, + }, + ], + value=DepthType.TVD, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_parameter_filter.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_parameter_filter.py new file mode 100644 index 000000000..18cf37ce8 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_parameter_filter.py @@ -0,0 +1,27 @@ +from typing import List + +import pandas as pd +from dash.development.base_component import Component +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ......_components.parameter_filter import ParameterFilter + + +class ParameterFilterSettings(SettingsGroupABC): + class Ids(StrEnum): + PARAM_FILTER = "param-filter" + + def __init__(self, parameter_df: pd.DataFrame, mc_ensembles: List[str]) -> None: + super().__init__("Parameter Filter") + self._parameter_df = parameter_df + self._mc_ensembles = mc_ensembles + + def layout(self) -> List[Component]: + return ParameterFilter( + uuid=self.register_component_unique_id(self.Ids.PARAM_FILTER), + dframe=self._parameter_df[ + self._parameter_df["ENSEMBLE"].isin(self._mc_ensembles) + ].copy(), + reset_on_ensemble_update=True, + ).layout diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_selections.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_selections.py new file mode 100644 index 000000000..417c32b96 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_settings/_selections.py @@ -0,0 +1,100 @@ +from typing import Dict, List, Tuple + +import webviz_core_components as wcc +from dash import Input, Output, State, callback +from dash.development.base_component import Component +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + +from ...._utils import RftPlotterDataModel + + +class Selections(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLE = "param-response-ensemble" + WELL = "param-response-well" + DATE = "param-response-date" + ZONE = "param-response-zone" + PARAM = "param-response-param" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Selections") + self._datamodel = datamodel + self._ensembles = datamodel.ensembles + self._well_names = datamodel.well_names + self._params = datamodel.parameters if not datamodel.parameters is None else [] + self._parameter_df = datamodel.param_model.dataframe + + well = self._well_names[0] if self._well_names else "" + + self._dates_in_well, self._zones_in_well = self._datamodel.well_dates_and_zones( + well + ) + + 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], + value=self._ensembles[0], + clearable=False, + ), + wcc.Dropdown( + label="Well", + id=self.register_component_unique_id(self.Ids.WELL), + options=[{"label": well, "value": well} for well in self._well_names], + value=self._well_names[0] if self._well_names else "", + clearable=False, + ), + wcc.Dropdown( + label="Date", + id=self.register_component_unique_id(self.Ids.DATE), + options=[ + {"label": date, "value": date} for date in self._dates_in_well + ], + value=self._dates_in_well[0], + clearable=False, + ), + wcc.Dropdown( + label="Zone", + id=self.register_component_unique_id(self.Ids.ZONE), + options=[ + {"label": zone, "value": zone} for zone in self._zones_in_well + ], + value=self._zones_in_well[0], + clearable=False, + ), + wcc.Dropdown( + label="Parameter", + id=self.register_component_unique_id(self.Ids.PARAM), + options=[{"label": param, "value": param} for param in self._params], + clearable=False, + value=None, + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output(self.component_unique_id(self.Ids.DATE).to_string(), "options"), + Output(self.component_unique_id(self.Ids.DATE).to_string(), "value"), + Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"), + Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"), + Input(self.component_unique_id(self.Ids.WELL).to_string(), "value"), + State(self.component_unique_id(self.Ids.ZONE).to_string(), "value"), + ) + @callback_typecheck + def _update_date_and_zone( + well: str, zone_state: str + ) -> Tuple[List[Dict[str, str]], str, List[Dict[str, str]], str]: + """Update dates and zones when selecting well. If the current + selected zone is also present in the new well it will be kept as value. + """ + + dates_in_well, zones_in_well = self._datamodel.well_dates_and_zones(well) + return ( + [{"label": date, "value": date} for date in dates_in_well], + dates_in_well[0], + [{"label": zone, "value": zone} for zone in zones_in_well], + zone_state if zone_state in zones_in_well else zones_in_well[0], + ) diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_view.py b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_view.py new file mode 100644 index 000000000..ae1b3d8cd --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_parameter_response_view/_view.py @@ -0,0 +1,323 @@ +from typing import Any, Dict, List, Optional, Union + +import webviz_core_components as wcc +from dash import Input, Output, State, callback +from dash.exceptions import PreventUpdate +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import ViewABC + +from ....._figures import BarChart, ScatterPlot +from ..._reusable_view_element import GeneralViewElement +from ..._types import CorrType, DepthType, LineType +from ..._utils import FormationFigure, RftPlotterDataModel, correlate +from ._settings import Options, ParameterFilterSettings, Selections + + +class ParameterResponseView(ViewABC): + class Ids(StrEnum): + SELECTIONS = "selections" + OPTIONS = "options" + PARAMETER_FILTER = "parameter-filter" + FORMATION_PLOT = "formation-plot" + CORR_BARCHART = "corr-barchart" + CORR_BARCHART_FIGURE = "corr-barchart-figure" + SCATTERPLOT = "scatterplot" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Parameter Response") + self._datamodel = datamodel + self._parameter_df = datamodel.param_model.dataframe + + self.add_settings_group(Selections(self._datamodel), self.Ids.SELECTIONS) + self.add_settings_group(Options(), self.Ids.OPTIONS) + self.add_settings_group( + ParameterFilterSettings( + parameter_df=self._datamodel.param_model.dataframe, + mc_ensembles=self._datamodel.param_model.mc_ensembles, + ), + self.Ids.PARAMETER_FILTER, + ) + + first_column = self.add_column() + first_column.add_view_element(GeneralViewElement(), self.Ids.CORR_BARCHART) + first_column.add_view_element(GeneralViewElement(), self.Ids.SCATTERPLOT) + second_column = self.add_column() + second_column.add_view_element(GeneralViewElement(), self.Ids.FORMATION_PLOT) + + def set_callbacks(self) -> None: + + corr_barchart_figure_id = self.view_element( + self.Ids.CORR_BARCHART + ).register_component_unique_id(self.Ids.CORR_BARCHART_FIGURE) + + @callback( + Output( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.PARAM) + .to_string(), + "value", + ), + Input(corr_barchart_figure_id, "clickData"), + State( + self.settings_group(self.Ids.OPTIONS) + .component_unique_id(Options.Ids.CORRTYPE) + .to_string(), + "value", + ), + prevent_initial_call=True, + ) + @callback_typecheck + def _update_param_from_clickdata( + corr_vector_clickdata: Union[None, dict], + corrtype: CorrType, + ) -> str: + """Update the selected parameter from clickdata""" + if corr_vector_clickdata is None or corrtype == CorrType.PARAM_VS_SIM: + raise PreventUpdate + return corr_vector_clickdata.get("points", [{}])[0].get("y") + + @callback( + Output( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.WELL) + .to_string(), + "value", + ), + Input(corr_barchart_figure_id, "clickData"), + State( + self.settings_group(self.Ids.OPTIONS) + .component_unique_id(Options.Ids.CORRTYPE) + .to_string(), + "value", + ), + prevent_initial_call=True, + ) + @callback_typecheck + def _update_selections_from_clickdata( + corr_vector_clickdata: Union[None, dict], + corrtype: CorrType, + ) -> str: + """Update well, date and zone from clickdata""" + if corr_vector_clickdata is None or corrtype == CorrType.SIM_VS_PARAM: + raise PreventUpdate + + clickdata = corr_vector_clickdata.get("points", [{}])[0].get("y") + ls_clickdata = clickdata.split() + print("click dat ais: ", ls_clickdata) + return ls_clickdata[0] + + @callback( + Output( + self.view_element(self.Ids.CORR_BARCHART) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Output( + self.view_element(self.Ids.SCATTERPLOT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Output( + self.view_element(self.Ids.FORMATION_PLOT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.ENSEMBLE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.WELL) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.DATE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.ZONE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.PARAM) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.OPTIONS) + .component_unique_id(Options.Ids.CORRTYPE) + .to_string(), + "value", + ), + Input( + { + "id": self.settings_group(self.Ids.PARAMETER_FILTER) + .component_unique_id(ParameterFilterSettings.Ids.PARAM_FILTER) + .to_string(), + "type": "data-store", + }, + "data", + ), + Input( + self.settings_group(self.Ids.OPTIONS) + .component_unique_id(Options.Ids.DEPTHTYPE) + .to_string(), + "value", + ), + ) + @callback_typecheck + # pylint: disable=too-many-locals + def _update_paramresp_graphs( + ensemble: str, + well: str, + date: str, + zone: str, + param: Optional[str], + corrtype: CorrType, + real_filter: Dict[str, List[int]], + depthtype: DepthType, + ) -> List[Optional[Any]]: + """Main callback to update the graphs: + * ranked correlations bar chart + * response vs param scatter plot + * formations chart RFT pressure vs depth, colored by parameter value + """ + ( + df, + obs, + obs_err, + ens_params, + ens_rfts, + ) = self._datamodel.create_rft_and_param_pivot_table( + ensemble=ensemble, + well=well, + date=date, + zone=zone, + reals=real_filter[ensemble], + keep_all_rfts=(corrtype == CorrType.PARAM_VS_SIM), + ) + current_key = f"{well} {date} {zone}" + + if df is None: + # This happens if the filtering criterias returns no data + # Could f.ex happen when there are ensembles with different well names + return ["No data matching the given filter criterias."] * 3 + if param is not None and param not in ens_params: + # This happens if the selected parameter does not exist in the + # selected ensemble + return ["The selected parameter not valid for selected ensemble."] * 3 + if not ens_params: + # This happens if there are multiple ensembles and one of the ensembles + # doesn't have non-constant parameters. + return ["The selected ensemble has no non-constant parameters."] * 3 + + if corrtype == CorrType.SIM_VS_PARAM or param is None: + corrseries = correlate(df[ens_params + [current_key]], current_key) + param = param if param is not None else corrseries.abs().idxmax() + corr_title = f"{current_key} vs parameters" + scatter_x, scatter_y, highlight_bar = param, current_key, param + + if corrtype == CorrType.PARAM_VS_SIM: + corrseries = correlate(df[ens_rfts + [param]], param) + corr_title = f"{param} vs simulated RFTs" + scatter_x, scatter_y, highlight_bar = param, current_key, current_key + + # Correlation bar chart + corrfig = BarChart(corrseries, n_rows=15, title=corr_title, orientation="h") + corrfig.color_bars(highlight_bar, "#007079", 0.5) + corr_graph = wcc.Graph( + style={"height": "40vh"}, + figure=corrfig.figure, + id=corr_barchart_figure_id, + ) + + # Scatter plot + scatterplot = ScatterPlot( + df, scatter_y, scatter_x, "#007079", f"{current_key} vs {param}" + ) + scatterplot.add_vertical_line_with_error( + obs, + obs_err, + df[param].min(), + df[param].max(), + ) + scatter_graph = ( + wcc.Graph( + style={"height": "40vh"}, + figure=scatterplot.figure, + ), + ) + + # Formations plot + formations_figure = FormationFigure( + well=well, + ertdf=self._datamodel.ertdatadf, + enscolors=self._datamodel.enscolors, + depthtype=depthtype, + date=date, + ensembles=[ensemble], + reals=real_filter[ensemble], + simdf=self._datamodel.simdf, + obsdf=self._datamodel.obsdatadf, + ) + + if formations_figure.use_ertdf: + return [ + corr_graph, + scatter_graph, + f"Realization lines not available for depth option {depthtype}", + ] + + if self._datamodel.formations is not None: + formations_figure.add_formation( + self._datamodel.formationdf, fill_color=False + ) + + formations_figure.add_simulated_lines(LineType.REALIZATION) + formations_figure.add_additional_observations() + formations_figure.add_ert_observed() + + df_value_norm = self._datamodel.get_param_real_and_value_df( + ensemble, parameter=param, normalize=True + ) + formations_figure.color_by_param_value(df_value_norm, param) + + return [ + corr_graph, + scatter_graph, + wcc.Graph( + style={"height": "87vh"}, + figure=formations_figure.figure, + ), + ] + + @callback( + Output( + { + "id": ParameterFilterSettings.Ids.PARAM_FILTER, + "type": "ensemble-update", + }, + "data", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.ENSEMBLE) + .to_string(), + "value", + ), + ) + def _update_parameter_filter_selection(ensemble: str) -> List[str]: + """Update ensemble in parameter filter""" + return [ensemble] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/__init__.py new file mode 100644 index 000000000..8197880f8 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/__init__.py @@ -0,0 +1 @@ +from ._view import SimVsObsView diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/__init__.py new file mode 100644 index 000000000..18ad93440 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/__init__.py @@ -0,0 +1,2 @@ +from ._selections import PlotType, Selections +from ._size_color_settings import SizeColorSettings diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_selections.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_selections.py new file mode 100644 index 000000000..4c37e9135 --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_selections.py @@ -0,0 +1,48 @@ +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 PlotType(StrEnum): + CROSSPLOT = "crossplot" + ERROR_BOXPLOT = "error-boxplot" + + +class Selections(SettingsGroupABC): + class Ids(StrEnum): + PLOT_TYPE = "plot-type" + ENSEMBLES = "ensembles" + + def __init__(self, ensembles: List[str]) -> None: + super().__init__("Selections") + self._ensembles = ensembles + + def layout(self) -> List[Component]: + return [ + wcc.RadioItems( + label="Plot Type", + id=self.register_component_unique_id(self.Ids.PLOT_TYPE), + options=[ + { + "label": "CrossPlot", + "value": PlotType.CROSSPLOT, + }, + { + "label": "Error BoxPlot", + "value": PlotType.ERROR_BOXPLOT, + }, + ], + value=PlotType.CROSSPLOT, + ), + wcc.Dropdown( + label="Ensembles", + id=self.register_component_unique_id(self.Ids.ENSEMBLES), + options=[{"label": ens, "value": ens} for ens in self._ensembles], + value=[self._ensembles[0] if len(self._ensembles) > 0 else None], + clearable=False, + multi=True, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_size_color_settings.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_size_color_settings.py new file mode 100644 index 000000000..6e83c5b4a --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_settings/_size_color_settings.py @@ -0,0 +1,53 @@ +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 ColorAndSizeByType + + +class SizeColorSettings(SettingsGroupABC): + class Ids(StrEnum): + CROSSPLOT_COLOR_BY = "crossplot-color-by" + CROSSPLOT_SIZE_BY = "crossplot-size-by" + + def __init__(self) -> None: + super().__init__("Crossplot options") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Color by", + id=self.register_component_unique_id(self.Ids.CROSSPLOT_COLOR_BY), + options=[ + { + "label": "Misfit", + "value": ColorAndSizeByType.MISFIT, + }, + { + "label": "Standard Deviation", + "value": ColorAndSizeByType.STDDEV, + }, + ], + value=ColorAndSizeByType.STDDEV, + clearable=False, + ), + wcc.Dropdown( + label="Size by", + id=self.register_component_unique_id(self.Ids.CROSSPLOT_SIZE_BY), + options=[ + { + "label": "Standard Deviation", + "value": ColorAndSizeByType.STDDEV, + }, + { + "label": "Misfit", + "value": ColorAndSizeByType.MISFIT, + }, + ], + value=ColorAndSizeByType.MISFIT, + clearable=False, + ), + ] diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/__init__.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/__init__.py new file mode 100644 index 000000000..970f8c5ac --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/__init__.py @@ -0,0 +1,2 @@ +from ._crossplot_figure import update_crossplot +from ._errorplot_figure import update_errorplot diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/_crossplot_figure.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/_crossplot_figure.py similarity index 88% rename from webviz_subsurface/plugins/_rft_plotter/_figures/_crossplot_figure.py rename to webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/_crossplot_figure.py index e77a51c77..24959cb1d 100644 --- a/webviz_subsurface/plugins/_rft_plotter/_figures/_crossplot_figure.py +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/_crossplot_figure.py @@ -4,8 +4,12 @@ import pandas as pd import webviz_core_components as wcc +from ...._types import ColorAndSizeByType -def update_crossplot(df: pd.DataFrame, sizeby: str, colorby: str) -> List[wcc.Graph]: + +def update_crossplot( + df: pd.DataFrame, sizeby: ColorAndSizeByType, colorby: ColorAndSizeByType +) -> List[wcc.Graph]: sim_range = find_sim_range(df) sizeref, cmin, cmax = size_color_settings(df, sizeby, colorby) @@ -40,11 +44,11 @@ def update_crossplot(df: pd.DataFrame, sizeby: str, colorby: str) -> List[wcc.Gr ], "hoverinfo": "text", "marker": { - "size": dframe[sizeby], + "size": dframe[sizeby.value], "sizeref": 2.0 * sizeref / (30.0**2), "sizemode": "area", "sizemin": 6, - "color": dframe[colorby], + "color": dframe[colorby.value], "cmin": cmin, "cmax": cmax, "colorscale": [[0, "#2584DE"], [1, "#E50000"]], @@ -90,14 +94,14 @@ def update_crossplot(df: pd.DataFrame, sizeby: str, colorby: str) -> List[wcc.Gr def size_color_settings( - df: pd.DataFrame, sizeby: str, colorby: str + df: pd.DataFrame, sizeby: ColorAndSizeByType, colorby: ColorAndSizeByType ) -> Tuple[np.float64, np.float64, np.float64]: df = df.groupby(["WELL", "DATE", "ZONE", "TVD", "ENSEMBLE"]).mean().reset_index() - sizeref = df[sizeby].quantile(0.9) - cmin = df[colorby].min() - cmax = df[colorby].quantile(0.9) + sizeref = df[sizeby.value].quantile(0.9) + cmin = df[colorby.value].min() + cmax = df[colorby.value].quantile(0.9) return sizeref, cmin, cmax diff --git a/webviz_subsurface/plugins/_rft_plotter/_figures/_errorplot_figure.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/_errorplot_figure.py similarity index 100% rename from webviz_subsurface/plugins/_rft_plotter/_figures/_errorplot_figure.py rename to webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_utils/_errorplot_figure.py diff --git a/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_view.py b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_view.py new file mode 100644 index 000000000..36ae52dfc --- /dev/null +++ b/webviz_subsurface/plugins/_rft_plotter/_views/_sim_vs_obs_view/_view.py @@ -0,0 +1,113 @@ +from typing import List, Union + +import webviz_core_components as wcc +from dash import Input, Output, callback +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import ViewABC + +from ..._reusable_settings import FilterLayout +from ..._reusable_view_element import GeneralViewElement +from ..._types import ColorAndSizeByType +from ..._utils import RftPlotterDataModel, filter_frame +from ._settings import PlotType, Selections, SizeColorSettings +from ._utils import update_crossplot, update_errorplot + + +class SimVsObsView(ViewABC): + class Ids(StrEnum): + SELECTIONS = "selections" + FILTERS = "filters" + SIZE_COLOR_SETTINGS = "size-color-settings" + VIEW_ELEMENT = "view-element" + + def __init__(self, datamodel: RftPlotterDataModel) -> None: + super().__init__("Sim vs obs") + self._datamodel = datamodel + + self.add_settings_groups( + { + self.Ids.SELECTIONS: Selections(self._datamodel.ensembles), + self.Ids.FILTERS: FilterLayout( + wells=self._datamodel.well_names, + zones=self._datamodel.zone_names, + dates=self._datamodel.dates, + ), + self.Ids.SIZE_COLOR_SETTINGS: SizeColorSettings(), + } + ) + + self.add_view_element(GeneralViewElement(), self.Ids.VIEW_ELEMENT) + + def set_callbacks(self) -> None: + @callback( + Output( + self.view_element(self.Ids.VIEW_ELEMENT) + .component_unique_id(GeneralViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.PLOT_TYPE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SELECTIONS) + .component_unique_id(Selections.Ids.ENSEMBLES) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_WELLS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_ZONES) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(FilterLayout.Ids.FILTER_DATES) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SIZE_COLOR_SETTINGS) + .component_unique_id(SizeColorSettings.Ids.CROSSPLOT_SIZE_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SIZE_COLOR_SETTINGS) + .component_unique_id(SizeColorSettings.Ids.CROSSPLOT_COLOR_BY) + .to_string(), + "value", + ), + ) + @callback_typecheck + def _update_graph( + plot_type: PlotType, + ensembles: List[str], + wells: List[str], + zones: List[str], + dates: List[str], + sizeby: ColorAndSizeByType, + colorby: ColorAndSizeByType, + ) -> Union[str, List[wcc.Graph]]: + df = filter_frame( + self._datamodel.ertdatadf, + {"WELL": wells, "ZONE": zones, "DATE": dates, "ENSEMBLE": ensembles}, + ) + if df.empty: + return "No data matching the given filter criterias" + + if plot_type == PlotType.CROSSPLOT: + return update_crossplot(df, sizeby, colorby) + if plot_type == PlotType.ERROR_BOXPLOT: + return [update_errorplot(df, self._datamodel.enscolors)] + raise ValueError(f"Plot type: {plot_type.value} not implemented")