webviz-subsurface 0.2.43__py3-none-any.whl → 0.2.46__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 (21) hide show
  1. webviz_subsurface/_datainput/well_completions.py +1 -1
  2. webviz_subsurface/_models/gruptree_model.py +31 -7
  3. webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +2 -1
  4. webviz_subsurface/_version.py +2 -2
  5. webviz_subsurface/plugins/_co2_migration/_plugin.py +31 -8
  6. webviz_subsurface/plugins/_co2_migration/_utilities/callbacks.py +183 -19
  7. webviz_subsurface/plugins/_co2_migration/_utilities/co2volume.py +83 -62
  8. webviz_subsurface/plugins/_co2_migration/_utilities/containment_data_provider.py +6 -0
  9. webviz_subsurface/plugins/_co2_migration/_utilities/generic.py +40 -3
  10. webviz_subsurface/plugins/_co2_migration/_utilities/initialization.py +2 -1
  11. webviz_subsurface/plugins/_co2_migration/_utilities/polygon_handler.py +4 -0
  12. webviz_subsurface/plugins/_co2_migration/views/mainview/settings.py +38 -3
  13. webviz_subsurface/plugins/_simulation_time_series/_views/_subplot_view/_property_serialization/ensemble_subplot_builder.py +3 -1
  14. webviz_subsurface/plugins/_vfp_analysis/_utils/_vfp_data_model.py +1 -1
  15. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/METADATA +1 -2
  16. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/RECORD +20 -21
  17. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/WHEEL +1 -1
  18. webviz_subsurface-0.2.43.dist-info/licenses/LICENSE.chromedriver +0 -6291
  19. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/entry_points.txt +0 -0
  20. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/licenses/LICENSE +0 -0
  21. {webviz_subsurface-0.2.43.dist-info → webviz_subsurface-0.2.46.dist-info}/top_level.txt +0 -0
@@ -69,7 +69,7 @@ def read_zone_layer_mapping(
69
69
  )
70
70
  df_real["K1"] = df_real.index
71
71
  df_real["REAL"] = real
72
- zonelist = remove_invalid_colors(zonelist)
72
+ zonelist = remove_invalid_colors(zonelist) if zonelist else []
73
73
  zone_color_mapping = {
74
74
  zonedict["name"]: zonedict["color"]
75
75
  for zonedict in zonelist
@@ -191,16 +191,13 @@ GruptreeDataModel({self._ens_name!r}, {self._ens_path!r}, {self._gruptree_file!r
191
191
  raise ValueError(
192
192
  f"Keyword {self._tree_type.value} not found in {row['FULLPATH']}"
193
193
  )
194
+
195
+ # Only filter if both tree types are present
194
196
  if (
195
- self._tree_type == TreeType.GRUPTREE
197
+ TreeType.GRUPTREE.value in unique_keywords
196
198
  and TreeType.BRANPROP.value in unique_keywords
197
199
  ):
198
- # Filter out BRANPROP entries
199
- df_real = df_real[df_real["KEYWORD"] != TreeType.BRANPROP.value]
200
-
201
- if self._tree_type == TreeType.BRANPROP:
202
- # Filter out GRUPTREE entries
203
- df_real = df_real[df_real["KEYWORD"] != TreeType.GRUPTREE.value]
200
+ df_real = self._filter_by_tree_type(df_real, self._tree_type)
204
201
 
205
202
  if (
206
203
  i > 0
@@ -222,3 +219,30 @@ GruptreeDataModel({self._ens_name!r}, {self._ens_path!r}, {self._gruptree_file!r
222
219
  df["DATE"] = pd.to_datetime(df["DATE"])
223
220
 
224
221
  return df.where(pd.notnull(df), None)
222
+
223
+ @staticmethod
224
+ def _filter_by_tree_type(df: pd.DataFrame, tree_type: TreeType) -> pd.DataFrame:
225
+ """
226
+ Filter dataframe to include only dates where the selected tree type is defined,
227
+ and only include WELSPECS nodes that belong to the selected tree type.
228
+
229
+ A WELSPECS node belongs to a tree if its parent node exists in that tree's definition.
230
+ """
231
+ # Find dates where the selected tree type is defined
232
+ dates_with_tree_type = df[df["KEYWORD"] == tree_type.value]["DATE"].unique()
233
+
234
+ # Filter to only include rows from those dates
235
+ df = df[df["DATE"].isin(dates_with_tree_type)].copy()
236
+
237
+ # Get all nodes that are defined in the selected tree type (across all dates)
238
+ tree_nodes = set(df[df["KEYWORD"] == tree_type.value]["CHILD"].unique())
239
+
240
+ # Keep rows that are:
241
+ # 1. The selected tree type itself (GRUPTREE or BRANPROP)
242
+ # 2. WELSPECS whose parent exists in the selected tree type
243
+ is_selected_tree = df["KEYWORD"] == tree_type.value
244
+ is_welspecs_with_valid_parent = (df["KEYWORD"] == "WELSPECS") & df[
245
+ "PARENT"
246
+ ].isin(tree_nodes)
247
+
248
+ return df[is_selected_tree | is_welspecs_with_valid_parent].copy()
@@ -33,7 +33,8 @@ class Col:
33
33
 
34
34
  class PolygonType(StrEnum):
35
35
  SIMULATED = "simulated"
36
- HAZARDUOUS_BOUNDARY = "hazarduous_boundary"
36
+ HAZARDOUS_BOUNDARY = "hazardous_boundary" # Keep for backward compatibility
37
+ NOGO_BOUNDARY = "nogo_boundary"
37
38
  CONTAINMENT_BOUNDARY = "containment_boundary"
38
39
 
39
40
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.43'
32
- __version_tuple__ = version_tuple = (0, 2, 43)
31
+ __version__ = version = '0.2.46'
32
+ __version_tuple__ = version_tuple = (0, 2, 46)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -40,6 +40,8 @@ from webviz_subsurface.plugins._co2_migration._utilities.generic import (
40
40
  MapAttribute,
41
41
  MapThresholds,
42
42
  MapType,
43
+ check_hazardous_polygon,
44
+ deactivate_polygon_warnings,
43
45
  )
44
46
  from webviz_subsurface.plugins._co2_migration._utilities.initialization import (
45
47
  init_containment_data_providers,
@@ -95,7 +97,7 @@ class CO2Migration(WebvizPluginABC):
95
97
  * **`map_surface_names_to_fault_polygons`:** Mapping between surface map names and
96
98
  surface names used by the fault polygons
97
99
  * **`boundary_settings`:** Settings for polygons representing the containment and
98
- hazardous areas
100
+ nogo areas
99
101
  ---
100
102
 
101
103
  This plugin is tightly linked to the FMU CCS post-process available in the ccs-scripts
@@ -141,17 +143,17 @@ class CO2Migration(WebvizPluginABC):
141
143
  Similar for `map_surface_names_to_fault_polygons`.
142
144
 
143
145
  `boundary_settings` is the final override option, and it can be used to specify
144
- polygons representing the containment and hazardous areas. By default, the polygons are
146
+ polygons representing the containment and nogo areas. By default, the polygons are
145
147
  expected to be named:
146
148
  - `share/results/polygons/containment--boundary.csv`
147
- - `share/results/polygons/hazarduous--boundary.csv`
149
+ - `share/results/polygons/nogo--boundary.csv`
148
150
 
149
151
  This corresponds to the following input:
150
152
  ```
151
153
  boundary_settings:
152
154
  polygon_file_pattern: share/results/polygons/*.csv
153
155
  attribute: boundary
154
- hazardous_name: hazardous
156
+ nogo_name: nogo
155
157
  containment_name: containment
156
158
  ```
157
159
  All four settings are optional, and if not specified, the default values are used.
@@ -182,6 +184,8 @@ class CO2Migration(WebvizPluginABC):
182
184
  super().__init__()
183
185
  self._error_message = ""
184
186
  try:
187
+ deactivate_polygon_warnings()
188
+ check_hazardous_polygon(boundary_settings)
185
189
  ensemble_paths = {
186
190
  ensemble_name: webviz_settings.shared_settings["scratch_ensembles"][
187
191
  ensemble_name
@@ -435,6 +439,13 @@ class CO2Migration(WebvizPluginABC):
435
439
  "cm_max_val": Input(
436
440
  self._settings_component(ViewSettings.Ids.CM_MAX), "value"
437
441
  ),
442
+ "contour_switch": Input(
443
+ self._settings_component(ViewSettings.Ids.CONTOURS_SWITCH), "value"
444
+ ),
445
+ "contour_quantity": Input(
446
+ self._settings_component(ViewSettings.Ids.CONTOURS_QUANTITY),
447
+ "value",
448
+ ),
438
449
  "plume_threshold": Input(
439
450
  self._settings_component(ViewSettings.Ids.PLUME_THRESHOLD),
440
451
  "value",
@@ -480,12 +491,14 @@ class CO2Migration(WebvizPluginABC):
480
491
  cm_min_val: Optional[float],
481
492
  cm_max_auto: List[str],
482
493
  cm_max_val: Optional[float],
494
+ contour_switch: List[str],
495
+ contour_quantity: Optional[float],
483
496
  plume_threshold: Optional[float],
484
497
  plume_smoothing: Optional[float],
485
498
  visualization_update: int,
486
499
  mass_unit: str,
487
500
  mass_unit_update: int,
488
- options_dialog_options: List[int],
501
+ options_dialog_options: List[str],
489
502
  selected_wells: List[str],
490
503
  ensemble: str,
491
504
  current_views: List[Any],
@@ -545,13 +558,12 @@ class CO2Migration(WebvizPluginABC):
545
558
  map_attribute_names=self._map_attribute_names,
546
559
  )
547
560
  assert isinstance(self._visualization_info["unit"], str)
548
- surf_data, self._summed_co2 = process_summed_mass(
561
+ current_summed_mass, self._summed_co2 = process_summed_mass(
549
562
  formation,
550
563
  realization,
551
564
  datestr,
552
565
  attribute,
553
566
  summed_mass,
554
- surf_data,
555
567
  self._summed_co2,
556
568
  self._visualization_info["unit"],
557
569
  )
@@ -568,6 +580,9 @@ class CO2Migration(WebvizPluginABC):
568
580
  fault_polygon_url = self._fault_polygon_handlers[
569
581
  ensemble
570
582
  ].extract_fault_polygon_url(formation, realization)
583
+ nogo_polygon_url = self._polygon_handlers[ensemble].extract_nogo_poly_url(
584
+ realization
585
+ )
571
586
  hazardous_polygon_url = self._polygon_handlers[
572
587
  ensemble
573
588
  ].extract_hazardous_poly_url(realization)
@@ -580,11 +595,14 @@ class CO2Migration(WebvizPluginABC):
580
595
  surface_data=surf_data,
581
596
  fault_polygon_url=fault_polygon_url,
582
597
  containment_bounds_url=containment_polygon_url,
583
- haz_bounds_url=hazardous_polygon_url,
598
+ nogo_bounds_url=nogo_polygon_url,
599
+ hazardous_bounds_url=hazardous_polygon_url,
584
600
  well_pick_provider=self._well_pick_provider.get(ensemble, None),
585
601
  plume_extent_data=plume_polygon,
586
602
  options_dialog_options=options_dialog_options,
587
603
  selected_wells=selected_wells,
604
+ show_contours=len(contour_switch) > 0,
605
+ num_contours=contour_quantity,
588
606
  )
589
607
  annotations = create_map_annotations(
590
608
  formation=formation,
@@ -592,6 +610,11 @@ class CO2Migration(WebvizPluginABC):
592
610
  colortables=self._color_tables,
593
611
  attribute=attribute,
594
612
  unit=self._visualization_info["unit"],
613
+ current_total=current_summed_mass,
614
+ options=options_dialog_options,
615
+ con_url=containment_polygon_url,
616
+ haz_url=hazardous_polygon_url,
617
+ nogo_url=nogo_polygon_url,
595
618
  )
596
619
  viewports = no_update if current_views else create_map_viewports()
597
620
  return layers, annotations, viewports
@@ -7,7 +7,7 @@ import geojson
7
7
  import numpy as np
8
8
  import plotly.graph_objects as go
9
9
  import webviz_subsurface_components as wsc
10
- from dash import dcc, no_update
10
+ from dash import dcc, html, no_update
11
11
  from flask_caching import Cache
12
12
 
13
13
  from webviz_subsurface._providers import (
@@ -234,12 +234,127 @@ def _find_legend_title(attribute: MapAttribute, unit: str) -> str:
234
234
  return ""
235
235
 
236
236
 
237
+ def _create_summed_mass_annotation(
238
+ attribute: MapAttribute,
239
+ summed_mass: Optional[float],
240
+ unit: str,
241
+ ) -> Union[str, tuple]:
242
+ annotation = (
243
+ html.P(
244
+ [
245
+ f"Total {MapAttribute[attribute.name].value.lower()}:",
246
+ html.Br(),
247
+ f"{summed_mass:.2f} {unit}",
248
+ ]
249
+ )
250
+ if MapType[attribute.name].value == "MASS" and summed_mass is not None
251
+ else ""
252
+ )
253
+ return html.Div(
254
+ annotation,
255
+ style={
256
+ "position": "absolute",
257
+ "top": "210px",
258
+ "right": "4px",
259
+ "backgroundColor": "rgba(255,255,255,0.8)",
260
+ "padding": "1px 1px",
261
+ "fontWeight": "bold",
262
+ "fontSize": "15px",
263
+ "display": "block",
264
+ },
265
+ )
266
+
267
+
268
+ def _create_polygon_legend(
269
+ options: List[str],
270
+ con_url: Optional[str],
271
+ haz_url: Optional[str], # Keep for backward compatibility
272
+ nogo_url: Optional[str],
273
+ ) -> List:
274
+ legend: List = []
275
+ hide_con = con_url is None or LayoutLabels.SHOW_CONTAINMENT_POLYGON not in options
276
+ hide_nogo = (
277
+ nogo_url is None and haz_url is None
278
+ ) or LayoutLabels.SHOW_NOGO_POLYGON not in options
279
+ outline = LayoutLabels.SHOW_POLYGONS_AS_OUTLINES in options
280
+ if hide_con and hide_nogo:
281
+ return legend
282
+ legend_items = []
283
+ square = {"width": "12px", "height": "12px", "marginRight": "6px"}
284
+ text = {"color": "black", "fontSize": "14px"}
285
+ if not hide_con:
286
+ legend_items.append(
287
+ html.Div(
288
+ style={"display": "flex", "alignItems": "center"},
289
+ children=[
290
+ html.Div(
291
+ style={
292
+ **square,
293
+ "backgroundColor": "transparent"
294
+ if outline
295
+ else "rgba(0, 172, 0, 0.47)",
296
+ "border": "3px solid rgba(0, 172, 0, 0.70)"
297
+ if outline
298
+ else "transparent",
299
+ }
300
+ ),
301
+ html.Div("Containment Polygon", style=text),
302
+ ],
303
+ )
304
+ )
305
+ if not hide_nogo:
306
+ legend_items.append(
307
+ html.Div(
308
+ style={"display": "flex", "alignItems": "center"},
309
+ children=[
310
+ html.Div(
311
+ style={
312
+ **square,
313
+ "backgroundColor": "transparent"
314
+ if outline
315
+ else "rgba(200, 0, 0, 0.47)",
316
+ "border": "3px solid rgba(200, 0, 0, 0.70)"
317
+ if outline
318
+ else "transparent",
319
+ }
320
+ ),
321
+ html.Div("No-go Polygon", style=text),
322
+ ],
323
+ )
324
+ )
325
+ legend.append(
326
+ wsc.ViewAnnotation(
327
+ id="polygon_legends",
328
+ children=html.Div(
329
+ children=legend_items,
330
+ style={
331
+ "position": "absolute",
332
+ "top": "50px",
333
+ "left": "4px",
334
+ "backgroundColor": "rgba(255,255,255,0.9)",
335
+ "padding": "6px 8px",
336
+ "borderRadius": "4px",
337
+ "boxShadow": "0 0 4px rgba(0,0,0,0.2)",
338
+ "zIndex": 10,
339
+ },
340
+ ),
341
+ )
342
+ )
343
+ return legend
344
+
345
+
346
+ # pylint: disable=too-many-arguments, too-many-locals
237
347
  def create_map_annotations(
238
348
  formation: str,
239
349
  surface_data: Optional[SurfaceData],
240
350
  colortables: List[Dict[str, Any]],
241
351
  attribute: MapAttribute,
242
352
  unit: str,
353
+ current_total: Optional[float],
354
+ options: List[str],
355
+ con_url: Optional[str],
356
+ haz_url: Optional[str],
357
+ nogo_url: Optional[str],
243
358
  ) -> List[wsc.ViewAnnotation]:
244
359
  annotations = []
245
360
  if (
@@ -268,7 +383,9 @@ def create_map_annotations(
268
383
  colorTables=colortables,
269
384
  ),
270
385
  wsc.ViewFooter(children=formation),
271
- ],
386
+ _create_summed_mass_annotation(attribute, current_total, unit),
387
+ ]
388
+ + _create_polygon_legend(options, con_url, haz_url, nogo_url),
272
389
  )
273
390
  )
274
391
  return annotations
@@ -286,7 +403,8 @@ def create_map_viewports() -> Dict:
286
403
  "colormap-layer",
287
404
  "fault-polygons-layer",
288
405
  "license-boundary-layer",
289
- "hazardous-boundary-layer",
406
+ "nogo-boundary-layer",
407
+ "hazardous-boundary-layer", # Keep for backward compatibility
290
408
  "well-picks-layer",
291
409
  "plume-polygon-layer",
292
410
  ],
@@ -302,16 +420,35 @@ def create_map_layers(
302
420
  surface_data: Optional[SurfaceData],
303
421
  fault_polygon_url: Optional[str],
304
422
  containment_bounds_url: Optional[str],
305
- haz_bounds_url: Optional[str],
423
+ nogo_bounds_url: Optional[str],
424
+ hazardous_bounds_url: Optional[str], # Keep for backward compatibility
306
425
  well_pick_provider: Optional[EnsembleWellPicks],
307
426
  plume_extent_data: Optional[geojson.FeatureCollection],
308
- options_dialog_options: List[int],
427
+ options_dialog_options: List[str],
309
428
  selected_wells: List[str],
429
+ show_contours: bool,
430
+ num_contours: Optional[float],
310
431
  ) -> List[Dict]:
311
432
  layers = []
433
+ outline = LayoutLabels.SHOW_POLYGONS_AS_OUTLINES in options_dialog_options
312
434
  if surface_data is not None:
313
435
  # Update ColormapLayer
314
436
  meta = surface_data.meta_data
437
+
438
+ # Generate contour lines
439
+ contours = []
440
+ if (
441
+ show_contours
442
+ and surface_data.color_map_range[0] is not None
443
+ and surface_data.color_map_range[1] is not None
444
+ and num_contours is not None
445
+ ):
446
+ min_val, max_val = surface_data.color_map_range
447
+ assert min_val is not None and max_val is not None
448
+ buffer = 0.01 * (max_val - min_val) # Strange effects at min_val/max_val
449
+ step = (max_val - min_val + 2 * buffer) / (np.round(num_contours) + 1)
450
+ contours = [min_val + step - buffer, step]
451
+
315
452
  layers.append(
316
453
  {
317
454
  "@@type": "MapLayer",
@@ -327,6 +464,7 @@ def create_map_layers(
327
464
  "colorMapName": surface_data.color_map_name,
328
465
  "colorMapRange": surface_data.color_map_range,
329
466
  "material": False,
467
+ "contours": contours,
330
468
  }
331
469
  )
332
470
 
@@ -353,24 +491,52 @@ def create_map_layers(
353
491
  "name": "Containment Polygon",
354
492
  "id": "license-boundary-layer",
355
493
  "data": containment_bounds_url,
356
- "stroked": False,
494
+ "stroked": outline,
495
+ "filled": not outline,
357
496
  "getFillColor": [0, 172, 0, 120],
497
+ "getLineColor": [0, 172, 0, 120],
498
+ "getLineWidth": 3,
499
+ "lineWidthUnits": "pixels",
500
+ "visible": True,
501
+ }
502
+ )
503
+
504
+ if (
505
+ nogo_bounds_url is not None
506
+ and LayoutLabels.SHOW_NOGO_POLYGON in options_dialog_options
507
+ ):
508
+ layers.append(
509
+ {
510
+ "@@type": "GeoJsonLayer",
511
+ "name": "No-go Polygon",
512
+ "id": "nogo-boundary-layer",
513
+ "data": nogo_bounds_url,
514
+ "stroked": outline,
515
+ "filled": not outline,
516
+ "getFillColor": [200, 0, 0, 120],
517
+ "getLineColor": [200, 0, 0, 180],
518
+ "getLineWidth": 3,
519
+ "lineWidthUnits": "pixels",
358
520
  "visible": True,
359
521
  }
360
522
  )
361
523
 
362
524
  if (
363
- haz_bounds_url is not None
364
- and LayoutLabels.SHOW_HAZARDOUS_POLYGON in options_dialog_options
525
+ hazardous_bounds_url is not None
526
+ and LayoutLabels.SHOW_NOGO_POLYGON in options_dialog_options
365
527
  ):
366
528
  layers.append(
367
529
  {
368
530
  "@@type": "GeoJsonLayer",
369
- "name": "Hazardous Polygon",
531
+ "name": "No-go Polygon",
370
532
  "id": "hazardous-boundary-layer",
371
- "data": haz_bounds_url,
372
- "stroked": False,
533
+ "data": hazardous_bounds_url,
534
+ "stroked": outline,
535
+ "filled": not outline,
373
536
  "getFillColor": [200, 0, 0, 120],
537
+ "getLineColor": [200, 0, 0, 180],
538
+ "getLineWidth": 3,
539
+ "lineWidthUnits": "pixels",
374
540
  "visible": True,
375
541
  }
376
542
  )
@@ -551,7 +717,7 @@ def process_containment_info(
551
717
  mark_choice=mark_choice,
552
718
  sorting=sorting,
553
719
  phases=[phase for phase in menu_options["phases"] if phase != "total"],
554
- containments=["hazardous", "outside", "contained"],
720
+ containments=["nogo", "outside", "contained"],
555
721
  plume_groups=plume_groups,
556
722
  use_stats=lines_to_show == "stat",
557
723
  date_option=date_option,
@@ -631,20 +797,18 @@ def process_summed_mass(
631
797
  datestr: Optional[str],
632
798
  attribute: MapAttribute,
633
799
  summed_mass: Optional[float],
634
- surf_data: Optional[SurfaceData],
635
800
  summed_co2: Dict[str, float],
636
801
  unit: str,
637
- ) -> Tuple[Optional[SurfaceData], Dict[str, float]]:
802
+ ) -> Tuple[Optional[float], Dict[str, float]]:
638
803
  summed_co2_key = f"{formation}-{realization[0]}-{datestr}-{attribute}-{unit}"
804
+ current_total = None
639
805
  if len(realization) == 1:
640
806
  if MapType[MapAttribute(attribute).name].value == "MASS":
641
807
  if summed_mass is not None and summed_co2_key not in summed_co2:
642
808
  summed_co2[summed_co2_key] = summed_mass
643
- if summed_co2_key in summed_co2 and surf_data is not None:
644
- surf_data.readable_name += (
645
- f" ({unit}) (Total: {summed_co2[summed_co2_key]:.2E}): "
646
- )
647
- return surf_data, summed_co2
809
+ if summed_co2_key in summed_co2:
810
+ current_total = summed_co2[summed_co2_key]
811
+ return current_total, summed_co2
648
812
 
649
813
 
650
814
  def export_figure_data_to_csv(