webviz-subsurface 0.2.36__py3-none-any.whl → 0.2.37__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. webviz_subsurface/__init__.py +1 -1
  2. webviz_subsurface/_components/color_picker.py +1 -1
  3. webviz_subsurface/_providers/ensemble_polygon_provider/__init__.py +3 -0
  4. webviz_subsurface/_providers/ensemble_polygon_provider/_polygon_discovery.py +97 -0
  5. webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +226 -0
  6. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider.py +53 -0
  7. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider_factory.py +99 -0
  8. webviz_subsurface/_providers/ensemble_polygon_provider/polygon_server.py +125 -0
  9. webviz_subsurface/plugins/_co2_leakage/_plugin.py +531 -377
  10. webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
  11. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +169 -173
  12. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +329 -84
  13. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
  14. webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
  15. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
  16. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +189 -96
  17. webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
  18. webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
  19. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +29 -21
  20. webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
  21. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +30 -18
  22. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +805 -343
  23. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/METADATA +2 -2
  24. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/RECORD +30 -19
  25. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/WHEEL +1 -1
  26. /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
  27. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE +0 -0
  28. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE.chromedriver +0 -0
  29. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/entry_points.txt +0 -0
  30. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,147 @@
1
+ from typing import List, Optional, Union
2
+
3
+ import pandas as pd
4
+
5
+ from webviz_subsurface._providers import EnsembleTableProvider
6
+ from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
7
+ Co2MassScale,
8
+ Co2VolumeScale,
9
+ MenuOptions,
10
+ )
11
+
12
+
13
+ class ContainmentDataValidationError(Exception):
14
+ pass
15
+
16
+
17
+ class ContainmentDataProvider:
18
+ def __init__(self, table_provider: EnsembleTableProvider):
19
+ ContainmentDataProvider._validate(table_provider)
20
+ self._provider = table_provider
21
+ self._menu_options = ContainmentDataProvider._get_menu_options(self._provider)
22
+
23
+ @property
24
+ def menu_options(self) -> MenuOptions:
25
+ return self._menu_options
26
+
27
+ @property
28
+ def realizations(self) -> List[int]:
29
+ return self._provider.realizations()
30
+
31
+ def extract_dataframe(
32
+ self, realization: int, scale: Union[Co2MassScale, Co2VolumeScale]
33
+ ) -> pd.DataFrame:
34
+ df = self._provider.get_column_data(
35
+ self._provider.column_names(), [realization]
36
+ )
37
+ scale_factor = self._find_scale_factor(scale)
38
+ if scale_factor == 1.0:
39
+ return df
40
+ df["amount"] /= scale_factor
41
+ return df
42
+
43
+ def extract_condensed_dataframe(
44
+ self,
45
+ co2_scale: Union[Co2MassScale, Co2VolumeScale],
46
+ ) -> pd.DataFrame:
47
+ df = self._provider.get_column_data(self._provider.column_names())
48
+ df = df.loc[
49
+ (df["zone"] == "all")
50
+ & (df["region"] == "all")
51
+ & (df["plume_group"] == "all")
52
+ ]
53
+ if co2_scale == Co2MassScale.MTONS:
54
+ df.loc[:, "amount"] /= 1e9
55
+ elif co2_scale == Co2MassScale.NORMALIZE:
56
+ df.loc[:, "amount"] /= df["amount"].max()
57
+ return df
58
+
59
+ def _find_scale_factor(
60
+ self,
61
+ scale: Union[Co2MassScale, Co2VolumeScale],
62
+ ) -> float:
63
+ if scale == Co2MassScale.KG:
64
+ return 0.001
65
+ if scale in (Co2MassScale.TONS, Co2VolumeScale.CUBIC_METERS):
66
+ return 1.0
67
+ if scale == Co2MassScale.MTONS:
68
+ return 1e6
69
+ if scale == Co2VolumeScale.BILLION_CUBIC_METERS:
70
+ return 1e9
71
+ if scale in (Co2MassScale.NORMALIZE, Co2VolumeScale.NORMALIZE):
72
+ df = self._provider.get_column_data(["amount"])
73
+ return df["amount"].max()
74
+ return 1.0
75
+
76
+ @staticmethod
77
+ def _get_menu_options(provider: EnsembleTableProvider) -> MenuOptions:
78
+ col_names = provider.column_names()
79
+ realization = provider.realizations()[0]
80
+ # NBNB: Check that these are the same for all realizations????
81
+ # NBNB: WARNING and empty for zones / regions, and Error if phases are different?
82
+ df = provider.get_column_data(col_names, [realization])
83
+ zones = ["all"]
84
+ if "zone" in df:
85
+ for zone in list(df["zone"]):
86
+ if zone not in zones:
87
+ zones.append(zone)
88
+ regions = ["all"]
89
+ if "region" in df:
90
+ for region in list(df["region"]):
91
+ if region not in regions:
92
+ regions.append(region)
93
+ plume_groups = ["all"]
94
+ if "plume_group" in df:
95
+ for plume_group in list(df["plume_group"]):
96
+ if plume_group not in plume_groups and plume_group is not None:
97
+ plume_groups.append(plume_group)
98
+
99
+ def plume_sort_key(name: Optional[str]) -> int:
100
+ if name is None:
101
+ return 999 # Not sure why/when this can happen, just a precaution
102
+ if name == "undetermined":
103
+ return 998
104
+ return name.count("+")
105
+
106
+ plume_groups = sorted(plume_groups, key=plume_sort_key)
107
+
108
+ if "free_gas" in list(df["phase"]):
109
+ phases = ["total", "free_gas", "trapped_gas", "dissolved"]
110
+ else:
111
+ phases = ["total", "gas", "dissolved"]
112
+
113
+ dates = df["date"].unique()
114
+ dates.sort()
115
+
116
+ return {
117
+ "zones": zones if len(zones) > 1 else [],
118
+ "regions": regions if len(regions) > 1 else [],
119
+ "phases": phases,
120
+ "plume_groups": plume_groups if len(plume_groups) > 1 else [],
121
+ "dates": dates,
122
+ }
123
+
124
+ @staticmethod
125
+ def _validate(provider: EnsembleTableProvider) -> None:
126
+ col_names = provider.column_names()
127
+ required_columns = [
128
+ "date",
129
+ "amount",
130
+ "phase",
131
+ "containment",
132
+ "zone",
133
+ "region",
134
+ "plume_group",
135
+ ]
136
+ missing_columns = [col for col in required_columns if col not in col_names]
137
+ realization = provider.realizations()[0]
138
+ if len(missing_columns) == 0:
139
+ return
140
+ raise ContainmentDataValidationError(
141
+ f"EnsembleTableProvider validation error for provider {provider} in "
142
+ f"realization {realization} (and possibly other csv-files).\n"
143
+ f" Expected columns: {', '.join(missing_columns)}\n"
144
+ f" Found columns: {', '.join(col_names)}\n"
145
+ f" (Missing columns: {', '.join(missing_columns)})"
146
+ f"Provided files are possibly from an outdated version of ccs-scripts?"
147
+ )
@@ -0,0 +1,105 @@
1
+ import logging
2
+ from functools import cached_property
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from webviz_subsurface._utils.webvizstore_functions import read_csv
7
+ from webviz_subsurface.plugins._co2_leakage._utilities._misc import realization_paths
8
+ from webviz_subsurface.plugins._map_viewer_fmu._tmp_well_pick_provider import (
9
+ WellPickProvider,
10
+ )
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class EnsembleWellPicks:
16
+ def __init__(
17
+ self,
18
+ ens_path: str,
19
+ well_picks_path: str,
20
+ map_surface_names_to_well_pick_names: Optional[Dict[str, str]],
21
+ ):
22
+ self._absolute_well_pick_provider: Optional[WellPickProvider] = None
23
+ self._per_real_well_pick_providers: Dict[int, WellPickProvider] = {}
24
+
25
+ if Path(well_picks_path).is_absolute():
26
+ self._absolute_well_pick_provider = _try_get_well_pick_provider(
27
+ read_csv(well_picks_path),
28
+ map_surface_names_to_well_pick_names,
29
+ )
30
+ else:
31
+ realizations = realization_paths(ens_path)
32
+ for r, r_path in realizations.items():
33
+ try:
34
+ self._per_real_well_pick_providers[r] = WellPickProvider(
35
+ read_csv(r_path / well_picks_path),
36
+ map_surface_names_to_well_pick_names,
37
+ )
38
+ except (FileNotFoundError, OSError) as e:
39
+ LOGGER.warning(
40
+ f"Failed to find well picks for realization {r} at {r_path}: {e}"
41
+ )
42
+
43
+ @cached_property
44
+ def well_names(self) -> List[str]:
45
+ if self._absolute_well_pick_provider is not None:
46
+ return self._absolute_well_pick_provider.well_names()
47
+
48
+ return list(
49
+ dict.fromkeys(
50
+ w
51
+ for v in self._per_real_well_pick_providers.values()
52
+ for w in v.well_names()
53
+ ).keys()
54
+ )
55
+
56
+ def geojson_layer(
57
+ self, realization: int, selected_wells: List[str], formation: str
58
+ ) -> Optional[Dict[str, Any]]:
59
+ if self._absolute_well_pick_provider is not None:
60
+ wpp = self._absolute_well_pick_provider
61
+ elif realization in self._per_real_well_pick_providers:
62
+ wpp = self._per_real_well_pick_providers[realization]
63
+ else:
64
+ return None
65
+
66
+ well_data = dict(wpp.get_geojson(selected_wells, formation))
67
+ if "features" in well_data:
68
+ if len(well_data["features"]) == 0:
69
+ wellstring = "well: " if len(selected_wells) == 1 else "wells: "
70
+ wellstring += ", ".join(selected_wells)
71
+ LOGGER.warning(
72
+ f"Combination of formation: {formation} and "
73
+ f"{wellstring} not found in well picks file."
74
+ )
75
+ for i in range(len(well_data["features"])):
76
+ current_attribute = well_data["features"][i]["properties"]["attribute"]
77
+ well_data["features"][i]["properties"]["attribute"] = (
78
+ " " + current_attribute
79
+ )
80
+
81
+ return {
82
+ "@@type": "GeoJsonLayer",
83
+ "name": "Well Picks",
84
+ "id": "well-picks-layer",
85
+ "data": well_data,
86
+ "visible": True,
87
+ "getText": "@@=properties.attribute",
88
+ "getTextSize": 12,
89
+ "getTextAnchor": "start",
90
+ "pointType": "circle+text",
91
+ "lineWidthMinPixels": 2,
92
+ "pointRadiusMinPixels": 2,
93
+ "pickable": True,
94
+ "parameters": {"depthTest": False},
95
+ }
96
+
97
+
98
+ def _try_get_well_pick_provider(
99
+ p: Path, name_mapping: Optional[Dict[str, str]]
100
+ ) -> Optional[WellPickProvider]:
101
+ try:
102
+ return WellPickProvider(read_csv(p), name_mapping)
103
+ except OSError as e:
104
+ LOGGER.warning(f"Failed to read well picks file '{p}': {e}")
105
+ return None
@@ -1,22 +1,145 @@
1
+ from __future__ import ( # Change to import Self from typing if we update to Python >3.11
2
+ annotations,
3
+ )
4
+
5
+ from typing import Dict, List, TypedDict
6
+
1
7
  from webviz_subsurface._utils.enum_shim import StrEnum
2
8
 
3
9
 
4
10
  class MapAttribute(StrEnum):
5
11
  MIGRATION_TIME_SGAS = "Migration time (SGAS)"
6
12
  MIGRATION_TIME_AMFG = "Migration time (AMFG)"
13
+ MIGRATION_TIME_XMF2 = "Migration time (XMF2)"
7
14
  MAX_SGAS = "Maximum SGAS"
8
15
  MAX_AMFG = "Maximum AMFG"
16
+ MAX_XMF2 = "Maximum XMF2"
17
+ MAX_SGSTRAND = "Maximum SGSTRAND"
18
+ MAX_SGTRH = "Maximum SGTRH"
9
19
  SGAS_PLUME = "Plume (SGAS)"
10
20
  AMFG_PLUME = "Plume (AMFG)"
21
+ XMF2_PLUME = "Plume (XMF2)"
22
+ SGSTRAND_PLUME = "Plume (SGSTRAND)"
23
+ SGTRH_PLUME = "Plume (SGTRH)"
11
24
  MASS = "Mass"
12
25
  DISSOLVED = "Dissolved mass"
13
- FREE = "Free mass"
26
+ FREE = "Free gas mass"
27
+ FREE_GAS = "Free gas phase mass"
28
+ TRAPPED_GAS = "Trapped gas phase mass"
29
+
30
+
31
+ class MapGroup(StrEnum):
32
+ MIGRATION_TIME_SGAS = "SGAS"
33
+ MIGRATION_TIME_AMFG = "AMFG"
34
+ MIGRATION_TIME_XMF2 = "XMF2"
35
+ MAX_SGAS = "SGAS"
36
+ MAX_AMFG = "AMFG"
37
+ MAX_XMF2 = "XMF2"
38
+ MAX_SGSTRAND = "SGSTRAND"
39
+ MAX_SGTRH = "SGTRH"
40
+ SGAS_PLUME = "SGAS"
41
+ AMFG_PLUME = "AMFG"
42
+ XMF2_PLUME = "XMF2"
43
+ SGSTRAND_PLUME = "SGSTRAND"
44
+ SGTRH_PLUME = "SGTRH"
45
+ MASS = "CO2 MASS"
46
+ DISSOLVED = "CO2 MASS"
47
+ FREE = "CO2 MASS"
48
+ FREE_GAS = "CO2 MASS"
49
+ TRAPPED_GAS = "CO2 MASS"
50
+
51
+
52
+ map_group_labels = {
53
+ "SGAS": "Gas phase",
54
+ "AMFG": "Dissolved phase",
55
+ "XMF2": "Dissolved phase",
56
+ "SGSTRAND": "Trapped gas phase",
57
+ "SGTRH": "Trapped gas phase",
58
+ "CO2 MASS": "CO2 mass",
59
+ }
60
+
61
+
62
+ class MapType(StrEnum):
63
+ MIGRATION_TIME_SGAS = "MIGRATION_TIME"
64
+ MIGRATION_TIME_AMFG = "MIGRATION_TIME"
65
+ MIGRATION_TIME_XMF2 = "MIGRATION_TIME"
66
+ MAX_SGAS = "MAX"
67
+ MAX_AMFG = "MAX"
68
+ MAX_XMF2 = "MAX"
69
+ MAX_SGSTRAND = "MAX"
70
+ MAX_SGTRH = "MAX"
71
+ SGAS_PLUME = "PLUME"
72
+ AMFG_PLUME = "PLUME"
73
+ XMF2_PLUME = "PLUME"
74
+ SGSTRAND_PLUME = "PLUME"
75
+ SGTRH_PLUME = "PLUME"
76
+ MASS = "MASS"
77
+ DISSOLVED = "MASS"
78
+ FREE = "MASS"
79
+ FREE_GAS = "MASS"
80
+ TRAPPED_GAS = "MASS"
81
+
82
+
83
+ class MapNamingConvention(StrEnum):
84
+ MIGRATION_TIME_SGAS = "migrationtime_sgas"
85
+ MIGRATION_TIME_AMFG = "migrationtime_amfg"
86
+ MIGRATION_TIME_XMF2 = "migrationtime_xmf2"
87
+ MAX_SGAS = "max_sgas"
88
+ MAX_AMFG = "max_amfg"
89
+ MAX_XMF2 = "max_xmf2"
90
+ MAX_SGSTRAND = "max_sgstrand"
91
+ MAX_SGTRH = "max_sgtrh"
92
+ MASS = "co2_mass_total"
93
+ DISSOLVED = "co2_mass_dissolved_phase"
94
+ FREE = "co2_mass_gas_phase"
95
+ FREE_GAS = "co2_mass_free_gas_phase"
96
+ TRAPPED_GAS = "co2_mass_trapped_gas_phase"
97
+
98
+
99
+ class FilteredMapAttribute:
100
+ def __init__(self, mapping: Dict):
101
+ self.mapping = mapping
102
+ map_types = {
103
+ key: MapType[key].value
104
+ for key in MapAttribute.__members__
105
+ if MapAttribute[key].value in self.mapping
106
+ }
107
+ map_groups = {
108
+ key: MapGroup[key].value
109
+ for key in MapAttribute.__members__
110
+ if MapAttribute[key].value in self.mapping
111
+ }
112
+ map_attrs_with_plume = [
113
+ map_groups[key] for key, value in map_types.items() if value == "MAX"
114
+ ]
115
+ plume_request = {
116
+ f"Plume ({item})": f"{item.lower()}_plume" for item in map_attrs_with_plume
117
+ }
118
+ self.mapping.update(plume_request)
119
+ self.filtered_values = self.filter_map_attribute()
120
+
121
+ def filter_map_attribute(self) -> Dict:
122
+ return {
123
+ MapAttribute[key]: self.mapping[MapAttribute[key].value]
124
+ for key in MapAttribute.__members__
125
+ if MapAttribute[key].value in self.mapping
126
+ }
127
+
128
+ def __getitem__(self, key: MapAttribute) -> MapAttribute:
129
+ if isinstance(key, MapAttribute):
130
+ return self.filtered_values[key]
131
+ raise KeyError(f"Key must be a MapAttribute, " f"got {type(key)} instead.")
132
+
133
+ @property
134
+ def values(self) -> Dict:
135
+ return self.filtered_values
14
136
 
15
137
 
16
138
  class Co2MassScale(StrEnum):
17
139
  NORMALIZE = "Fraction"
140
+ KG = "kg"
141
+ TONS = "tons"
18
142
  MTONS = "M tons"
19
- KG = "Kg"
20
143
 
21
144
 
22
145
  class Co2VolumeScale(StrEnum):
@@ -42,6 +165,8 @@ class LayoutLabels(StrEnum):
42
165
  COMMON_SELECTIONS = "Options and global filters"
43
166
  FEEDBACK = "User feedback"
44
167
  VISUALIZATION_UPDATE = "Update threshold"
168
+ VISUALIZATION_THRESHOLDS = "Manage visualization filter"
169
+ ALL_REAL = "Select all"
45
170
 
46
171
 
47
172
  # pylint: disable=too-few-public-methods
@@ -56,6 +181,13 @@ class LayoutStyle:
56
181
  "background-color": "lightgrey",
57
182
  }
58
183
 
184
+ ALL_REAL_BUTTON = {
185
+ "marginLeft": "10px",
186
+ "height": "25px",
187
+ "line-height": "25px",
188
+ "background-color": "lightgrey",
189
+ }
190
+
59
191
  FEEDBACK_BUTTON = {
60
192
  "marginBottom": "10px",
61
193
  "width": "100%",
@@ -70,3 +202,39 @@ class LayoutStyle:
70
202
  "line-height": "30px",
71
203
  "background-color": "lightgrey",
72
204
  }
205
+
206
+ THRESHOLDS_BUTTON = {
207
+ "marginTop": "10px",
208
+ "width": "100%",
209
+ "height": "30px",
210
+ "line-height": "30px",
211
+ "padding": "0",
212
+ "background-color": "lightgrey",
213
+ }
214
+
215
+
216
+ class MenuOptions(TypedDict):
217
+ zones: List[str]
218
+ regions: List[str]
219
+ phases: List[str]
220
+ plume_groups: List[str]
221
+ dates: List[str]
222
+
223
+
224
+ class MapThresholds:
225
+ def __init__(self, mapping: FilteredMapAttribute):
226
+ self.standard_thresholds = {
227
+ MapAttribute[key.name].value: 0.0
228
+ for key in mapping.filtered_values.keys()
229
+ if MapType[MapAttribute[key.name].name].value
230
+ not in ["PLUME", "MIGRATION_TIME"]
231
+ }
232
+ if MapAttribute.MAX_AMFG in self.standard_thresholds.keys():
233
+ self.standard_thresholds[MapAttribute.MAX_AMFG] = 0.0005
234
+
235
+
236
+ class BoundarySettings(TypedDict):
237
+ polygon_file_pattern: str
238
+ attribute: str
239
+ hazardous_name: str
240
+ containment_name: str