From e3c723f28112fa4239734c77704dbeba060736b8 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Thu, 28 Jul 2022 15:58:38 +0200 Subject: [PATCH 01/16] rebuild seismic misfit --- .../plugins/_seismic_misfit/__init__.py | 1 + .../plugins/_seismic_misfit/_plugin.py | 395 ++++++ .../plugins/_seismic_misfit/_plugin_ids.py | 11 + .../_seismic_misfit/_seismic_color_scales.py | 37 + .../_shared_settings/__init__.py | 5 + .../_shared_settings/_case_settings.py | 49 + .../_shared_settings/_filter_settings.py | 37 + .../_shared_settings/_map_plot_settings.py | 109 ++ .../_shared_settings/_plot_options.py | 79 ++ .../_plot_settings_and_layout.py | 74 ++ .../_supporting_files/_dataframe_functions.py | 357 ++++++ .../_supporting_files/_plot_functions.py | 1062 +++++++++++++++++ .../_supporting_files/_support_functions.py | 116 ++ .../_view_elements/__init__.py | 2 + .../_view_elements/_info_box.py | 24 + .../_seismic_misfit/_view_elements/_slider.py | 27 + .../_seismic_misfit/_views/__init__.py | 5 + .../_seismic_misfit/_views/_crossplot.py | 176 +++ .../_seismic_misfit/_views/_errorbar_plot.py | 400 +++++++ .../_seismic_misfit/_views/_map_plot.py | 389 ++++++ .../_views/_misfit_per_real.py | 306 +++++ .../_seismic_misfit/_views/_obs_data.py | 401 +++++++ ...ismic_misfit.py => _seismic_misfit_ori.py} | 0 23 files changed, 4062 insertions(+) create mode 100644 webviz_subsurface/plugins/_seismic_misfit/__init__.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_plugin.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/__init__.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_case_settings.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_map_plot_settings.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_settings_and_layout.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_support_functions.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_view_elements/__init__.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_view_elements/_info_box.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/__init__.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py rename webviz_subsurface/plugins/{_seismic_misfit.py => _seismic_misfit_ori.py} (100%) diff --git a/webviz_subsurface/plugins/_seismic_misfit/__init__.py b/webviz_subsurface/plugins/_seismic_misfit/__init__.py new file mode 100644 index 000000000..019e7d0bd --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/__init__.py @@ -0,0 +1 @@ +from ._plugin import SeismicMisfit diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py new file mode 100644 index 000000000..e0e7c7a43 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py @@ -0,0 +1,395 @@ +import logging +from typing import Callable, List, Tuple + +from dash import html +from dash.development.base_component import Component +from webviz_config import WebvizPluginABC, WebvizSettings + +from ._plugin_ids import PluginIds +from ._shared_settings import CaseSettings, MapPlotSettings +from ._supporting_files._dataframe_functions import make_polygon_df, makedf +from ._supporting_files._support_functions import ( + _compare_dfs_obs, + _map_initial_marker_size, +) +from ._views import Crossplot, ErrorbarPlots, MapPlot, MisfitPerReal, ObsData +from ._views._obs_data import ObsFilterSettings, RawPlotSettings + + +class SeismicMisfit(WebvizPluginABC): + """Seismic misfit plotting. + Consists of several tabs with different plots of + observed and simulated seismic 4d attribute. + * Seismic obs data (overview) + * Seismic misfit per real (misfit quantification and ranking) + * Seismic crossplot - sim vs obs (data points statistics) + * Seismic errorbar plot - sim vs obs (data points statistics) + * Seismic map plot - sim vs obs (data points statistics) + + --- + + * **`ensembles`:** Which *scratch_ensembles* in *shared_settings* to include. +
(Note that **realization-** must be part of the *shared_settings* paths.) + + * **`attributes`:** List of the simulated attribute file names to include. + It is a requirement that there is a corresponding file with the observed + and meta data included. This file must have the same name, but with an + additional prefix = "meta--". For example, if one includes a file + called "my_awesome_attribute.txt" in the attributes list, the corresponding + obs/meta file must be called "meta--my_awesome_attribute.txt". See Data input + section for more details. + + * **`attribute_sim_path`:** Path to the `attributes` simulation file. + Path is given as relative to *runpath*, where *runpath* = path as defined + for `ensembles` in shared settings. + + * **`attribute_obs_path`:** Path to the `attributes` obs/meta file. + Path is either given as relative to *runpath* or as an absolute path. + + * **`obs_mult`:** Multiplier for all observation and observation error data. + Can be used for calibration purposes. + + * **`sim_mult`:** Multiplier for all simulated data. + Can be used for calibration purposes. + + * **`polygon`:** Path to a folder or a file containing (fault-) polygons. + If value is a folder all csv files in that folder will be included + (e.g. "share/results/polygons/"). + If value is a file, then that file will be read. One can also use \\*-notation + in filename to read filtered list of files + (e.g. "share/results/polygons/\\*faultlines\\*csv"). + Path is either given as relative to *runpath* or as an absolute path. + If path is ambigious (e.g. with multi-realization runpath), + only the first successful find is used. + + * **`realrange`:** Realization range filter for each of the ensembles. + Assign as list of two integers in square brackets (e.g. [0, 99]). + Realizations outside range will be excluded. + If `realrange` is omitted, no realization filter will be applied (i.e. include all). + + --- + + a) The required input data consists of 2 different file types.
+ + 1) Observation and meta data csv file (one per attribute): + This csv file must contain the 5 column headers "EAST" (or "X_UTME"), + "NORTH" (or "Y_UTMN"), "REGION", "OBS" and "OBS_ERROR". + The column names are case insensitive and can be in any order. + "OBS" is the observed attribute value and "OBS_ERROR" + is the corresponding error.
+ ```csv + X_UTME,Y_UTMN,REGION,OBS,OBS_ERROR + 456166.26,5935963.72,1,0.002072,0.001 + 456241.17,5935834.17,2,0.001379,0.001 + 456316.08,5935704.57,3,0.001239,0.001 + ... + ... + ``` + 2) Simulation data file (one per attribute and realization): + This is a 1 column file (ERT compatible format). + The column is the simulated attribute value. This file has no header. + ``` + 0.0023456 + 0.0012345 + 0.0013579 + ... + ... + ``` + + It is a requirement that each line of data in these 2 files represent + the same data point. I.e. line number N+1 in obs/metadata file corresponds to + line N in sim files. The +1 shift for the obs/metadata file + is due to that file is the only one with a header. + + b) Polygon data is optional to include. Polygons must be stored in + csv file(s) on the format shown below. A csv file can have multiple + polygons (e.g. fault polygons), identified with the ID value. + The alternative header names "X_UTME", "Y_UTMN", "Z_TVDSS", "POLY_ID" will also + be accepted. The "Z"/"Z_TVDSS" column can be omitted. Any other column can be + included, but they will be skipped upon reading. + ```csv + X,Y,Z,ID + 460606.36,5935605.44,1676.49,1 + 460604.92,5935583.99,1674.84,1 + 460604.33,5935575.08,1674.16,3 + ... + ... + ``` + """ + + def __init__( + self, + webviz_settings: WebvizSettings, + ensembles: List[str], + attributes: List[str], + attribute_sim_path: str = "share/results/maps/", + attribute_obs_path: str = "../../share/observations/seismic/", + obs_mult: float = 1.0, + sim_mult: float = 1.0, + polygon: str = None, + realrange: List[List[int]] = None, + ) -> None: + super().__init__() + + self.attributes = attributes + + self.ensemble_set = { + ens: webviz_settings.shared_settings["scratch_ensembles"][ens] + for ens in ensembles + } + + self.ens_names = [] + for ens_name, _ in self.ensemble_set.items(): + self.ens_names.append(ens_name) + + self.polygon = polygon + if not polygon: + self.df_polygons = None + logging.info("Polygon not assigned in config file - continue without.\n") + else: # grab polygon files and store in dataframe + self.df_polygons = make_polygon_df( + ensemble_set=self.ensemble_set, polygon=self.polygon + ) + + self.caseinfo = "" + self.dframe = {} + self.dframeobs = {} + self.makedf_args = {} + self.region_names: List[int] = [] + + for attribute_name in self.attributes: + logging.debug(f"Build dataframe for attribute: \n{attribute_name}\n") + # make dataframe with all data + self.dframe[attribute_name] = makedf( + self.ensemble_set, + attribute_name, + attribute_sim_path, + attribute_obs_path, + obs_mult, + sim_mult, + realrange, + ) + # make dataframe with only obs and meta data + self.dframeobs[attribute_name] = self.dframe[attribute_name].drop( + columns=[ + col + for col in self.dframe[attribute_name] + if col.startswith("real-") + ] + ) + + self.makedf_args[attribute_name] = { # for add_webvizstore + "ensemble_set": self.ensemble_set, + "attribute_name": attribute_name, + "attribute_sim_path": attribute_sim_path, + "attribute_obs_path": attribute_obs_path, + "obs_mult": obs_mult, + "sim_mult": sim_mult, + "realrange": realrange, + } + + obsinfo = _compare_dfs_obs(self.dframeobs[attribute_name], self.ens_names) + self.caseinfo = ( + f"{self.caseinfo}Attribute: {attribute_name}" + f"\n{obsinfo}\n-----------\n" + ) + + # get sorted list of unique region values + # simplified approach: union across all attributes/metafiles + if not self.region_names: + self.region_names = sorted( + list(self.dframeobs[attribute_name]["region"].unique()) + ) + else: + for regname in self.dframeobs[attribute_name]["region"].unique(): + if regname not in self.region_names: + self.region_names.append(regname) + self.region_names = sorted(self.region_names) + + # get list of all realizations (based on column names real-x) + self.realizations = [ + col.replace("real-", "") + for col in self.dframe[attributes[0]] + if col.startswith("real") + ] + + self.add_view( + ObsData( + self.attributes, + self.ens_names, + self.region_names, + self.dframeobs, + self.df_polygons, + self.caseinfo, + ), + PluginIds.ViewsIds.OBS_DATA, + PluginIds.ViewsIds.VIEWS_GROUP, + ) + + self.add_view( + MisfitPerReal( + self.attributes, + self.ens_names, + self.region_names, + self.realizations, + self.dframe, + self.caseinfo, + ), + PluginIds.ViewsIds.MISFIT_PER_REAL, + PluginIds.ViewsIds.VIEWS_GROUP, + ) + + self.add_view( + Crossplot( + self.attributes, + self.ens_names, + self.region_names, + self.realizations, + self.dframe, + self.caseinfo, + ), + PluginIds.ViewsIds.CROSSPLOT, + PluginIds.ViewsIds.VIEWS_GROUP, + ) + + self.add_view( + ErrorbarPlots( + self.attributes, + self.ens_names, + self.region_names, + self.realizations, + self.dframe, + self.caseinfo, + ), + PluginIds.ViewsIds.ERRORBAR_PLOTS, + PluginIds.ViewsIds.VIEWS_GROUP, + ) + + self.add_view( + MapPlot( + self.attributes, + self.ens_names, + self.region_names, + self.realizations, + self.dframe, + self.dframeobs, + self.df_polygons, + self.caseinfo, + ), + PluginIds.ViewsIds.MAP_PLOT, + PluginIds.ViewsIds.VIEWS_GROUP, + ) + + @property + def layout(self) -> Component: + return html.Div("No view is loaded.") + + def add_webvizstore(self) -> List[Tuple[Callable, list]]: + funcs = [] + for attribute_name in self.attributes: + funcs.append((makedf, [self.makedf_args[attribute_name]])) + if self.polygon is not None: + funcs.append( + ( + make_polygon_df, + [ + { + "ensemble_set": self.ensemble_set, + "polygon": self.polygon, + } + ], + ) + ) + return funcs + + @property + def tour_steps(self) -> List[dict]: + return [ + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .layout_element(ObsData.Ids.GRAPHS_RAW) + .get_unique_id(), + "content": ("Observation data 'raw' plot."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .layout_element(ObsData.Ids.GRAPHS_MAP) + .get_unique_id(), + "content": ("Observation data map view plot."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME), + "content": ( + "Select ensemble to view. " + "One can only select one at a time in this tab." + ), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME), + "content": ( + "Select which attribute to view. One can only select one at a time." + ), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.REGION_NAME), + "content": ("Region filter. "), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.NOISE_FILTER), + "content": ("Noise filter. In steps of half of the lowest obs error."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.OBS_ERROR), + "content": ("Toggle observation error on or off."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.HISTOGRAM), + "content": ("Toggle observation data histogram on or off."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.X_AXIS_SETTINGS), + "content": ( + "Use original ordering (as from imported data) or reset index" + + " (can be useful in combination with filters." + ), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY), + "content": ("Select data to use for coloring of the map view plot."), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .settings_group(ObsData.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING), + "content": ( + "Select color range scaling factor used " + + "with the map view plot." + ), + }, + { + "id": self.view(PluginIds.ViewsIds.OBS_DATA) + .layout_element(ObsData.Ids.ERROR_INFO) + .get_unique_id(), + "content": ( + "Info of the ensembles observation data comparison. " + + "For a direct comparison they should have the same " + + "observation and observation error data." + ), + }, + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py new file mode 100644 index 000000000..36ade49b1 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py @@ -0,0 +1,11 @@ +class PluginIds: + class SharedSettings: + CASE_SETTINGS = "case-settings" + + class ViewsIds: + VIEWS_GROUP = "Seismic" + OBS_DATA = "obs-data" + MISFIT_PER_REAL = "misfit-per-real" + CROSSPLOT = "crossplot" + ERRORBAR_PLOTS = "errorbar-plots" + MAP_PLOT = "map-plot" diff --git a/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py b/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py new file mode 100644 index 000000000..f0eb57afc --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py @@ -0,0 +1,37 @@ +class ColorScales: + SEISMIC_SYMMETRIC = [ + [0, "yellow"], + [0.1, "orangered"], + [0.3, "darkred"], + [0.4, "dimgrey"], + [0.45, "lightgrey"], + [0.5, "WhiteSmoke"], + [0.55, "lightgrey"], + [0.6, "dimgrey"], + [0.7, "darkblue"], + [0.9, "blue"], + [1, "cyan"], + ] + SEISMIC_ERROR = [ + [0, "silver"], + [0.20, "darkred"], + [0.60, "orangered"], + [0.90, "orange"], + [1, "yellow"], + ] + SEISMIC_DIFF = [ + [0, "WhiteSmoke"], + [0.10, "lightgrey"], + [0.33, "dimgrey"], + [0.67, "orangered"], + [1, "yellow"], + ] + SEISMIC_COVERAGE = [ + [0, "blue"], + [0.33, "lightblue"], + [0.36, "lightgreen"], + [0.5, "beige"], + [0.64, "lightgreen"], + [0.67, "lightcoral"], + [1, "red"], + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/__init__.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/__init__.py new file mode 100644 index 000000000..33e45df32 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/__init__.py @@ -0,0 +1,5 @@ +from ._case_settings import CaseSettings +from ._filter_settings import FilterSettings +from ._map_plot_settings import MapPlotSettings +from ._plot_options import PlotOptions +from ._plot_settings_and_layout import PlotSettingsAndLayout diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_case_settings.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_case_settings.py new file mode 100644 index 000000000..f12e156b6 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_case_settings.py @@ -0,0 +1,49 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + + +class CaseSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + ATTRIBUTE_NAME = "attribute-name" + ENSEMBLES_NAME = "ensembles-name" + + def __init__(self, attributes: List[str], ens_names: List) -> None: + super().__init__("Case sttings") + self.attributes = attributes + self.ens_names = ens_names + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Attribute selector", + id=self.register_component_unique_id(self.Ids.ATTRIBUTE_NAME), + optionHeight=60, + options=[ + { + "label": attr.replace(".txt", "") + .replace("_", " ") + .replace("--", " "), + "value": attr, + } + for attr in self.attributes + ], + value=self.attributes[0], + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Ensemble selector", + id=self.register_component_unique_id(self.Ids.ENSEMBLES_NAME), + options=[{"label": ens, "value": ens} for ens in self.ens_names], + value=self.ens_names, + multi=True, + clearable=False, + persistence=True, + persistence_type="memory", + ), + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py new file mode 100644 index 000000000..a1c00742e --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py @@ -0,0 +1,37 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + + +class FilterSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + REGION_SELECTOR = "region-selector" + REALIZATION_SELECTOR = "realization-selector" + + def __init__(self, region_names, realizations) -> None: + super().__init__("Filter sttings") + self.region_names = region_names + self.realizations = realizations + + def layout(self) -> List[Component]: + return [ + wcc.SelectWithLabel( + label="Region selector", + id=self.register_component_unique_id(self.Ids.REGION_SELECTOR), + options=[ + {"label": regno, "value": regno} for regno in self.region_names + ], + value=self.region_names, + size=min([len(self.region_names), 5]), + ), + wcc.SelectWithLabel( + label="Realization selector", + id=self.register_component_unique_id(self.Ids.REALIZATION_SELECTOR), + options=[{"label": real, "value": real} for real in self.realizations], + value=self.realizations, + size=min([len(self.realizations), 5]), + ), + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_map_plot_settings.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_map_plot_settings.py new file mode 100644 index 000000000..3bc7fa17d --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_map_plot_settings.py @@ -0,0 +1,109 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + + +class MapPlotSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + COLOR_BY = "color-by" + COLOR_RANGE_SCALING = "color-range-scaling" + MARKER_SIZE = "marker-size" + POLYGONS = "polygons" + + def __init__(self, map_intial_marker_size: int, polygon_names: List) -> None: + super().__init__("Map plot settings") + self.map_intial_marker_size = map_intial_marker_size + self.polygon_names = polygon_names + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Color by", + id=self.register_component_unique_id(self.Ids.COLOR_BY), + options=[ + { + "label": "region", + "value": "region", + }, + { + "label": "obs", + "value": "obs", + }, + { + "label": "obs error", + "value": "obs_error", + }, + ], + value="obs", + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Color range scaling (relative to max)", + id=self.register_component_unique_id(self.Ids.COLOR_RANGE_SCALING), + options=[ + {"label": f"{x:.0%}", "value": x} + for x in [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1.0, + ] + ], + style={"display": "block"}, + value=0.8, + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Marker size", + id=self.register_component_unique_id(self.Ids.MARKER_SIZE), + options=[ + {"label": val, "value": val} + for val in sorted( + [ + self.map_intial_marker_size, + 2, + 5, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 25, + 30, + ] + ) + ], + value=self.map_intial_marker_size, + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Polygons", + id=self.register_component_unique_id(self.Ids.POLYGONS), + optionHeight=60, + options=[ + {"label": polyname, "value": polyname} + for polyname in self.polygon_names + ], + multi=False, + clearable=True, + persistence=True, + persistence_type="memory", + ), + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py new file mode 100644 index 000000000..83fc39e71 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py @@ -0,0 +1,79 @@ +from typing import List +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC +import webviz_core_components as wcc + + +class PlotOptions(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + COLOR_BY = "color-by" + SIZE_BY = "size-by" + SIM_ERROR_BAR = "sim-error-bar" + + def __init__(self) -> None: + super().__init__("Plot options") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Color by", + id=self.register_component_unique_id(self.Ids.COLOR_BY), + options=[ + { + "label": "none", + "value": None, + }, + { + "label": "region", + "value": "region", + }, + ], + value="region", + clearable=True, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Size by", + id=self.register_component_unique_id(self.Ids.SIZE_BY), + options=[ + { + "label": "none", + "value": None, + }, + { + "label": "sim_std", + "value": "sim_std", + }, + { + "label": "diff_mean", + "value": "diff_mean", + }, + { + "label": "diff_std", + "value": "diff_std", + }, + ], + value=None, + ), + wcc.Dropdown( + label="Sim errorbar", + id=self.register_component_unique_id(self.Ids.SIM_ERROR_BAR), + options=[ + { + "label": "None", + "value": None, + }, + { + "label": "Sim std", + "value": "sim_std", + }, + { + "label": "Sim p10/p90", + "value": "sim_p10_p90", + }, + ], + value="None", + ), + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_settings_and_layout.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_settings_and_layout.py new file mode 100644 index 000000000..ca284eca5 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_settings_and_layout.py @@ -0,0 +1,74 @@ +from typing import List + +import webviz_core_components as wcc +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC + + +class PlotSettingsAndLayout(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + LAYOUT_HEIGHT = "layout-height" + LAYOUT_COLUMNS = "layout-columns" + X_AXIS_SETTINGS = "x-axix-settings" + SUPERIMPOSE_PLOT = "superimpose-plot" + + def __init__(self) -> None: + super().__init__("Plot settings and layout") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Fig layout - height", + id=self.register_component_unique_id(self.Ids.LAYOUT_HEIGHT), + options=[ + { + "label": "Very small", + "value": 250, + }, + { + "label": "Small", + "value": 350, + }, + { + "label": "Medium", + "value": 450, + }, + { + "label": "Large", + "value": 700, + }, + { + "label": "Very large", + "value": 1000, + }, + ], + value=450, + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Fig layout - # columns", + id=self.register_component_unique_id(self.Ids.LAYOUT_COLUMNS), + options=[ + { + "label": "One column", + "value": 1, + }, + { + "label": "Two columns", + "value": 2, + }, + { + "label": "Three columns", + "value": 3, + }, + ], + style={"display": "block"}, + value=1, + clearable=False, + persistence=True, + persistence_type="memory", + ), + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py new file mode 100644 index 000000000..7c0a1fb31 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py @@ -0,0 +1,357 @@ +import glob +import logging +import re +from pathlib import Path +from typing import List, Optional + +import numpy as np +import pandas as pd +from webviz_config import WebvizPluginABC, WebvizSettings +from webviz_config.webviz_store import webvizstore + + +# ------------------------------- +@webvizstore +def makedf( + ensemble_set: dict, + attribute_name: str, + attribute_sim_path: str, + attribute_obs_path: str, + obs_mult: float, + sim_mult: float, + realrange: Optional[List[List[int]]], +) -> pd.DataFrame: + """Create dataframe of obs, meta and sim data for all ensembles. + Uses the functions 'makedf_seis_obs_meta' and 'makedf_seis_addsim'.""" + + meta_name = "meta--" + attribute_name + dfs = [] + dfs_obs = [] + ens_count = 0 + for ens_name, ens_path in ensemble_set.items(): + logging.info( + f"Working with ensemble name {ens_name}:\nRunpath: {ens_path}" + f"\nAttribute name: {attribute_name}" + ) + + # grab runpath for one realization and locate obs/meta data relative to it + single_runpath = sorted(glob.glob(ens_path))[0] + obsfile = Path(single_runpath) / Path(attribute_obs_path) / meta_name + + df = makedf_seis_obs_meta(obsfile, obs_mult=obs_mult) + + df["ENSEMBLE"] = ens_name # add ENSEMBLE column + dfs_obs.append(df.copy()) + + # --- add sim data --- + fromreal, toreal = 0, 999 + if realrange is not None: + if len(realrange[ens_count]) == 2: + fromreal = int(realrange[ens_count][0]) + toreal = int(realrange[ens_count][1]) + else: + raise RuntimeError( + "Error in " + + makedf.__name__ + + "\nrealrange input is assigned wrongly (in yaml config file).\n" + "Make sure to add 2 integers in square brackets " + "for each of the ensembles. Alternatively, remove this optional " + "argument to use default settings (all realizations included)." + ) + + df = makedf_seis_addsim( + df, + ens_path, + attribute_name, + attribute_sim_path, + fromreal=fromreal, + toreal=toreal, + sim_mult=sim_mult, + ) + dfs.append(df) + + ens_count += 1 + + return pd.concat(dfs) # , pd.concat(dfs_obs) + + +# ------------------------------- +def makedf_seis_obs_meta( + obsfile: Path, + obs_mult: float = 1.0, +) -> pd.DataFrame: + """Make a dataframe of obsdata and metadata. + (obs data multiplier: optional, default is 1.0") + """ + # --- read obsfile into pandas dataframe --- + df = pd.read_csv(obsfile) + dframe = df.copy() # make a copy to avoid strange pylint errors + # for info: https://github.com/PyCQA/pylint/issues/4577 + dframe.columns = dframe.columns.str.lower() # convert all headers to lower case + logging.debug( + f"Obs file: {obsfile} \n--> Number of seismic data points: {len(dframe)}" + ) + tot_nan_val = dframe.isnull().sum().sum() # count all nan values + if tot_nan_val > 0: + logging.warning(f"{obsfile} contains {tot_nan_val} NaN values") + + if "obs" not in dframe.columns: + raise RuntimeError(f"'obs' column not included in {obsfile}") + + if "obs_error" not in dframe.columns: + raise RuntimeError(f"'obs_error' column not included in {obsfile}") + + if "east" not in dframe.columns: + if "x_utme" in dframe.columns: + dframe.rename(columns={"x_utme": "east"}, inplace=True) + logging.debug("renamed x_utme column to east") + else: + raise RuntimeError("'x_utm' (or 'east') column not included in meta data") + + if "north" not in dframe.columns: + if "y_utmn" in dframe.columns: + dframe.rename(columns={"y_utmn": "north"}, inplace=True) + logging.debug("renamed y_utmn column to north") + else: + raise RuntimeError("'y_utm' (or 'north') column not included in meta data") + + if "region" not in dframe.columns: + if "regions" in dframe.columns: + dframe.rename(columns={"regions": "region"}, inplace=True) + logging.debug("renamed regions column to region") + else: + raise RuntimeError( + "'Region' column is not included in meta data" + "Please check your yaml config file settings and/or the metadata file." + ) + + # --- apply obs multiplier --- + dframe["obs"] = dframe["obs"] * obs_mult + dframe["obs_error"] = dframe["obs_error"] * obs_mult + + # redefine to int if region numbers in metafile is float + if dframe.region.dtype == "float64": + dframe = dframe.astype({"region": "int64"}) + + dframe["data_number"] = dframe.index + 1 # add a simple counter + + return dframe + + +def makedf_seis_addsim( + df: pd.DataFrame, + ens_path: str, + attribute_name: str, + attribute_sim_path: str, + fromreal: int = 0, + toreal: int = 99, + sim_mult: float = 1.0, +) -> pd.DataFrame: + """Make a merged dataframe of obsdata/metadata and simdata.""" + + data_found, no_data_found = [], [] + real_path = {} + obs_size = len(df.index) + + runpaths = glob.glob(ens_path) + if len(runpaths) == 0: + logging.warning(f"No realizations was found, wrong input?: {ens_path}") + return pd.DataFrame() + + for runpath in runpaths: + realno = int(re.search(r"(?<=realization-)\d+", runpath).group(0)) # type: ignore + real_path[realno] = runpath + + sim_df_list = [df] + for real in sorted(real_path.keys()): + if fromreal <= real <= toreal: + + simfile = ( + Path(real_path[real]) / Path(attribute_sim_path) / Path(attribute_name) + ) + if simfile.exists(): + # ---read sim data and apply sim multiplier --- + colname = "real-" + str(real) + sim_df = pd.read_csv(simfile, header=None, names=[colname]) * sim_mult + if len(sim_df.index) != obs_size: + raise RuntimeError( + f"---\nThe length of {simfile} is {len(sim_df.index)} which is " + f"different to the obs data which has {obs_size} data points. " + "These must be the same size.\n---" + ) + sim_df_list.append(sim_df) + data_found.append(real) + else: + no_data_found.append(real) + logging.debug(f"File does not exist: {str(simfile)}") + df_addsim = pd.concat(sim_df_list, axis=1) + + if len(data_found) == 0: + logging.warning( + f"{ens_path}/{attribute_sim_path}: no sim data found for {attribute_name}" + ) + else: + logging.debug(f"Sim values added to dataframe for realizations: {data_found}") + if len(no_data_found) == 0: + logging.debug("OK. Found data for all realizations") + else: + logging.debug(f"No data found for realizations: {no_data_found}") + + return df_addsim + + +def df_seis_ens_stat( + df: pd.DataFrame, ens_name: str, obs_error_weight: bool = False +) -> pd.DataFrame: + """Make a dataframe with ensemble statistics per datapoint across all realizations. + Calculate for both sim and diff values. Return with obs/meta data included. + Return empty dataframe if no realizations included in df.""" + + # --- make dataframe with obs and meta data only + column_names = df.columns.values.tolist() + x = [name for name in column_names if not name.startswith("real-")] + start, end = x[0], x[-1] + df_obs_meta = df.loc[:, start:end] + + # --- make dataframe with real- columns only + column_names = df.columns.values.tolist() + x = [name for name in column_names if name.startswith("real-")] + if len(x) > 0: + start, end = x[0], x[-1] + df_sim = df.loc[:, start:end] + else: + logging.info(f"{ens_name}: no data found for selected realizations.") + return pd.DataFrame() + + # --- calculate absolute diff, (|sim - obs| / obs_error), and store in new df + df_diff = pd.DataFrame() + for col in df.columns: + if col.startswith("real-"): + df_diff[col] = abs(df[col] - df["obs"]) + if obs_error_weight: + df_diff[col] = df_diff[col] / df["obs_error"] # divide by obs error + + # --- ensemble statistics of sim and diff for each data point ---- + # --- calculate statistics per row (data point) + sim_mean = df_sim.mean(axis=1) + sim_std = df_sim.std(axis=1) + sim_p90 = df_sim.quantile(q=0.1, axis=1) + sim_p10 = df_sim.quantile(q=0.9, axis=1) + sim_min = df_sim.min(axis=1) + sim_max = df_sim.max(axis=1) + diff_mean = df_diff.mean(axis=1) + diff_std = df_diff.std(axis=1) + + df_stat = pd.DataFrame( + data={ + "sim_mean": sim_mean, + "sim_std": sim_std, + "sim_p90": sim_p90, + "sim_p10": sim_p10, + "sim_min": sim_min, + "sim_max": sim_max, + "diff_mean": diff_mean, + "diff_std": diff_std, + } + ) + + # --- add obsdata and metadata to the dataframe + df_stat = pd.concat([df_stat, df_obs_meta], axis=1, sort=False) + + # Create coverage parameter + # • Values between 0 and 1 = coverage + # • Values above 1 = all sim values lower than obs values + # • Values below 0 = all sim values higher than obs values + + # (obs-min)/(max-min) + df_stat["sim_coverage"] = (df_stat.obs - df_stat.sim_min) / ( + df_stat.sim_max - df_stat.sim_min + ) + # obs_error adjusted: (obs-min)/(obs_error+max-min) + df_stat["sim_coverage_adj"] = (df_stat.obs - df_stat.sim_min) / ( + df_stat.obs_error + df_stat.sim_max - df_stat.sim_min + ) + # force to zero if diff smaller than obs_error, but keep values already in range(0,1) + # (this removes dilemma of small negative values showing up as overmodelled) + df_stat["sim_coverage_adj"] = np.where( + ( + ((df_stat.sim_coverage_adj > 0) & (df_stat.sim_coverage_adj < 1)) + | ( + (abs(df_stat.obs - df_stat.sim_min) > df_stat.obs_error) + & (abs(df_stat.obs - df_stat.sim_max) > df_stat.obs_error) + ) + ), + df_stat.sim_coverage_adj, + 0, + ) + + return df_stat + + +@webvizstore +def make_polygon_df(ensemble_set: dict, polygon: str) -> pd.DataFrame: + """Read polygon file. If there are one polygon file + per realization only one will be read (first found)""" + + df_polygon: pd.DataFrame = pd.DataFrame() + df_polygons: pd.DataFrame = pd.DataFrame() + for _, ens_path in ensemble_set.items(): + for single_runpath in sorted(glob.glob(ens_path)): + poly = Path(single_runpath) / Path(polygon) + if poly.is_dir(): # grab all csv files in folder + poly_files = glob.glob(str(poly) + "/*csv") + else: + poly_files = glob.glob(str(poly)) + + if not poly_files: + logging.debug(f"No polygon files found in '{poly}'") + else: + for poly_file in poly_files: + logging.debug(f"Read polygon file:\n {poly_file}") + df_polygon = pd.read_csv(poly_file) + cols = df_polygon.columns + + if ("ID" in cols) and ("X" in cols) and ("Y" in cols): + df_polygon = df_polygon[["X", "Y", "ID"]] + elif ( + ("POLY_ID" in cols) + and ("X_UTME" in cols) + and ("Y_UTMN" in cols) + ): + df_polygon = df_polygon[["X_UTME", "Y_UTMN", "POLY_ID"]].rename( + columns={"X_UTME": "X", "Y_UTMN": "Y", "POLY_ID": "ID"} + ) + logging.warning( + "For the future, consider using X,Y,Z,ID as header names in " + "the polygon files, as this is regarded as the FMU standard." + f"The {poly_file} file uses X_UTME,Y_UTMN,POLY_ID." + ) + else: + logging.warning( + f"The polygon file {poly_file} does not have an expected " + "format and is therefore skipped. The file must either " + "contain the columns 'POLY_ID', 'X_UTME' and 'Y_UTMN' or " + "the columns 'ID', 'X' and 'Y'." + ) + continue + + df_polygon["name"] = str(Path(poly_file).stem).replace("_", " ") + df_polygons = pd.concat([df_polygons, df_polygon]) + + logging.debug(f"Polygon dataframe:\n{df_polygons}") + return df_polygons + + if df_polygons.empty(): + raise RuntimeError( + "Error in " + + str(make_polygon_df.__name__) + + ". Could not find polygon files with a valid format." + f" Please update the polygon argument '{polygon}' in " + "the config file (default is None) or edit the files in the " + "list. The polygon files must contain the columns " + "'POLY_ID', 'X_UTME' and 'Y_UTMN' " + "(the column names must match exactly)." + ) + + logging.debug("Polygon file not assigned - continue without.") + return df_polygons diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py new file mode 100644 index 000000000..2456e8bcf --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py @@ -0,0 +1,1062 @@ +import logging +from typing import Any, List, Optional, Tuple, Union + +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +import webviz_core_components as wcc +from plotly.subplots import make_subplots + +from .._seismic_color_scales import ColorScales +from ._dataframe_functions import df_seis_ens_stat +from ._support_functions import average_arrow_annotation + + +# ------------------------------- +def update_misfit_plot( + df: pd.DataFrame, + sorting: str, + figheight: int = 450, + misfit_weight: Optional[str] = None, + misfit_exponent: float = 1.0, + normalize: bool = False, +) -> List[wcc.Graph]: + """Create plot of misfit per realization. One plot per ensemble. + Misfit is absolute value of |sim - obs|, weighted by obs_error""" + + # max_diff = find_max_diff(df) + max_diff = None + figures = [] + + for ens_name, ensdf in df.groupby("ENSEMBLE"): + logging.debug(f"Seismic misfit plot, updating {ens_name}") + + # --- drop columns (realizations) with no data + ensdf = ensdf.dropna(axis="columns") + + # --- calculate absolute diff, (|sim - obs| / obs_error), and store in new df + ensdf_diff = pd.DataFrame() + for col in ensdf.columns: + if col.startswith("real-"): + ensdf_diff[col] = abs(ensdf[col] - ensdf["obs"]) + if misfit_weight == "obs_error": + ensdf_diff[col] = ensdf_diff[col] / ensdf["obs_error"] + ensdf_diff[col] = ensdf_diff[col] ** misfit_exponent + + # --- make sum of abs diff values over each column (realization) + ensdf_diff_sum = ensdf_diff.abs().sum().reset_index() + ensdf_diff_sum = ensdf_diff_sum.rename(columns={"index": "REAL", 0: "ABSDIFF"}) + ensdf_diff_sum["ENSEMBLE"] = ens_name + + if normalize: + ensdf_diff_sum["ABSDIFF"] = ( + ensdf_diff_sum["ABSDIFF"] / len(ensdf_diff) + ) ** (1 / misfit_exponent) + + # --- remove "real-" from REAL column values + # --- (only keep real number for nicer xaxis label) + ensdf_diff_sum = ensdf_diff_sum.replace( + to_replace=r"^real-", value="", regex=True + ) + + # --- calculate max from first ensemble, use with color range --- + if max_diff is None: + max_diff = ensdf_diff_sum["ABSDIFF"].max() + + mean_diff = ensdf_diff_sum["ABSDIFF"].mean() + + # --- sorting ---- + if sorting is not None: + ensdf_diff_sum = ensdf_diff_sum.sort_values( + by=["ABSDIFF"], ascending=sorting + ) + + fig = px.bar( + ensdf_diff_sum, + x="REAL", + y="ABSDIFF", + title=ens_name, + range_y=[0, max_diff * 1.05], + color="ABSDIFF", + range_color=[0, max_diff], + color_continuous_scale=px.colors.sequential.amp, + hover_data={"ABSDIFF": ":,.3r"}, + ) + fig.update_xaxes(showticklabels=False) + fig.update_xaxes(title_text="Realization (hover to see values)") + fig.update_yaxes(title_text="Cumulative misfit") + fig.add_hline(mean_diff) + fig.add_annotation(average_arrow_annotation(mean_diff, "y")) + fig.update_layout(margin=dict(l=20, r=20, t=30, b=20)) + fig.update_layout(coloraxis_colorbar_thickness=20) + # fig.update(layout_coloraxis_showscale=False) + + figures.append(wcc.Graph(figure=fig, style={"height": figheight})) + + return figures + + +# ------------------------------- +def update_obsdata_raw( + df_obs: pd.DataFrame, + colorby: Optional[str] = None, + showerror: bool = False, + showhistogram: bool = False, + reset_index: bool = False, +) -> px.scatter: + """Plot seismic obsdata; raw plot. + Takes dataframe with obsdata and metadata as input""" + + if colorby not in df_obs.columns and colorby is not None: + colorby = None + logging.warning(f"{colorby} is not included, colorby is reset to None") + + df_obs = df_obs.sort_values(by=["region"]) + df_obs = df_obs.astype({colorby: "string"}) + # df_obs = df_obs.astype({colorby: int}) + + # ---------------------------------------- + # fig: raw data plot + # ---------------------------------------- + + if reset_index: + df_obs.reset_index(inplace=True) + df_obs["data_point"] = df_obs.index + 1 + else: + df_obs["data_point"] = df_obs.data_number + + marg_y = None + if showhistogram: + marg_y = "histogram" + + err_y = None + if showerror: + err_y = "obs_error" + + fig_raw = px.scatter( + df_obs, + x="data_point", + y="obs", + color=colorby, + marginal_y=marg_y, + error_y=err_y, + hover_data={ + "region": True, + "data_point": False, + "obs": ":.2r", + "obs_error": ":.2r", + "east": ":,.0f", + "north": ":,.0f", + "data_number": True, + }, + title="obs data raw plot | colorby: " + str(colorby), + ) + if reset_index: + fig_raw.update_xaxes(title_text="data point (sorted by region)") + else: + fig_raw.update_xaxes(title_text="data point (original ordering)") + if showerror: + fig_raw.update_yaxes(title_text="observation value w/error") + else: + fig_raw.update_yaxes(title_text="observation value") + + fig_raw.update_yaxes(uirevision="true") # don't update y-range during callbacks + return fig_raw + + +# ------------------------------- +def update_obsdata_map( + df_obs: pd.DataFrame, + colorby: str, + df_polygon: pd.DataFrame, + obs_range: List[float], + obs_err_range: List[float], + scale_col_range: float = 0.6, + marker_size: int = 10, +) -> Optional[px.scatter]: + """Plot seismic obsdata; map view plot. + Takes dataframe with obsdata and metadata as input""" + + if ("east" not in df_obs.columns) or ("north" not in df_obs.columns): + logging.warning("-- Do not have necessary data for making map view plot") + logging.warning("-- Consider adding east/north coordinates to metafile") + return None + + if df_obs[colorby].dtype == "int64" or colorby == "region": + df_obs = df_obs.sort_values(by=[colorby]) + df_obs = df_obs.astype( + {colorby: "string"} + ) # define as string to colorby discrete variable + # ---------------------------------------- + color_scale = None + scale_midpoint = None + range_col = None + + if colorby == "obs": + range_col, scale_midpoint, color_scale = _get_obsdata_col_settings( + colorby, obs_range, scale_col_range + ) + if colorby == "obs_error": + range_col, scale_midpoint, color_scale = _get_obsdata_col_settings( + colorby, obs_err_range, scale_col_range + ) + + # ---------------------------------------- + fig = px.scatter( # map view plot + df_obs, + x="east", + y="north", + color=colorby, + hover_data={ + "east": False, + "north": False, + "region": True, + "obs": ":.2r", + "obs_error": ":.2r", + "data_number": True, + }, + color_continuous_scale=color_scale, + color_continuous_midpoint=scale_midpoint, + range_color=range_col, + title="obs data map view plot | colorby: " + str(colorby), + ) + + # ---------------------------------------- + # add polygon to map if defined + if not df_polygon.empty: + for poly_id, polydf in df_polygon.groupby("ID"): + fig.append_trace( + go.Scattergl( + x=polydf["X"], + y=polydf["Y"], + mode="lines", + line_color="RoyalBlue", + name=f"pol{poly_id}", + showlegend=False, + hoverinfo="name", + ), + row="all", + col="all", + # exclude_empty_subplots=True, + ) + + fig.update_yaxes(scaleanchor="x") + fig.update_layout(coloraxis_colorbar_x=0.95) + fig.update_layout(coloraxis_colorbar_y=1.0) + fig.update_layout(coloraxis_colorbar_yanchor="top") + fig.update_layout(coloraxis_colorbar_len=0.9) + fig.update_layout(coloraxis_colorbar_thickness=20) + fig.update_traces(marker=dict(size=marker_size), selector=dict(mode="markers")) + + fig.update_layout(uirevision="true") # don't update layout during callbacks + + return fig + + +# ------------------------------- +def update_obs_sim_map_plot( + df: pd.DataFrame, + ens_name: str, + df_polygon: pd.DataFrame, + obs_range: List[float], + scale_col_range: float = 0.8, + slice_accuracy: Union[int, float] = 100, + slice_position: float = 0.0, + plot_coverage: int = 0, + marker_size: int = 10, + slice_type: str = "stat", +) -> Tuple[Optional[Any], Optional[Any]]: + """Plot seismic obsdata, simdata and diffdata; side by side map view plots. + Takes dataframe with obsdata, metadata and simdata as input""" + + logging.debug(f"Seismic obs vs sim map plot, updating {ens_name}") + + ensdf = df[df.ENSEMBLE.eq(ens_name)] + + if ("east" not in ensdf.columns) or ("north" not in ensdf.columns): + logging.warning("-- Do not have necessary data for making map view plot") + logging.warning("-- Consider adding east/north coordinates to metafile") + return None, None + + # --- drop columns (realizations) with no data + ensdf = ensdf.dropna(axis="columns") + + # --- get dataframe with statistics per datapoint + ensdf_stat = df_seis_ens_stat(ensdf, ens_name) + + if ensdf_stat.empty: + return ( + make_subplots( + rows=1, + cols=3, + subplot_titles=("No data for current selection", "---", "---"), + ), + go.Figure(), + ) + + # ---------------------------------------- + # set obs/sim color scale and ranges + range_col, _, color_scale = _get_obsdata_col_settings( + "obs", obs_range, scale_col_range + ) + + # ---------------------------------------- + if plot_coverage == 0: + title3 = "Abs diff (mean)" + elif plot_coverage in [1, 2]: + title3 = "Coverage plot" + else: + title3 = "Region plot" + + fig = make_subplots( + rows=1, + cols=3, + subplot_titles=("Observed", "Simulated (mean)", title3), + shared_xaxes=True, + vertical_spacing=0.02, + shared_yaxes=True, + horizontal_spacing=0.02, + ) + + fig.add_trace( + go.Scattergl( + x=ensdf_stat["east"], + y=ensdf_stat["north"], + mode="markers", + marker=dict( + size=marker_size, + color=ensdf["obs"], + colorscale=color_scale, + colorbar_x=0.29, + colorbar_thicknessmode="fraction", + colorbar_thickness=0.02, + colorbar_len=0.9, + cmin=range_col[0], + cmax=range_col[1], + showscale=True, + ), + showlegend=False, + text=ensdf.obs, + customdata=list(zip(ensdf.region, ensdf.east)), + hovertemplate=( + "Obs: %{text:.2r}
Region: %{customdata[0]}
" + "East: %{customdata[1]:,.0f}" + ), + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Scattergl( + x=ensdf_stat["east"], + y=ensdf_stat["north"], + mode="markers", + marker=dict( + size=marker_size, + color=ensdf_stat["sim_mean"], + colorscale=color_scale, + colorbar_x=0.63, + colorbar_thicknessmode="fraction", + colorbar_thickness=0.02, + colorbar_len=0.9, + cmin=range_col[0], + cmax=range_col[1], + showscale=True, + ), + showlegend=False, + text=ensdf_stat.sim_mean, + customdata=list(zip(ensdf.region, ensdf.east)), + hovertemplate=( + "Sim (mean): %{text:.2r}
Region: %{customdata[0]}
" + "East: %{customdata[1]:,.0f}" + ), + ), + row=1, + col=2, + ) + + if plot_coverage == 0: # abs diff plot + fig.add_trace( + go.Scattergl( + x=ensdf_stat["east"], + y=ensdf_stat["north"], + mode="markers", + marker=dict( + size=marker_size, + color=ensdf_stat["diff_mean"], + cmin=0, + cmax=obs_range[1] * scale_col_range, + colorscale=ColorScales.SEISMIC_DIFF, + colorbar_x=0.97, + colorbar_thicknessmode="fraction", + colorbar_thickness=0.02, + colorbar_len=0.9, + showscale=True, + ), + showlegend=False, + text=ensdf_stat.diff_mean, + customdata=list(zip(ensdf.region, ensdf.east)), + hovertemplate=( + "Abs diff (mean): %{text:.2r}
Region: %{customdata[0]}
" + "East: %{customdata[1]:,.0f}" + ), + ), + row=1, + col=3, + ) + elif plot_coverage in [1, 2]: + coverage = "sim_coverage" if plot_coverage == 1 else "sim_coverage_adj" + fig.add_trace( + go.Scattergl( + x=ensdf_stat["east"], + y=ensdf_stat["north"], + mode="markers", + marker=dict( + size=marker_size, + color=ensdf_stat[coverage], + cmin=-1.0, + cmax=2.0, + colorscale=ColorScales.SEISMIC_COVERAGE, + colorbar=dict( + # title="Coverage", + tickvals=[-0.5, 0.5, 1.5], + ticktext=["Overmodelled", "Coverage", "Undermodelled"], + ), + colorbar_x=0.97, + colorbar_thicknessmode="fraction", + colorbar_thickness=0.02, + colorbar_len=0.9, + showscale=True, + ), + opacity=0.5, + showlegend=False, + text=ensdf_stat[coverage], + customdata=list(zip(ensdf.region, ensdf.east)), + hovertemplate=( + "Coverage value: %{text:.2r}
Region: %{customdata[0]}
" + "East: %{customdata[1]:,.0f}" + ), + ), + row=1, + col=3, + ) + else: # region plot + fig.add_trace( + go.Scattergl( + x=ensdf["east"], + y=ensdf["north"], + mode="markers", + marker=dict( + size=marker_size, + color=ensdf.region, + colorscale=px.colors.qualitative.Plotly, + colorbar_x=0.97, + colorbar_thicknessmode="fraction", + colorbar_thickness=0.02, + colorbar_len=0.9, + showscale=False, + ), + opacity=0.8, + showlegend=False, + hovertemplate="Region: %{text}", + text=ensdf.region, + ), + row=1, + col=3, + ) + + # ---------------------------------------- + # add horizontal line at slice position + fig.add_hline( + y=slice_position, + line_dash="dot", + line_color="green", + row="all", + col="all", + annotation_text="slice", + annotation_position="bottom left", + ) + + # ---------------------------------------- + # add polygon to map if defined + if not df_polygon.empty: + for poly_id, polydf in df_polygon.groupby("ID"): + fig.append_trace( + go.Scattergl( + x=polydf["X"], + y=polydf["Y"], + mode="lines", + line_color="RoyalBlue", + name=f"pol{poly_id}", + showlegend=False, + hoverinfo="name", + ), + row="all", + col="all", + # exclude_empty_subplots=True, + ) + + fig.update_yaxes(scaleanchor="x") + fig.update_xaxes(scaleanchor="x") + fig.update_xaxes(matches="x") # this solved issue with misaligned zoom/pan + + fig.update_layout(uirevision="true") # don't update layout during callbacks + + fig.update_layout(hovermode="closest") + # fig.update_layout(template="plotly_dark") + + # ---------------------------------------- + if slice_type == "stat": + # Create lineplot along slice - statistics + + df_sliced_stat = ensdf_stat[ + (ensdf_stat.north < slice_position + slice_accuracy) + & (ensdf_stat.north > slice_position - slice_accuracy) + ] + df_sliced_stat = df_sliced_stat.sort_values(by="east", ascending=True) + + fig_slice_stat = go.Figure( + [ + go.Scatter( + name="Obsdata", + x=df_sliced_stat["east"], + y=df_sliced_stat["obs"], + mode="markers+lines", + marker=dict(color="red", size=5), + line=dict(width=2, dash="solid"), + showlegend=True, + ), + go.Scatter( + name="Sim mean", + x=df_sliced_stat["east"], + y=df_sliced_stat["sim_mean"], + mode="markers+lines", + marker=dict(color="green", size=3), + line=dict(width=1, dash="dot"), + showlegend=True, + ), + go.Scatter( + name="Sim p10", + x=df_sliced_stat["east"], + y=df_sliced_stat["sim_p10"], + mode="lines", + marker=dict(color="#444"), + line=dict(width=1), + showlegend=True, + ), + go.Scatter( + name="Sim p90", + x=df_sliced_stat["east"], + y=df_sliced_stat["sim_p90"], + marker=dict(color="#444"), + line=dict(width=1), + mode="lines", + fillcolor="rgba(68, 68, 68, 0.3)", + fill="tonexty", + showlegend=True, + ), + go.Scatter( + name="Sim min", + x=df_sliced_stat["east"], + y=df_sliced_stat["sim_min"], + mode="lines", + line=dict(width=1, dash="dot", color="grey"), + showlegend=True, + ), + go.Scatter( + name="Sim max", + x=df_sliced_stat["east"], + y=df_sliced_stat["sim_max"], + mode="lines", + line=dict(width=1, dash="dot", color="grey"), + showlegend=True, + ), + ] + ) + fig_slice_stat.update_layout( + yaxis_title="Attribute value", + xaxis_title="East", + title="Attribute values along slice", + hovermode="x", + ) + fig_slice_stat.update_yaxes( + uirevision="true" + ) # don't update y-range during callbacks + + return fig, fig_slice_stat + + # ---------------------------------------- + if slice_type == "reals": + # Create lineplot along slice - individual realizations + + df_sliced_reals = ensdf[ + (ensdf.north < slice_position + slice_accuracy) + & (ensdf.north > slice_position - slice_accuracy) + ] + df_sliced_reals = df_sliced_reals.sort_values(by="east", ascending=True) + + fig_slice_reals = go.Figure( + [ + go.Scatter( + name="Obsdata", + x=df_sliced_reals["east"], + y=df_sliced_reals["obs"], + mode="markers+lines", + marker=dict(color="red", size=7), + line=dict(width=5, dash="solid"), + showlegend=True, + ), + ], + ) + + for col in df_sliced_reals.columns: + if col.startswith("real-"): + fig_slice_reals.add_trace( + go.Scattergl( + x=df_sliced_reals["east"], + y=df_sliced_reals[col], + mode="lines", # "markers+lines", + line_shape="linear", + line=dict(width=1, dash="dash"), + name=col, + showlegend=True, + hoverinfo="name", + ) + ) + + fig_slice_reals.update_layout( + yaxis_title="Attribute value", + xaxis_title="East", + title="Attribute values along slice", + hovermode="closest", + clickmode="event+select", + ) + fig_slice_reals.update_yaxes( + uirevision="true" + ) # don't update user selected y-ranges during callbacks + + return fig, fig_slice_reals + + return fig, None + + +# ------------------------------- +def update_crossplot( + df: pd.DataFrame, + colorby: Optional[str] = None, + sizeby: Optional[str] = None, + showerrorbar: Optional[str] = None, + fig_columns: int = 1, + figheight: int = 450, +) -> Optional[List[wcc.Graph]]: + """Create crossplot of ensemble average sim versus obs, + one value per seismic datapoint.""" + + dfs, figures = [], [] + for ens_name, ensdf in df.groupby("ENSEMBLE"): + logging.debug(f"Seismic crossplot; updating {ens_name}") + + # --- drop columns (realizations) with no data + ensdf = ensdf.dropna(axis="columns") + + # --- make dataframe with statistics per datapoint + ensdf_stat = df_seis_ens_stat(ensdf, ens_name) + if ensdf_stat.empty: + break + + # del ensdf + + if ( + sizeby in ("sim_std", "diff_std") + and ensdf_stat["sim_std"].isnull().values.any() + ): + logging.info("Chosen sizeby is ignored for current selections (std = nan).") + sizeby = None + + errory = None + errory_minus = None + if showerrorbar == "sim_std": + errory = "sim_std" + elif showerrorbar == "sim_p10_p90": + ensdf_stat["error_plus"] = abs( + ensdf_stat["sim_mean"] - ensdf_stat["sim_p10"] + ) + ensdf_stat["error_minus"] = abs( + ensdf_stat["sim_mean"] - ensdf_stat["sim_p90"] + ) + errory = "error_plus" + errory_minus = "error_minus" + + # ------------------------------------------------------------- + if colorby == "region": + ensdf_stat = ensdf_stat.sort_values(by=[colorby]) + ensdf_stat = ensdf_stat.astype({"region": "string"}) + + dfs.append(ensdf_stat) + # ------------------------------------------------------------- + if len(dfs) == 0: + return None + + df_stat = pd.concat(dfs) + + no_plots = len(df_stat.ENSEMBLE.unique()) + if no_plots <= fig_columns: + total_height = figheight * (1 + 45 / figheight) + else: + total_height = figheight * round(no_plots / fig_columns) + + fig = px.scatter( + df_stat, + facet_col="ENSEMBLE", + facet_col_wrap=fig_columns, + x="obs", + y="sim_mean", + error_y=errory, + error_y_minus=errory_minus, + color=colorby, + size=sizeby, + size_max=20, + # hover_data=list(df_stat.columns), + hover_data={ + "region": True, + "ENSEMBLE": False, + "obs": ":.2r", + # "obs_error": ":.2r", + "sim_mean": ":.2r", + # "sim_std": ":.2r", + "diff_mean": ":.2r", + # "east": ":,.0f", + # "north": ":,.0f", + "data_number": True, + }, + ) + fig.update_traces(marker=dict(sizemode="area"), error_y_thickness=1.0) + fig.update_layout(uirevision="true") # don't update layout during callbacks + + # add zero/diagonal line + min_obs = df.obs.min() + max_obs = df.obs.max() + fig.add_trace( + go.Scattergl( + x=[min_obs, max_obs], # xplot_range, + y=[min_obs, max_obs], # yplot_range, + mode="lines", + line_color="gray", + name="zeroline", + showlegend=False, + ), + row="all", + col="all", + exclude_empty_subplots=True, + ) + + # set marker line color = black (default is white) + if sizeby is None: + fig.update_traces( + marker=dict(line=dict(width=0.4, color="black")), + selector=dict(mode="markers"), + ) + + figures.append(wcc.Graph(figure=fig.to_dict(), style={"height": total_height})) + return figures + + +# ------------------------------- +# pylint: disable=too-many-statements +def update_errorbarplot( + df: pd.DataFrame, + colorby: Optional[str] = None, + showerrorbar: Optional[str] = None, + showerrorbarobs: Optional[str] = None, + reset_index: bool = False, + fig_columns: int = 1, + figheight: int = 450, +) -> Optional[List[wcc.Graph]]: + """Create errorbar plot of ensemble sim versus obs, + one value per seismic datapoint.""" + + first = True + figures = [] + dfs = [] + + for ens_name, ensdf in df.groupby("ENSEMBLE"): + logging.debug(f"Seismic errorbar plot; updating {ens_name}") + + # --- drop columns (realizations) with no data + ensdf = ensdf.dropna(axis="columns") + + # --- make dataframe with statistics per datapoint + ensdf_stat = df_seis_ens_stat(ensdf, ens_name) + if ensdf_stat.empty: + break + + del ensdf + + errory = None + errory_minus = None + if showerrorbar == "sim_std": + errory = "sim_std" + elif showerrorbar == "sim_p10_p90": + ensdf_stat["error_plus"] = abs( + ensdf_stat["sim_mean"] - ensdf_stat["sim_p10"] + ) + ensdf_stat["error_minus"] = abs( + ensdf_stat["sim_mean"] - ensdf_stat["sim_p90"] + ) + errory = "error_plus" + errory_minus = "error_minus" + + # ------------------------------------------------------------- + ensdf_stat = ensdf_stat.sort_values(by=["region"]) + ensdf_stat = ensdf_stat.astype({"region": "string"}) + + if reset_index: + ensdf_stat.reset_index(inplace=True) + + ensdf_stat["counter"] = ( + ensdf_stat.index + 1 + ) # make new counter after reset index + + # ------------------------------------------------------------- + # get color ranges from first case + if first: + cmin = None + cmax = None + if isinstance(colorby, float): + cmin = ensdf_stat[colorby].min() + cmax = ensdf_stat[colorby].quantile(0.8) + first = False + + dfs.append(ensdf_stat) + # ------------------------------------------------------------- + if len(dfs) == 0: + return None + + df_stat = pd.concat(dfs) + + no_plots = len(df_stat.ENSEMBLE.unique()) + if no_plots <= fig_columns: + total_height = figheight * (1 + 45 / figheight) + else: + total_height = figheight * round(no_plots / fig_columns) + + fig = px.scatter( + df_stat, + facet_col="ENSEMBLE", + facet_col_wrap=fig_columns, + x="counter", + y="sim_mean", + error_y=errory, + error_y_minus=errory_minus, + color=colorby, + range_color=[cmin, cmax], + # hover_data=list(df_stat.columns), + hover_data={ + "region": True, + "ENSEMBLE": False, + "counter": False, + "obs": ":.2r", + # "obs_error": ":.2r", + "sim_mean": ":.2r", + # "sim_std": ":.2r", + "diff_mean": ":.2r", + # "east": ":,.0f", + # "north": ":,.0f", + "data_number": True, + }, + ) + fig.update_traces(error_y_thickness=1.0, selector=dict(type="scatter")) + + # ----------------------- + obserrory = ( + dict(type="data", array=df_stat["obs_error"], visible=True, thickness=1.0) + if showerrorbarobs is not None + else None + ) + obslegend = colorby == "region" + + fig.add_trace( + go.Scattergl( + x=df_stat["counter"], + y=df_stat["obs"], + error_y=obserrory, + mode="markers", + line_color="gray", + name="obs", + showlegend=obslegend, + opacity=0.5, + ), + row="all", + col="all", + exclude_empty_subplots=True, + ) + fig.update_layout(hovermode="closest") + + if reset_index: + fig.update_xaxes(title_text="data point (index reset, sorted by region)") + else: + fig.update_xaxes(title_text="data point (original numbering)") + if showerrorbar: + fig.update_yaxes(title_text="Simulated mean w/error") + else: + fig.update_yaxes(title_text="Simulated mean") + + fig.update_yaxes(uirevision="true") # don't update y-range during callbacks + figures.append(wcc.Graph(figure=fig.to_dict(), style={"height": total_height})) + return figures + + +# ------------------------------- +def update_errorbarplot_superimpose( + df: pd.DataFrame, + showerrorbar: Optional[str] = None, + showerrorbarobs: Optional[str] = None, + reset_index: bool = True, + figheight: int = 450, +) -> Optional[List[wcc.Graph]]: + """Create errorbar plot of ensemble sim versus obs, + one value per seismic datapoint.""" + + first = True + figures = [] + ensdf_stat = {} + data_to_plot = False + + for ens_name, ensdf in df.groupby("ENSEMBLE"): + logging.debug(f"Seismic errorbar plot; updating {ens_name}") + + # --- drop columns (realizations) with no data + ensdf = ensdf.dropna(axis="columns") + + # --- make dataframe with statistics per datapoint + ensdf_stat[ens_name] = df_seis_ens_stat(ensdf, ens_name) + if not ensdf_stat[ens_name].empty: + data_to_plot = True + else: + break + + del ensdf + + # ------------------------------------------------------------- + errory = None + + if showerrorbar == "sim_std": + errory = dict( + type="data", + array=ensdf_stat[ens_name]["sim_std"], + visible=True, + thickness=1.0, + ) + elif showerrorbar == "sim_p10_p90": + ensdf_stat[ens_name]["error_plus"] = abs( + ensdf_stat[ens_name]["sim_mean"] - ensdf_stat[ens_name]["sim_p10"] + ) + ensdf_stat[ens_name]["error_minus"] = abs( + ensdf_stat[ens_name]["sim_mean"] - ensdf_stat[ens_name]["sim_p90"] + ) + errory = dict( + type="data", + symmetric=False, + array=ensdf_stat[ens_name]["error_plus"], + arrayminus=ensdf_stat[ens_name]["error_minus"], + visible=True, + thickness=1.0, + ) + + # ------------------------------------------------------------- + ensdf_stat[ens_name] = ensdf_stat[ens_name].sort_values(by=["region"]) + ensdf_stat[ens_name] = ensdf_stat[ens_name].astype({"region": "string"}) + + if reset_index: + ensdf_stat[ens_name].reset_index(inplace=True) + + ensdf_stat[ens_name]["counter"] = ( + ensdf_stat[ens_name].index + 1 + ) # make new counter after index reset + + # ----------------------- + if first: + + fig = px.scatter() + + obserrory = None + if showerrorbarobs is not None: + obserrory = dict( + type="data", + array=ensdf_stat[ens_name]["obs_error"], + visible=True, + thickness=1.0, + ) + + fig.add_scattergl( + x=ensdf_stat[ens_name]["counter"], + y=ensdf_stat[ens_name]["obs"], + error_y=obserrory, + mode="markers", + line_color="gray", + name="obs", + showlegend=True, + ) + fig.add_scattergl( + x=ensdf_stat[ens_name]["counter"], + y=ensdf_stat[ens_name]["sim_mean"], + mode="markers", + name=ens_name, + error_y=errory, + ) + first = False + # ----------------------- + else: + fig.add_scattergl( + x=ensdf_stat[ens_name]["counter"], + y=ensdf_stat[ens_name]["sim_mean"], + mode="markers", + name=ens_name, + error_y=errory, + ) + + if not data_to_plot: + return None + + fig.update_layout(hovermode="x") + + if reset_index: + fig.update_xaxes(title_text="data point (index reset, sorted by region)") + else: + fig.update_xaxes(title_text="data point (original numbering)") + if showerrorbar: + fig.update_yaxes(title_text="Simulated mean w/error") + else: + fig.update_yaxes(title_text="Simulated mean") + + fig.update_yaxes(uirevision="true") # don't update y-range during callbacks + figures.append(wcc.Graph(figure=fig.to_dict(), style={"height": figheight})) + return figures + + +def _get_obsdata_col_settings( + colorby: str, + obs_range: List[float], + scale_col: float, +) -> Tuple[List[float], Union[None, float], Any]: + """return color scale range for obs or obs_error. + Make obs range symetric and obs_error range positive. + Adjust range with scale_col value.""" + + if colorby == "obs_error": + lower = obs_range[0] + upper = max(obs_range[1] * scale_col, lower * 1.01) + range_col = [lower, upper] + scale_midpoint = None + color_scale = ColorScales.SEISMIC_ERROR + + if colorby == "obs": + abs_max = max(abs(obs_range[0]), abs(obs_range[1])) + upper = abs_max * scale_col + lower = -1 * upper + range_col = [lower, upper] + scale_midpoint = 0.0 + color_scale = ColorScales.SEISMIC_SYMMETRIC + + return range_col, scale_midpoint, color_scale diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_support_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_support_functions.py new file mode 100644 index 000000000..f314a8f0e --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_support_functions.py @@ -0,0 +1,116 @@ +import math +from typing import Any, Dict, List + +import numpy as np +import pandas as pd + + +def _compare_dfs_obs(dframeobs: pd.DataFrame, ensembles: List) -> str: + """Compare obs and obs_error values for ensembles. + Return info text if not equal""" + + text = "" + if len(ensembles) > 1: + ens1 = ensembles[0] + obs1 = dframeobs[dframeobs.ENSEMBLE.eq(ens1)].obs + obserr1 = dframeobs[dframeobs.ENSEMBLE.eq(ens1)].obs_error + for idx in range(1, len(ensembles)): + ens = ensembles[idx] + obs = dframeobs[dframeobs.ENSEMBLE.eq(ens)].obs + obserr = dframeobs[dframeobs.ENSEMBLE.eq(ens)].obs_error + + if not obs1.equals(obs): + text = ( + text + "\n--WARNING-- " + ens + " obs data is different to " + ens1 + ) + else: + text = text + "\n" + "✅ " + ens + " obs data is equal to " + ens1 + + if not obserr1.equals(obserr): + text = ( + text + + "\n--WARNING-- " + + ens + + " obs error data is different to " + + ens1 + ) + else: + text = text + "\n" + "✅ " + ens + " obs error data is equal to " + ens1 + + return text + + +def get_unique_column_values(df: pd.DataFrame, colname: str) -> List: + """return dataframe column values. If no matching colname, return [999]. + Currently unused. Consider removing""" + if colname in df: + values = df[colname].unique() + values = sorted(values) + else: + values = [999] + return values + + +def find_max_diff(df: pd.DataFrame) -> np.float64: + max_diff = np.float64(0) + for _ens, ensdf in df.groupby("ENSEMBLE"): + realdf = ensdf.groupby("REAL").sum().reset_index() + max_diff = ( + max_diff if max_diff > realdf["ABSDIFF"].max() else realdf["ABSDIFF"].max() + ) + return max_diff + + +def average_line_shape(mean_value: np.float64, yref: str = "y") -> Dict[str, Any]: + return { + "type": "line", + "yref": yref, + "y0": mean_value, + "y1": mean_value, + "xref": "paper", + "x0": 0, + "x1": 1, + } + + +def average_arrow_annotation(mean_value: np.float64, yref: str = "y") -> Dict[str, Any]: + decimals = 1 + if mean_value < 0.001: + decimals = 5 + elif mean_value < 0.01: + decimals = 4 + elif mean_value < 0.1: + decimals = 3 + elif mean_value < 10: + decimals = 2 + return { + "x": 0.2, + "y": mean_value, + "xref": "paper", + "yref": yref, + "text": f"Average: {mean_value:.{decimals}f}", + "showarrow": True, + "align": "center", + "arrowhead": 2, + "arrowsize": 1, + "arrowwidth": 1, + "arrowcolor": "#636363", + "ax": 20, + "ay": -25, + } + + +def _map_initial_marker_size(total_data_points: int, no_ens: int) -> int: + """Calculate marker size based on number of datapoints per ensemble""" + if total_data_points < 1: + raise ValueError( + "No data points found. Something is wrong with your input data." + f"Value of total_data_points is {total_data_points}" + ) + data_points_per_ens = int(total_data_points / no_ens) + marker_size = int(550 / math.sqrt(data_points_per_ens)) + if marker_size > 30: + marker_size = 30 + elif marker_size < 2: + marker_size = 2 + return marker_size diff --git a/webviz_subsurface/plugins/_seismic_misfit/_view_elements/__init__.py b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/__init__.py new file mode 100644 index 000000000..4884077d0 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/__init__.py @@ -0,0 +1,2 @@ +from ._info_box import InfoBox +from ._slider import SeismicSlider diff --git a/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_info_box.py b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_info_box.py new file mode 100644 index 000000000..10e4dea7e --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_info_box.py @@ -0,0 +1,24 @@ +import webviz_core_components as wcc +from dash import dcc +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class InfoBox(ViewElementABC): + def __init__(self, label: str, caseinfo: str) -> None: + super().__init__() + self.label = label + self.caseinfo = caseinfo + + def inner_layout(self) -> Component: + return wcc.Selectors( + label=self.label, + children=[ + dcc.Textarea( + value=self.caseinfo, + style={ + "width": 500, + }, + ), + ], + ) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py new file mode 100644 index 000000000..cf6b30d89 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.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.webviz_plugin_subclasses import ViewElementABC + + +class SeismicSlider(ViewElementABC): + class Ids: + SLIDER = "slider" + + def __init__(self, map_y_range: List[float]) -> None: + super().__init__() + self.map_y_range = map_y_range + + def inner_layout(self) -> Component: + return wcc.Slider( + id=self.register_component_unique_id(self.Ids.SLIDER), + min=self.map_y_range[0], + max=self.map_y_range[1], + value=(self.map_y_range[0] + self.map_y_range[1]) / 2, + step=100, + marks={ + str(self.map_y_range[0]): f"min={round(self.map_y_range[0]):,}", + str(self.map_y_range[1]): f"max={round(self.map_y_range[1]):,}", + }, + ) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/__init__.py b/webviz_subsurface/plugins/_seismic_misfit/_views/__init__.py new file mode 100644 index 000000000..e8a246f14 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/__init__.py @@ -0,0 +1,5 @@ +from ._crossplot import Crossplot +from ._errorbar_plot import ErrorbarPlots +from ._map_plot import MapPlot +from ._misfit_per_real import MisfitPerReal +from ._obs_data import ObsData diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py new file mode 100644 index 000000000..7dcf6e66b --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -0,0 +1,176 @@ +from typing import Dict, List, Optional, Union + +import webviz_core_components as wcc +from dash import Input, Output, callback, dcc +from dash.exceptions import PreventUpdate +from webviz_config.webviz_plugin_subclasses import ViewABC + +from .._shared_settings import ( + CaseSettings, + FilterSettings, + PlotOptions, + PlotSettingsAndLayout, +) +from .._supporting_files._plot_functions import update_crossplot + + +class Crossplot(ViewABC): + class Ids: + CASE_SETTINGS = "case-setting" + FILTER_SETTINGS = "filter-settings" + PLOT_OPTIONS = "misfit-options" + PLOT_SETTINGS_AND_LAYOUT = "plot-settings-and-layout" + GRAPHS = "graphs" + + def __init__( + self, + attributes: List[str], + ens_names: List, + region_names: List[int], + realizations: List, + dframe: Dict, + caseinfo: str, + ) -> None: + super().__init__("Crossplot - sim vs obs") + self.attributes = attributes + self.ens_names = ens_names + self.region_names = region_names + self.realizations = realizations + self.dframe = dframe + self.caseinfo = caseinfo + + self.add_settings_groups( + { + self.Ids.CASE_SETTINGS: CaseSettings(self.attributes, self.ens_names), + self.Ids.FILTER_SETTINGS: FilterSettings( + self.region_names, self.realizations + ), + self.Ids.PLOT_OPTIONS: PlotOptions(), + self.Ids.PLOT_SETTINGS_AND_LAYOUT: PlotSettingsAndLayout(), + } + ) + + self.add_column(self.Ids.GRAPHS) + + def set_callbacks(self) -> None: + # --- Seismic crossplot - sim vs obs --- + @callback( + Output( + self.layout_element(self.Ids.GRAPHS).get_unique_id().to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REGION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REALIZATION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(PlotOptions.Ids.COLOR_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(PlotOptions.Ids.SIZE_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(PlotOptions.Ids.SIM_ERROR_BAR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(PlotSettingsAndLayout.Ids.LAYOUT_COLUMNS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(PlotSettingsAndLayout.Ids.LAYOUT_HEIGHT) + .to_string(), + "value", + ), + # prevent_initial_call=True, + ) + def _update_crossplot_graph( + attr_name: str, + ens_names: List[str], + regions: List[Union[int, str]], + realizations: List[Union[int, str]], + colorby: Optional[str], + sizeby: Optional[str], + showerrbar: Optional[str], + figcols: int, + figheight: int, + ) -> Optional[List[wcc.Graph]]: + + if not regions: + raise PreventUpdate + if not realizations: + raise PreventUpdate + + # --- ensure int type + regions = [int(reg) for reg in regions] + realizations = [int(real) for real in realizations] + + # --- apply region filter + dframe = self.dframe[attr_name].loc[ + self.dframe[attr_name]["region"].isin(regions) + ] + + # --- apply realization filter + col_names = ["real-" + str(real) for real in realizations] + dframe = dframe.drop( + columns=[ + col for col in dframe if "real-" in col and col not in col_names + ] + ) + + # --- apply ensemble filter + dframe = dframe[dframe.ENSEMBLE.isin(ens_names)] + + # --- make graphs + figures = update_crossplot( + dframe, + colorby=colorby, + sizeby=sizeby, + showerrorbar=showerrbar, + fig_columns=figcols, + figheight=figheight, + ) + return figures + [ + wcc.Selectors( + label="Ensemble info", + children=[ + dcc.Textarea( + value=self.caseinfo, + style={ + "width": 500, + }, + ), + ], + ) + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py new file mode 100644 index 000000000..45d38e09c --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -0,0 +1,400 @@ +from typing import Dict, List, Optional, Tuple, Union + +import webviz_core_components as wcc +from dash import Input, Output, callback, dcc +from dash.development.base_component import Component +from dash.exceptions import PreventUpdate +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from .._shared_settings import CaseSettings, FilterSettings +from .._supporting_files._plot_functions import ( + update_errorbarplot, + update_errorbarplot_superimpose, +) + + +class ErrorbarPlotOptions(SettingsGroupABC): + class Ids: + COLOR_BY = "color-br" + SIM_ERRORBAR = "sim-errorbar" + OBS_ERRORBAR = "obs-errorbar" + + def __init__(self) -> None: + super().__init__("Plot options") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Color by", + id=self.register_component_unique_id(self.Ids.COLOR_BY), + options=[ + { + "label": "none", + "value": None, + }, + { + "label": "region", + "value": "region", + }, + { + "label": "sim_std", + "value": "sim_std", + }, + { + "label": "diff_mean", + "value": "diff_mean", + }, + { + "label": "diff_std", + "value": "diff_std", + }, + ], + style={"display": "block"}, + value="region", + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Sim errorbar", + id=self.register_component_unique_id(self.Ids.SIM_ERRORBAR), + options=[ + { + "label": "Sim std", + "value": "sim_std", + }, + { + "label": "Sim p10/p90", + "value": "sim_p10_p90", + }, + { + "label": "none", + "value": None, + }, + ], + value="sim_std", + clearable=True, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Obs errorbar", + id=self.register_component_unique_id(self.Ids.OBS_ERRORBAR), + options=[ + { + "label": "Obs std", + "value": "obs_error", + }, + { + "label": "none", + "value": None, + }, + ], + value=None, + ), + ] + + +class ErrorbarPlotSettingsAndLayout(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + LAYOUT_HEIGHT = "layout-height" + LAYOUT_COLUMNS = "layout-columns" + X_AXIS_SETTINGS = "x-axix-settings" + SUPERIMPOSE_PLOT = "superimpose-plot" + + def __init__(self) -> None: + super().__init__("Plot settings and layout") + + def layout(self) -> List[Component]: + return [ + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.X_AXIS_SETTINGS), + label="X-axis settings", + options=[ + { + "label": "Reset index/sort by region", + "value": True, + }, + { + "label": "Original ordering", + "value": False, + }, + ], + value=False, + ), + wcc.RadioItems( + label="Superimpose plots", + id=self.register_component_unique_id(self.Ids.SUPERIMPOSE_PLOT), + options=[ + { + "label": "True", + "value": True, + }, + { + "label": "False", + "value": False, + }, + ], + value=False, + ), + wcc.Dropdown( + label="Fig layout - height", + id=self.register_component_unique_id(self.Ids.LAYOUT_HEIGHT), + options=[ + { + "label": "Very small", + "value": 250, + }, + { + "label": "Small", + "value": 350, + }, + { + "label": "Medium", + "value": 450, + }, + { + "label": "Large", + "value": 700, + }, + { + "label": "Very large", + "value": 1000, + }, + ], + value=450, + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Fig layout - # columns", + id=self.register_component_unique_id(self.Ids.LAYOUT_COLUMNS), + options=[ + { + "label": "One column", + "value": 1, + }, + { + "label": "Two columns", + "value": 2, + }, + { + "label": "Three columns", + "value": 3, + }, + ], + style={"display": "block"}, + value=1, + clearable=False, + persistence=True, + persistence_type="memory", + ), + ] + + +class ErrorbarPlots(ViewABC): + class Ids: + CASE_SETTINGS = "case-setting" + FILTER_SETTINGS = "filter-settings" + PLOT_OPTIONS = "misfit-options" + PLOT_SETTINGS_AND_LAYOUT = "plot-settings-and-layout" + GRAPHS = "graphs" + + def __init__( + self, + attributes: List[str], + ens_names: List, + region_names: List[int], + realizations: List, + dframe: Dict, + caseinfo: str, + ) -> None: + super().__init__("Errorbar - sim vs obs") + self.attributes = attributes + self.ens_names = ens_names + self.region_names = region_names + self.realizations = realizations + self.dframe = dframe + self.caseinfo = caseinfo + + self.add_settings_groups( + { + self.Ids.CASE_SETTINGS: CaseSettings(self.attributes, self.ens_names), + self.Ids.FILTER_SETTINGS: FilterSettings( + self.region_names, self.realizations + ), + self.Ids.PLOT_OPTIONS: ErrorbarPlotOptions(), + self.Ids.PLOT_SETTINGS_AND_LAYOUT: ErrorbarPlotSettingsAndLayout(), + } + ) + + self.add_column(self.Ids.GRAPHS) + + def set_callbacks(self) -> None: + # --- Seismic errorbar plot - sim vs obs --- + @callback( + Output( + self.layout_element(self.Ids.GRAPHS).get_unique_id().to_string(), + "children", + ), + Output( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(ErrorbarPlotSettingsAndLayout.Ids.LAYOUT_COLUMNS) + .to_string(), + "style", + ), + Output( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(ErrorbarPlotOptions.Ids.COLOR_BY) + .to_string(), + "style", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REGION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REALIZATION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(ErrorbarPlotOptions.Ids.COLOR_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(ErrorbarPlotOptions.Ids.SIM_ERRORBAR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_OPTIONS) + .component_unique_id(ErrorbarPlotOptions.Ids.OBS_ERRORBAR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(ErrorbarPlotSettingsAndLayout.Ids.X_AXIS_SETTINGS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(ErrorbarPlotSettingsAndLayout.Ids.SUPERIMPOSE_PLOT) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(ErrorbarPlotSettingsAndLayout.Ids.LAYOUT_COLUMNS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(ErrorbarPlotSettingsAndLayout.Ids.LAYOUT_HEIGHT) + .to_string(), + "value", + ), + # prevent_initial_call=True, + ) + def _update_errorbar_graph( + attr_name: str, + ens_names: List[str], + regions: List[Union[int, str]], + realizations: List[Union[int, str]], + colorby: Optional[str], + errbar: Optional[str], + errbarobs: Optional[str], + resetindex: bool, + superimpose: bool, + figcols: int, + figheight: int, + ) -> Tuple[Optional[List], Dict[str, str], Dict[str, str]]: + + if not regions: + raise PreventUpdate + if not realizations: + raise PreventUpdate + + # --- ensure int type + regions = [int(reg) for reg in regions] + realizations = [int(real) for real in realizations] + + # --- apply region filter + dframe = self.dframe[attr_name].loc[ + self.dframe[attr_name]["region"].isin(regions) + ] + # --- apply realization filter + col_names = ["real-" + str(real) for real in realizations] + dframe = dframe.drop( + columns=[ + col for col in dframe if "real-" in col and col not in col_names + ] + ) + + show_hide_selector = {"display": "block"} + if superimpose: + show_hide_selector = {"display": "none"} + + # --- apply ensemble filter + dframe = dframe[dframe.ENSEMBLE.isin(ens_names)] + + # --- make graphs + if superimpose: + figures = update_errorbarplot_superimpose( + dframe, + showerrorbar=errbar, + showerrorbarobs=errbarobs, + reset_index=resetindex, + figheight=figheight, + ) + else: + figures = update_errorbarplot( + dframe, + colorby=colorby, + showerrorbar=errbar, + showerrorbarobs=errbarobs, + reset_index=resetindex, + fig_columns=figcols, + figheight=figheight, + ) + return ( + figures + + [ + wcc.Selectors( + label="Ensemble info", + children=[ + dcc.Textarea( + value=self.caseinfo, + style={ + "width": 500, + }, + ), + ], + ) + ], + show_hide_selector, + show_hide_selector, + ) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py new file mode 100644 index 000000000..12e2cb992 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py @@ -0,0 +1,389 @@ +from typing import Any, Dict, List, Optional, Tuple, Union + +import pandas as pd +import webviz_core_components as wcc +from dash import Input, Output, callback +from dash.development.base_component import Component +from dash.exceptions import PreventUpdate +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from .._shared_settings import CaseSettings, FilterSettings, MapPlotSettings +from .._supporting_files._plot_functions import update_obs_sim_map_plot +from .._supporting_files._support_functions import _map_initial_marker_size +from .._view_elements import SeismicSlider + + +class SliceSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + SLICING_ACCURACY = "slicing-accuracy" + PLOT_TYPE = "plot-type" + + def __init__(self) -> None: + super().__init__("Plot settings and layout") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Slicing accuracy (north ± meters)", + id=self.register_component_unique_id(self.Ids.SLICING_ACCURACY), + options=[ + {"label": "± 10m", "value": 10}, + {"label": "± 25m", "value": 25}, + {"label": "± 50m", "value": 50}, + {"label": "± 75m", "value": 75}, + { + "label": "± 100m", + "value": 100, + }, + { + "label": "± 150m", + "value": 150, + }, + { + "label": "± 200m", + "value": 200, + }, + { + "label": "± 250m", + "value": 250, + }, + ], + value=75, + clearable=False, + persistence=True, + persistence_type="memory", + ), + # wcc.Dropdown( + wcc.RadioItems( + label="Plot type", + id=self.register_component_unique_id(self.Ids.PLOT_TYPE), + options=[ + {"label": "Statistics", "value": "stat"}, + { + "label": "Individual realizations", + "value": "reals", + }, + ], + value="stat", + # clearable=False, + # persistence=True, + # persistence_type="memory", + ), + ] + + +class MapPlot(ViewABC): + class Ids: + CASE_SETTINGS = "case-setting" + FILTER_SETTINGS = "filter-settings" + MAP_PLOT_SETTINGS = "map-plot-settings" + SLICE_SETTINGS = "slice-settings" + SLICE_POSITION = "slice-position" + PLOT_FIGS = "plot-figs" + PLOT_SLICE = "plot-slice" + + def __init__( + self, + attributes: List[str], + ens_names: List, + region_names: List[int], + realizations: List, + dframe: Dict, + dframeobs: dict, + df_polygons: pd.DataFrame, + caseinfo: str, + ) -> None: + super().__init__("Errorbar - sim vs obs") + self.attributes = attributes + self.ens_names = ens_names + self.region_names = region_names + self.realizations = realizations + self.dframe = dframe + self.dframeobs = dframeobs + self.df_polygons = df_polygons + self.polygon_names = sorted(list(self.df_polygons.name.unique())) + self.caseinfo = caseinfo + self.map_y_range: List[float] = [] + + # -- get initial obs data range + self.obs_range_init = [ + self.dframeobs[self.attributes[0]]["obs"].min(), + self.dframeobs[self.attributes[0]]["obs"].max(), + ] + self.obs_error_range_init = [ + self.dframeobs[self.attributes[0]]["obs_error"].min(), + self.dframeobs[self.attributes[0]]["obs_error"].max(), + ] + + self.map_intial_marker_size = _map_initial_marker_size( + len(self.dframeobs[attributes[0]].index), + len(self.ens_names), + ) + + # -- get map north range + for attribute_name in self.attributes: + if not self.map_y_range: + self.map_y_range = [ + self.dframeobs[attribute_name]["north"].min(), + self.dframeobs[attribute_name]["north"].max(), + ] + else: + north_min = self.dframeobs[attribute_name]["north"].min() + north_max = self.dframeobs[attribute_name]["north"].max() + self.map_y_range = [ + min(north_min, self.map_y_range[0]), + max(north_max, self.map_y_range[1]), + ] + + self.add_settings_groups( + { + self.Ids.CASE_SETTINGS: CaseSettings(self.attributes, self.ens_names), + self.Ids.FILTER_SETTINGS: FilterSettings( + self.region_names, self.realizations + ), + self.Ids.MAP_PLOT_SETTINGS: MapPlotSettings( + self.map_intial_marker_size, self.polygon_names + ), + self.Ids.SLICE_SETTINGS: SliceSettings(), + } + ) + + column = self.add_column() + column.make_row(self.Ids.PLOT_FIGS) + slice_position = column.make_row() + slice_position.add_view_element( + SeismicSlider(self.map_y_range), self.Ids.SLICE_POSITION + ) + column.make_row(self.Ids.PLOT_SLICE) + + def set_callbacks(self) -> None: + @callback( + Output( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "multi", + ), + Output( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input("webviz-content-manager", "activeViewId"), + ) + def _update_case_settings(viewId: str) -> Tuple: + return (False, self.ens_names[0]) + + @callback( + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY) + .to_string(), + "label", + ), + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY) + .to_string(), + "options", + ), + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY) + .to_string(), + "value", + ), + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING) + .to_string(), + "options", + ), + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING) + .to_string(), + "value", + ), + Input("webviz-content-manager", "activeViewId"), + ) + def _update_map_plot_settings(viewId: str) -> Tuple: + return ( + "Show difference or coverage plot", + [ + { + "label": "Difference plot", + "value": 0, + }, + { + "label": "Coverage plot", + "value": 1, + }, + { + "label": "Coverage plot (obs error adjusted)", + "value": 2, + }, + { + "label": "Region plot", + "value": 3, + }, + ], + 0, + [ + {"label": f"{val:.0%}", "value": val} + for val in [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1.0, + 1.5, + 2, + 5, + 10, + ] + ], + 0.8, + ) + + # --- Seismic errorbar plot - sim vs obs --- + @callback( + Output( + self.layout_element(self.Ids.PLOT_FIGS).get_unique_id().to_string(), + "children", + ), + Output( + self.layout_element(self.Ids.PLOT_SLICE).get_unique_id().to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REGION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REALIZATION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.MARKER_SIZE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.POLYGONS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SLICE_SETTINGS) + .component_unique_id(SliceSettings.Ids.SLICING_ACCURACY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SLICE_SETTINGS) + .component_unique_id(SliceSettings.Ids.PLOT_TYPE) + .to_string(), + "value", + ), + Input( + self.view_element(self.Ids.SLICE_POSITION) + .component_unique_id(SeismicSlider.Ids.SLIDER) + .to_string(), + "value", + ), + # prevent_initial_call=True, + ) + def _update_map_plot_obs_and_sim( + attr_name: str, + ens_name: str, + regions: List[Union[int, str]], + realizations: List[Union[int, str]], + plot_coverage: int, + scale_col_range: float, + marker_size: int, + map_plot_polygon: str, + slice_accuracy: Union[int, float], + slice_type: str, + slice_position: float, + ) -> Tuple[Optional[Any], Optional[Any]]: + + if not regions: + raise PreventUpdate + + # --- ensure int type + regions = [int(reg) for reg in regions] + + obs_range = [ + self.dframeobs[attr_name]["obs"].min(), + self.dframeobs[attr_name]["obs"].max(), + ] + + # --- apply region filter + dframe = self.dframe[attr_name].loc[ + self.dframe[attr_name]["region"].isin(regions) + ] + + # --- apply realization filter + col_names = ["real-" + str(real) for real in realizations] + dframe = dframe.drop( + columns=[ + col for col in dframe if "real-" in col and col not in col_names + ] + ) + + df_poly = pd.DataFrame() + if self.df_polygons is not None: + df_poly = self.df_polygons[self.df_polygons.name == map_plot_polygon] + + fig_maps, fig_slice = update_obs_sim_map_plot( + dframe, + ens_name, + df_polygon=df_poly, + obs_range=obs_range, + scale_col_range=scale_col_range, + slice_accuracy=slice_accuracy, + slice_position=slice_position, + plot_coverage=plot_coverage, + marker_size=marker_size, + slice_type=slice_type, + ) + + return (wcc.Graph(figure=fig_maps), wcc.Graph(figure=fig_slice)) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py new file mode 100644 index 000000000..6d3cbb119 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py @@ -0,0 +1,306 @@ +from typing import Dict, List, Union + +import webviz_core_components as wcc +from dash import Input, Output, callback, dcc +from dash.development.base_component import Component +from dash.exceptions import PreventUpdate +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from .._shared_settings import CaseSettings, FilterSettings +from .._supporting_files._plot_functions import update_misfit_plot + + +class MisfitOptions(SettingsGroupABC): + class Ids: + WEIGHT = "weight" + EXPONENT = "exponent" + NORMALIZATION = "normalization" + + def __init__(self) -> None: + super().__init__("Misfit Options") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Misfit weight", + id=self.register_component_unique_id(self.Ids.WEIGHT), + options=[ + { + "label": "none", + "value": None, + }, + { + "label": "Obs error", + "value": "obs_error", + }, + ], + value="obs_error", + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Misfit exponent", + id=self.register_component_unique_id(self.Ids.EXPONENT), + options=[ + { + "label": "Linear sum", + "value": 1.0, + }, + { + "label": "Squared sum", + "value": 2.0, + }, + ], + value=2.0, + clearable=False, + persistence=True, + persistence_type="memory", + ), + wcc.Dropdown( + label="Misfit normalization", + id=self.register_component_unique_id(self.Ids.NORMALIZATION), + options=[ + { + "label": "Yes", + "value": True, + }, + { + "label": "No", + "value": False, + }, + ], + value=False, + clearable=False, + persistence=True, + persistence_type="memory", + ), + ] + + +class PerRealPlotSettingsAndLayout(SettingsGroupABC): + class Ids: + SORTING = "sorting" + HEIGHT = "height" + + def __init__(self) -> None: + super().__init__("Plot settings and layout") + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Sorting/ranking", + id=self.register_component_unique_id(self.Ids.SORTING), + options=[ + { + "label": "none", + "value": None, + }, + { + "label": "ascending", + "value": True, + }, + { + "label": "descending", + "value": False, + }, + ], + value=True, + ), + wcc.Dropdown( + label="Fig layout - height", + id=self.register_component_unique_id(self.Ids.HEIGHT), + options=[ + { + "label": "Very small", + "value": 250, + }, + { + "label": "Small", + "value": 350, + }, + { + "label": "Medium", + "value": 450, + }, + { + "label": "Large", + "value": 700, + }, + { + "label": "Very large", + "value": 1000, + }, + ], + value=450, + clearable=False, + persistence=True, + persistence_type="memory", + ), + ] + + +class MisfitPerReal(ViewABC): + class Ids: + CASE_SETTINGS = "case-setting" + FILTER_SETTINGS = "filter-settings" + PLOT_SETTINGS_AND_LAYOUT = "plot-settings-and-layout" + MISFIT_OPTIONS = "misfit-options" + GRAPHS = "graphs" + INFO_ELEMENT = "info-element" + + def __init__( + self, + attributes: List[str], + ens_names: List, + region_names: List[int], + realizations: List, + dframe: Dict, + caseinfo: str, + ) -> None: + super().__init__("Misfit per real") + self.attributes = attributes + self.ens_names = ens_names + self.region_names = region_names + self.realizations = realizations + self.dframe = dframe + self.caseinfo = caseinfo + + self.add_settings_groups( + { + self.Ids.CASE_SETTINGS: CaseSettings(self.attributes, self.ens_names), + self.Ids.FILTER_SETTINGS: FilterSettings( + self.region_names, self.realizations + ), + self.Ids.PLOT_SETTINGS_AND_LAYOUT: PerRealPlotSettingsAndLayout(), + self.Ids.MISFIT_OPTIONS: MisfitOptions(), + } + ) + + self.add_column(self.Ids.GRAPHS) + + def set_callbacks(self) -> None: + @callback( + Output( + self.layout_element(self.Ids.GRAPHS).get_unique_id().to_string(), + "children", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REGION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(FilterSettings.Ids.REALIZATION_SELECTOR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(PerRealPlotSettingsAndLayout.Ids.SORTING) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.PLOT_SETTINGS_AND_LAYOUT) + .component_unique_id(PerRealPlotSettingsAndLayout.Ids.HEIGHT) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MISFIT_OPTIONS) + .component_unique_id(MisfitOptions.Ids.WEIGHT) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MISFIT_OPTIONS) + .component_unique_id(MisfitOptions.Ids.EXPONENT) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MISFIT_OPTIONS) + .component_unique_id(MisfitOptions.Ids.NORMALIZATION) + .to_string(), + "value", + ), + # prevent_initial_call=True, + ) + def _update_misfit_graph( + attr_name: str, + ens_names: List, + regions: List[Union[int, str]], + realizations: List[Union[int, str]], + sorting: str, + figheight: int, + misfit_weight: str, + misfit_exponent: float, + misfit_normalization: bool, + ) -> List: + + if not regions: + raise PreventUpdate + if not realizations: + raise PreventUpdate + + # --- ensure int type + regions = [int(reg) for reg in regions] + realizations = [int(real) for real in realizations] + + # --- apply region filter + dframe = self.dframe[attr_name].loc[ + self.dframe[attr_name]["region"].isin(regions) + ] + + # --- apply realization filter + col_names = ["real-" + str(real) for real in realizations] + dframe = dframe.drop( + columns=[ + col for col in dframe if "real-" in col and col not in col_names + ] + ) + + if not isinstance(ens_names, List): + ens_names = [ens_names] + + # --- apply ensemble filter + dframe = dframe[dframe.ENSEMBLE.isin(ens_names)] + + # --- make graphs, return as list + figures = update_misfit_plot( + dframe, + sorting, + figheight, + misfit_weight, + misfit_exponent, + misfit_normalization, + ) + + return figures + [ + wcc.Selectors( + label="Ensemble info", + children=[ + dcc.Textarea( + value=self.caseinfo, + style={ + "width": 500, + }, + ), + ], + ) + ] diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py new file mode 100644 index 000000000..4e36b4155 --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py @@ -0,0 +1,401 @@ +from typing import List, Tuple, Union + +import pandas as pd +import webviz_core_components as wcc +from dash import Input, Output, callback +from dash.development.base_component import Component +from dash.exceptions import PreventUpdate +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from .._shared_settings import CaseSettings, MapPlotSettings +from .._supporting_files._plot_functions import update_obsdata_map, update_obsdata_raw +from .._supporting_files._support_functions import _map_initial_marker_size +from .._view_elements import InfoBox + + +class ObsFilterSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods + class Ids: + REGION_NAME = "region-name" + NOISE_FILTER = "noise-filter" + FILTER_VALUE = "filter-value" + + def __init__( + self, region_names: List[int], obs_error_range_init: List, obs_range_init: List + ) -> None: + super().__init__("filter sttings") + self.region_names = region_names + self.obs_error_range_init = obs_error_range_init + self.obs_range_init = obs_range_init + + def layout(self) -> List[Component]: + return [ + wcc.SelectWithLabel( + label="Region selector", + id=self.register_component_unique_id(self.Ids.REGION_NAME), + options=[ + {"label": regno, "value": regno} for regno in self.region_names + ], + size=min([len(self.region_names), 5]), + value=self.region_names, + ), + wcc.Slider( + label="Noise filter", + id=self.register_component_unique_id(self.Ids.NOISE_FILTER), + min=0, + max=0.5 + * max( + abs(self.obs_range_init[0]), + abs(self.obs_range_init[1]), + ), + step=0.5 * self.obs_error_range_init[0], + marks=None, + value=0, + ), + wcc.Label( + id=self.register_component_unique_id(self.Ids.FILTER_VALUE), + style={ + "color": "blue", + "font-size": "15px", + }, + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output( + self.component_unique_id(self.Ids.FILTER_VALUE).to_string(), "children" + ), + Input(self.component_unique_id(self.Ids.NOISE_FILTER).to_string(), "value"), + ) + def _update_noise_filter_value(noise_filter_value: float) -> str: + return f"Current noise filter value: {noise_filter_value}" + + +class RawPlotSettings(SettingsGroupABC): + class Ids: + OBS_ERROR = "obs-error" + HISTOGRAM = "histogram" + X_AXIS_SETTINGS = "x-axix-settings" + + def __init__(self) -> None: + super().__init__("Raw plot settings") + + def layout(self) -> List[Component]: + return [ + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.OBS_ERROR), + label="Obs error", + options=[ + { + "label": "On", + "value": True, + }, + { + "label": "Off", + "value": False, + }, + ], + value=False, + ), + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.HISTOGRAM), + label="Histogram", + options=[ + { + "label": "On", + "value": True, + }, + { + "label": "Off", + "value": False, + }, + ], + value=False, + ), + wcc.RadioItems( + id=self.register_component_unique_id(self.Ids.X_AXIS_SETTINGS), + label="X-axis settings", + options=[ + { + "label": "Reset index/sort by region", + "value": True, + }, + { + "label": "Original ordering", + "value": False, + }, + ], + value=False, + ), + ] + + +class ObsData(ViewABC): + class Ids: + CASE_SETTINGS = "case-setting" + FILTER_SETTINGS = "filter-settings" + RAW_PLOT_SETTINGS = "raw-plot-settings" + MAP_PLOT_SETTINGS = "map_plot_settings" + GRAPHS_RAW = "graphs-raw" + GRAPHS_MAP = "graphs-map" + ERROR_INFO = "error-info" + ERROR_INFO_ELEMENT = "error-info-element" + + def __init__( + self, + attributes: List[str], + ens_names: List, + region_names: List[int], + dframeobs: dict, + df_polygons: pd.DataFrame, + caseinfo: str, + ) -> None: + super().__init__("Seismic obs data") + self.attributes = attributes + self.ens_names = ens_names + self.region_names = region_names + self.dframeobs = dframeobs + self.df_polygons = df_polygons + self.polygon_names = sorted(list(self.df_polygons.name.unique())) + self.caseinfo = caseinfo + + # -- get initial obs data range + self.obs_range_init = [ + self.dframeobs[self.attributes[0]]["obs"].min(), + self.dframeobs[self.attributes[0]]["obs"].max(), + ] + self.obs_error_range_init = [ + self.dframeobs[self.attributes[0]]["obs_error"].min(), + self.dframeobs[self.attributes[0]]["obs_error"].max(), + ] + + self.map_intial_marker_size = _map_initial_marker_size( + len(self.dframeobs[attributes[0]].index), + len(self.ens_names), + ) + + self.add_settings_groups( + { + self.Ids.CASE_SETTINGS: CaseSettings(self.attributes, self.ens_names), + self.Ids.FILTER_SETTINGS: ObsFilterSettings( + self.region_names, self.obs_error_range_init, self.obs_range_init + ), + self.Ids.RAW_PLOT_SETTINGS: RawPlotSettings(), + self.Ids.MAP_PLOT_SETTINGS: MapPlotSettings( + self.map_intial_marker_size, self.polygon_names + ), + } + ) + + column = self.add_column() + column.make_row(self.Ids.GRAPHS_RAW) + column.make_row(self.Ids.GRAPHS_MAP) + error_info = column.make_row(self.Ids.ERROR_INFO) + error_info.add_view_element( + InfoBox("Obsdata info", self.caseinfo), self.Ids.ERROR_INFO_ELEMENT + ) + + def set_callbacks(self) -> None: + @callback( + Output( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "multi", + ), + Output( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input("webviz-content-manager", "activeViewId"), + ) + def _update_case_settings(viewId: str) -> Tuple: + return (False, self.ens_names[0]) + + # --- Seismic obs data --- + @callback( + Output( + self.layout_element(self.Ids.GRAPHS_RAW).get_unique_id().to_string(), + "children", + ), + Output( + self.layout_element(self.Ids.GRAPHS_MAP).get_unique_id().to_string(), + "children", + ), + # Output(self.uuid("obsdata-graph-map"), "figure"), + Output( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING) + .to_string(), + "style", + ), + # Output(self.uuid("obsdata-noise_filter_text"), "children"), + Output( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.NOISE_FILTER) + .to_string(), + "max", + ), + Output( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.NOISE_FILTER) + .to_string(), + "step", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ATTRIBUTE_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.CASE_SETTINGS) + .component_unique_id(CaseSettings.Ids.ENSEMBLES_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.REGION_NAME) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTER_SETTINGS) + .component_unique_id(ObsFilterSettings.Ids.NOISE_FILTER) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.OBS_ERROR) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.HISTOGRAM) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.RAW_PLOT_SETTINGS) + .component_unique_id(RawPlotSettings.Ids.X_AXIS_SETTINGS) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_BY) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.COLOR_RANGE_SCALING) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.MARKER_SIZE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.MAP_PLOT_SETTINGS) + .component_unique_id(MapPlotSettings.Ids.POLYGONS) + .to_string(), + "value", + ), + # prevent_initial_call=True, + ) + def _update_obsdata_graph( + attr_name: str, + ens_name: str, + regions: List[Union[int, str]], + noise_filter: float, + showerror: bool, + showhistogram: bool, + resetindex: bool, + obsmap_colorby: str, + obsmap_scale_col_range: float, + obsmap_marker_size: int, + obsmap_polygon: str, + ) -> Tuple: + + if not regions: + raise PreventUpdate + + # --- ensure int type + regions = [int(reg) for reg in regions] + + obs_range = [ + self.dframeobs[attr_name]["obs"].min(), + self.dframeobs[attr_name]["obs"].max(), + ] + obs_error_range = [ + self.dframeobs[attr_name]["obs_error"].min(), + self.dframeobs[attr_name]["obs_error"].max(), + ] + + # --- apply region filter + dframe_obs = self.dframeobs[attr_name].loc[ + self.dframeobs[attr_name]["region"].isin(regions) + ] + + # --- apply ensemble filter + dframe_obs = dframe_obs[dframe_obs.ENSEMBLE.eq(ens_name)] + + # --- apply noise filter + dframe_obs = dframe_obs[abs(dframe_obs.obs).ge(noise_filter)] + + df_poly = pd.DataFrame() + if self.df_polygons is not None: + df_poly = self.df_polygons[self.df_polygons.name == obsmap_polygon] + + # --- make graphs + fig_map = update_obsdata_map( + dframe_obs.copy(), + colorby=obsmap_colorby, + df_polygon=df_poly, + obs_range=obs_range, + obs_err_range=obs_error_range, + scale_col_range=obsmap_scale_col_range, + marker_size=obsmap_marker_size, + ) + # if fig_raw is run before fig_map some strange value error + # my arise at init callback --> unknown reason + fig_raw = update_obsdata_raw( + dframe_obs.copy(), + colorby="region", + showerror=showerror, + showhistogram=showhistogram, + reset_index=resetindex, + ) + + graphs_raw = wcc.Graph( + style={"height": "37vh"}, + figure=fig_raw, + ) + graph_map = wcc.Graph( + style={"height": "50vh"}, + figure=fig_map, + ) + + show_hide_range_scaling = {"display": "block"} + if obsmap_colorby == "region": + show_hide_range_scaling = {"display": "none"} + + noise_filter_max = 0.5 * max(abs(obs_range[0]), abs(obs_range[1])) + noise_filter_step = 0.5 * obs_error_range[0] + return ( + graphs_raw, + graph_map, + show_hide_range_scaling, + noise_filter_max, + noise_filter_step, + ) diff --git a/webviz_subsurface/plugins/_seismic_misfit.py b/webviz_subsurface/plugins/_seismic_misfit_ori.py similarity index 100% rename from webviz_subsurface/plugins/_seismic_misfit.py rename to webviz_subsurface/plugins/_seismic_misfit_ori.py From 97fd83860cafe88f7114f0e4edbd57fe3bb468f6 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 09:10:53 +0200 Subject: [PATCH 02/16] reformating --- .../plugins/_seismic_misfit/_plugin.py | 6 +- .../plugins/_seismic_misfit/_plugin_ids.py | 3 + .../_seismic_misfit/_seismic_color_scales.py | 1 + .../_seismic_misfit/_view_elements/_slider.py | 1 + .../_seismic_misfit/_views/_crossplot.py | 2 + .../_seismic_misfit/_views/_errorbar_plot.py | 2 + .../_seismic_misfit/_views/_map_plot.py | 84 ++++++++++++++----- .../_views/_misfit_per_real.py | 4 + .../_seismic_misfit/_views/_obs_data.py | 10 ++- 9 files changed, 85 insertions(+), 28 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py index e0e7c7a43..f41684340 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_plugin.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py @@ -8,10 +8,7 @@ from ._plugin_ids import PluginIds from ._shared_settings import CaseSettings, MapPlotSettings from ._supporting_files._dataframe_functions import make_polygon_df, makedf -from ._supporting_files._support_functions import ( - _compare_dfs_obs, - _map_initial_marker_size, -) +from ._supporting_files._support_functions import _compare_dfs_obs from ._views import Crossplot, ErrorbarPlots, MapPlot, MisfitPerReal, ObsData from ._views._obs_data import ObsFilterSettings, RawPlotSettings @@ -117,6 +114,7 @@ class SeismicMisfit(WebvizPluginABC): ``` """ + # pylint: disable=too-many-arguments def __init__( self, webviz_settings: WebvizSettings, diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py index 36ade49b1..67374958d 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py @@ -1,7 +1,10 @@ +# pylint: disable=too-few-public-methods class PluginIds: + # pylint: disable=too-few-public-methods class SharedSettings: CASE_SETTINGS = "case-settings" + # pylint: disable=too-few-public-methods class ViewsIds: VIEWS_GROUP = "Seismic" OBS_DATA = "obs-data" diff --git a/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py b/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py index f0eb57afc..9fa53df5a 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_seismic_color_scales.py @@ -1,3 +1,4 @@ +# pylint: disable=too-few-public-methods class ColorScales: SEISMIC_SYMMETRIC = [ [0, "yellow"], diff --git a/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py index cf6b30d89..b59d46c7f 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_view_elements/_slider.py @@ -6,6 +6,7 @@ class SeismicSlider(ViewElementABC): + # pylint: disable=too-few-public-methods class Ids: SLIDER = "slider" diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py index 7dcf6e66b..98a899ee3 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -15,6 +15,7 @@ class Crossplot(ViewABC): + # pylint: disable=too-few-public-methods class Ids: CASE_SETTINGS = "case-setting" FILTER_SETTINGS = "filter-settings" @@ -115,6 +116,7 @@ def set_callbacks(self) -> None: ), # prevent_initial_call=True, ) + # pylint: disable=too-many-arguments def _update_crossplot_graph( attr_name: str, ens_names: List[str], diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index 45d38e09c..732c612cf 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -14,6 +14,7 @@ class ErrorbarPlotOptions(SettingsGroupABC): + # pylint: disable=too-few-public-methods class Ids: COLOR_BY = "color-br" SIM_ERRORBAR = "sim-errorbar" @@ -319,6 +320,7 @@ def set_callbacks(self) -> None: ), # prevent_initial_call=True, ) + # pylint: disable=too-many-arguments def _update_errorbar_graph( attr_name: str, ens_names: List[str], diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py index 12e2cb992..e9328620d 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py @@ -83,6 +83,7 @@ class Ids: PLOT_FIGS = "plot-figs" PLOT_SLICE = "plot-slice" + # pylint: disable=too-many-arguments def __init__( self, attributes: List[str], @@ -94,7 +95,7 @@ def __init__( df_polygons: pd.DataFrame, caseinfo: str, ) -> None: - super().__init__("Errorbar - sim vs obs") + super().__init__("Map plot - sim vs obs") self.attributes = attributes self.ens_names = ens_names self.region_names = region_names @@ -173,8 +174,10 @@ def set_callbacks(self) -> None: ), Input("webviz-content-manager", "activeViewId"), ) - def _update_case_settings(viewId: str) -> Tuple: - return (False, self.ens_names[0]) + def _update_case_settings(view_id: str) -> Tuple: + if view_id == self.get_unique_id().to_string(): + return (False, self.ens_names[0]) + return (True, self.ens_names) @callback( Output( @@ -209,31 +212,70 @@ def _update_case_settings(viewId: str) -> Tuple: ), Input("webviz-content-manager", "activeViewId"), ) - def _update_map_plot_settings(viewId: str) -> Tuple: + def _update_map_plot_settings(view_id: str) -> Tuple: + if view_id == self.get_unique_id().to_string(): + return ( + "Show difference or coverage plot", + [ + { + "label": "Difference plot", + "value": 0, + }, + { + "label": "Coverage plot", + "value": 1, + }, + { + "label": "Coverage plot (obs error adjusted)", + "value": 2, + }, + { + "label": "Region plot", + "value": 3, + }, + ], + 0, + [ + {"label": f"{val:.0%}", "value": val} + for val in [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1.0, + 1.5, + 2, + 5, + 10, + ] + ], + 0.8, + ) return ( - "Show difference or coverage plot", + "Color by", [ { - "label": "Difference plot", - "value": 0, + "label": "region", + "value": "region", }, { - "label": "Coverage plot", - "value": 1, + "label": "obs", + "value": "obs", }, { - "label": "Coverage plot (obs error adjusted)", - "value": 2, - }, - { - "label": "Region plot", - "value": 3, + "label": "obs error", + "value": "obs_error", }, ], - 0, + "obs", [ - {"label": f"{val:.0%}", "value": val} - for val in [ + {"label": f"{x:.0%}", "value": x} + for x in [ 0.1, 0.2, 0.3, @@ -244,10 +286,6 @@ def _update_map_plot_settings(viewId: str) -> Tuple: 0.8, 0.9, 1.0, - 1.5, - 2, - 5, - 10, ] ], 0.8, @@ -331,6 +369,8 @@ def _update_map_plot_settings(viewId: str) -> Tuple: ), # prevent_initial_call=True, ) + # pylint: disable=too-many-arguments + # pylint: disable=too-many-locals def _update_map_plot_obs_and_sim( attr_name: str, ens_name: str, diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py index 6d3cbb119..ef4e52605 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_misfit_per_real.py @@ -11,6 +11,7 @@ class MisfitOptions(SettingsGroupABC): + # pylint: disable=too-few-public-methods class Ids: WEIGHT = "weight" EXPONENT = "exponent" @@ -79,6 +80,7 @@ def layout(self) -> List[Component]: class PerRealPlotSettingsAndLayout(SettingsGroupABC): + # pylint: disable=too-few-public-methods class Ids: SORTING = "sorting" HEIGHT = "height" @@ -141,6 +143,7 @@ def layout(self) -> List[Component]: class MisfitPerReal(ViewABC): + # pylint: disable=too-few-public-methods class Ids: CASE_SETTINGS = "case-setting" FILTER_SETTINGS = "filter-settings" @@ -241,6 +244,7 @@ def set_callbacks(self) -> None: ), # prevent_initial_call=True, ) + # pylint: disable=too-many-arguments def _update_misfit_graph( attr_name: str, ens_names: List, diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py index 4e36b4155..4d38a8f7f 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_obs_data.py @@ -73,6 +73,7 @@ def _update_noise_filter_value(noise_filter_value: float) -> str: class RawPlotSettings(SettingsGroupABC): + # pylint: disable=too-few-public-methods class Ids: OBS_ERROR = "obs-error" HISTOGRAM = "histogram" @@ -132,6 +133,7 @@ def layout(self) -> List[Component]: class ObsData(ViewABC): + # pylint: disable=too-few-public-methods class Ids: CASE_SETTINGS = "case-setting" FILTER_SETTINGS = "filter-settings" @@ -212,8 +214,10 @@ def set_callbacks(self) -> None: ), Input("webviz-content-manager", "activeViewId"), ) - def _update_case_settings(viewId: str) -> Tuple: - return (False, self.ens_names[0]) + def _update_case_settings(view_id: str) -> Tuple: + if view_id == self.get_unique_id().to_string(): + return (False, self.ens_names[0]) + return (True, self.ens_names) # --- Seismic obs data --- @callback( @@ -313,6 +317,8 @@ def _update_case_settings(viewId: str) -> Tuple: ), # prevent_initial_call=True, ) + # pylint: disable=too-many-arguments + # pylint: disable=too-many-locals def _update_obsdata_graph( attr_name: str, ens_name: str, From e46ea0aa99ed812dbbbf14b61939fc360ee75dfd Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 09:21:24 +0200 Subject: [PATCH 03/16] reformating --- webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py | 1 - .../plugins/_seismic_misfit/_views/_errorbar_plot.py | 1 + webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py index 67374958d..bbaea8209 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin_ids.py @@ -1,4 +1,3 @@ -# pylint: disable=too-few-public-methods class PluginIds: # pylint: disable=too-few-public-methods class SharedSettings: diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index 732c612cf..19ae6f999 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -196,6 +196,7 @@ def layout(self) -> List[Component]: class ErrorbarPlots(ViewABC): + # pylint: disable=too-few-public-methods class Ids: CASE_SETTINGS = "case-setting" FILTER_SETTINGS = "filter-settings" diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py index e9328620d..4d00681e7 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_map_plot.py @@ -74,6 +74,7 @@ def layout(self) -> List[Component]: class MapPlot(ViewABC): + # pylint: disable=too-few-public-methods class Ids: CASE_SETTINGS = "case-setting" FILTER_SETTINGS = "filter-settings" From 01619cf75bd172d6cf5919f64cc60c37f827b258 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 09:26:56 +0200 Subject: [PATCH 04/16] reformatting --- .../plugins/_seismic_misfit/_shared_settings/_plot_options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py index 83fc39e71..560f8c8fd 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py @@ -1,7 +1,8 @@ from typing import List + +import webviz_core_components as wcc from dash.development.base_component import Component from webviz_config.webviz_plugin_subclasses import SettingsGroupABC -import webviz_core_components as wcc class PlotOptions(SettingsGroupABC): From d77c4d2e69c93c4e2b53126d9b1ded818676a51f Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 09:34:27 +0200 Subject: [PATCH 05/16] reformating --- .../_seismic_misfit/_shared_settings/_filter_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py index a1c00742e..ac6381d9b 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_filter_settings.py @@ -11,7 +11,7 @@ class Ids: REGION_SELECTOR = "region-selector" REALIZATION_SELECTOR = "realization-selector" - def __init__(self, region_names, realizations) -> None: + def __init__(self, region_names: List[int], realizations: List) -> None: super().__init__("Filter sttings") self.region_names = region_names self.realizations = realizations From e73a9bd992edd6f7971b4f56e8de7d4a2fd51c3d Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 10:02:00 +0200 Subject: [PATCH 06/16] reformating --- .../_supporting_files/__init__.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 webviz_subsurface/plugins/_seismic_misfit/_supporting_files/__init__.py diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/__init__.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/__init__.py new file mode 100644 index 000000000..2dc89b51e --- /dev/null +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/__init__.py @@ -0,0 +1,23 @@ +from ._dataframe_functions import ( + df_seis_ens_stat, + make_polygon_df, + makedf_seis_addsim, + makedf_seis_obs_meta, +) +from ._plot_functions import ( + update_crossplot, + update_errorbarplot, + update_errorbarplot_superimpose, + update_misfit_plot, + update_obs_sim_map_plot, + update_obsdata_map, + update_obsdata_raw, +) +from ._support_functions import ( + _compare_dfs_obs, + _map_initial_marker_size, + average_arrow_annotation, + average_line_shape, + find_max_diff, + get_unique_column_values, +) From 1fe11b03e82318f421a41f506ee8afa35a763e7e Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 10:14:25 +0200 Subject: [PATCH 07/16] reformatting --- .../_seismic_misfit/_shared_settings/_plot_options.py | 3 +-- .../_supporting_files/_dataframe_functions.py | 4 +++- .../_seismic_misfit/_supporting_files/_plot_functions.py | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py index 560f8c8fd..83fc39e71 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py @@ -1,8 +1,7 @@ from typing import List - -import webviz_core_components as wcc from dash.development.base_component import Component from webviz_config.webviz_plugin_subclasses import SettingsGroupABC +import webviz_core_components as wcc class PlotOptions(SettingsGroupABC): diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py index 7c0a1fb31..bc8a78b83 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_dataframe_functions.py @@ -6,12 +6,12 @@ import numpy as np import pandas as pd -from webviz_config import WebvizPluginABC, WebvizSettings from webviz_config.webviz_store import webvizstore # ------------------------------- @webvizstore +# pylint: disable=too-many-locals def makedf( ensemble_set: dict, attribute_name: str, @@ -138,6 +138,7 @@ def makedf_seis_obs_meta( return dframe +# pylint: disable=too-many-locals def makedf_seis_addsim( df: pd.DataFrame, ens_path: str, @@ -200,6 +201,7 @@ def makedf_seis_addsim( return df_addsim +# pylint: disable=too-many-locals def df_seis_ens_stat( df: pd.DataFrame, ens_name: str, obs_error_weight: bool = False ) -> pd.DataFrame: diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py index 2456e8bcf..b5a8a41f7 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines import logging from typing import Any, List, Optional, Tuple, Union @@ -254,6 +255,8 @@ def update_obsdata_map( # ------------------------------- +# pylint: disable=too-many-statements +# pylint: disable=too-many-locals def update_obs_sim_map_plot( df: pd.DataFrame, ens_name: str, @@ -642,6 +645,7 @@ def update_obs_sim_map_plot( # ------------------------------- +# pylint: disable=too-many-locals def update_crossplot( df: pd.DataFrame, colorby: Optional[str] = None, @@ -764,6 +768,7 @@ def update_crossplot( # ------------------------------- # pylint: disable=too-many-statements +# pylint: disable=too-many-locals def update_errorbarplot( df: pd.DataFrame, colorby: Optional[str] = None, From 178f15977a56dc73622e4c2a3087a924e423604e Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 10:20:56 +0200 Subject: [PATCH 08/16] reformatting --- .../_seismic_misfit/_supporting_files/_plot_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py index b5a8a41f7..8e87697d2 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py @@ -255,7 +255,7 @@ def update_obsdata_map( # ------------------------------- -# pylint: disable=too-many-statements +# pylint: disable=too-many-arguments # pylint: disable=too-many-locals def update_obs_sim_map_plot( df: pd.DataFrame, From 84dd2dd811e2282131ccff095322146e8e144b04 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 10:27:48 +0200 Subject: [PATCH 09/16] reformatting --- .../plugins/_seismic_misfit/_shared_settings/_plot_options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py index 83fc39e71..560f8c8fd 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_shared_settings/_plot_options.py @@ -1,7 +1,8 @@ from typing import List + +import webviz_core_components as wcc from dash.development.base_component import Component from webviz_config.webviz_plugin_subclasses import SettingsGroupABC -import webviz_core_components as wcc class PlotOptions(SettingsGroupABC): From 3ba5d92ff73b0b3f08a075009a673a43f14a24cd Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 11:08:06 +0200 Subject: [PATCH 10/16] reformatting --- .../plugins/_seismic_misfit/_views/_crossplot.py | 11 ++++++----- .../plugins/_seismic_misfit/_views/_errorbar_plot.py | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py index 98a899ee3..7b77e2e19 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union import webviz_core_components as wcc from dash import Input, Output, callback, dcc @@ -127,7 +127,7 @@ def _update_crossplot_graph( showerrbar: Optional[str], figcols: int, figheight: int, - ) -> Optional[List[wcc.Graph]]: + ) -> Tuple: if not regions: raise PreventUpdate @@ -163,7 +163,8 @@ def _update_crossplot_graph( fig_columns=figcols, figheight=figheight, ) - return figures + [ + return ( + figures, wcc.Selectors( label="Ensemble info", children=[ @@ -174,5 +175,5 @@ def _update_crossplot_graph( }, ), ], - ) - ] + ), + ) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index 19ae6f999..cdda14bba 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import webviz_core_components as wcc from dash import Input, Output, callback, dcc @@ -334,7 +334,7 @@ def _update_errorbar_graph( superimpose: bool, figcols: int, figheight: int, - ) -> Tuple[Optional[List], Dict[str, str], Dict[str, str]]: + ) -> Tuple: if not regions: raise PreventUpdate @@ -383,6 +383,7 @@ def _update_errorbar_graph( fig_columns=figcols, figheight=figheight, ) + return ( figures + [ From 2efb5adfb3aab50471e845122ba6ba92d7faacf2 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 11:14:13 +0200 Subject: [PATCH 11/16] reformatting --- webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py | 2 +- .../plugins/_seismic_misfit/_views/_errorbar_plot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py index 7b77e2e19..18fa624e1 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import webviz_core_components as wcc from dash import Input, Output, callback, dcc diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index cdda14bba..2cc8cfaad 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import webviz_core_components as wcc from dash import Input, Output, callback, dcc From 46e7260e798a060f9df3d09131536092f769024b Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 11:26:05 +0200 Subject: [PATCH 12/16] reformatting --- .../_seismic_misfit/_supporting_files/_plot_functions.py | 4 ++-- .../plugins/_seismic_misfit/_views/_errorbar_plot.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py index 8e87697d2..fc611a8c3 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_supporting_files/_plot_functions.py @@ -777,7 +777,7 @@ def update_errorbarplot( reset_index: bool = False, fig_columns: int = 1, figheight: int = 450, -) -> Optional[List[wcc.Graph]]: +) -> Any: """Create errorbar plot of ensemble sim versus obs, one value per seismic datapoint.""" @@ -919,7 +919,7 @@ def update_errorbarplot_superimpose( showerrorbarobs: Optional[str] = None, reset_index: bool = True, figheight: int = 450, -) -> Optional[List[wcc.Graph]]: +) -> Any: """Create errorbar plot of ensemble sim versus obs, one value per seismic datapoint.""" diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index 2cc8cfaad..b43a3ebbd 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -383,6 +383,8 @@ def _update_errorbar_graph( fig_columns=figcols, figheight=figheight, ) + if figures == None: + figures = [] return ( figures From 1f09ada50965de0bb99aa18e68e45b501cae31dc Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Fri, 29 Jul 2022 11:32:50 +0200 Subject: [PATCH 13/16] reformatting --- .../plugins/_seismic_misfit/_views/_errorbar_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py index b43a3ebbd..4f1196adb 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_errorbar_plot.py @@ -383,7 +383,7 @@ def _update_errorbar_graph( fig_columns=figcols, figheight=figheight, ) - if figures == None: + if figures is None: figures = [] return ( From bcc7baaed57d7a8f9dc33e6852e8e26fb125a9e5 Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Tue, 2 Aug 2022 14:10:44 +0200 Subject: [PATCH 14/16] change children from tuple to list --- .../plugins/_seismic_misfit/_views/_crossplot.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py index 18fa624e1..d690d5dd8 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -127,7 +127,7 @@ def _update_crossplot_graph( showerrbar: Optional[str], figcols: int, figheight: int, - ) -> Tuple: + ) -> List: if not regions: raise PreventUpdate @@ -163,8 +163,11 @@ def _update_crossplot_graph( fig_columns=figcols, figheight=figheight, ) - return ( - figures, + + if figures is None: + figures = [] + + return figures + [ wcc.Selectors( label="Ensemble info", children=[ @@ -175,5 +178,5 @@ def _update_crossplot_graph( }, ), ], - ), - ) + ) + ] From 23515e66bc7a0fd7766f088d12eaa8c3e104235d Mon Sep 17 00:00:00 2001 From: "Jincheng Liu (OG SUB RPE)" Date: Tue, 2 Aug 2022 14:15:48 +0200 Subject: [PATCH 15/16] reformatting --- webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py index d690d5dd8..81c8e6e20 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_views/_crossplot.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Union import webviz_core_components as wcc from dash import Input, Output, callback, dcc From 54a2bdeb1970065415322c27d2b52883886940b0 Mon Sep 17 00:00:00 2001 From: JinCheng2022 <107854035+JinCheng2022@users.noreply.github.com> Date: Thu, 4 Aug 2022 12:31:10 +0200 Subject: [PATCH 16/16] set stretch to true --- webviz_subsurface/plugins/_seismic_misfit/_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_seismic_misfit/_plugin.py b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py index f41684340..3b850e663 100644 --- a/webviz_subsurface/plugins/_seismic_misfit/_plugin.py +++ b/webviz_subsurface/plugins/_seismic_misfit/_plugin.py @@ -127,7 +127,7 @@ def __init__( polygon: str = None, realrange: List[List[int]] = None, ) -> None: - super().__init__() + super().__init__(stretch=True) self.attributes = attributes