webviz-subsurface 0.2.35__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 (32) 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/_providers/ensemble_summary_provider/_provider_impl_arrow_lazy.py +1 -1
  10. webviz_subsurface/plugins/_co2_leakage/_plugin.py +531 -377
  11. webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
  12. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +169 -173
  13. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +329 -84
  14. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
  15. webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
  16. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
  17. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +189 -96
  18. webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
  19. webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
  20. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +29 -21
  21. webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
  22. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +30 -18
  23. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +805 -343
  24. webviz_subsurface/plugins/_relative_permeability.py +1 -1
  25. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/METADATA +2 -2
  26. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/RECORD +32 -21
  27. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/WHEEL +1 -1
  28. /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
  29. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE +0 -0
  30. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE.chromedriver +0 -0
  31. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/entry_points.txt +0 -0
  32. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+
4
+ from fmu.ensemble import ScratchEnsemble
5
+
6
+
7
+ def realization_paths(ens_path: str) -> Dict[int, Path]:
8
+ scratch_ensemble = ScratchEnsemble("_", paths=ens_path).filter("OK")
9
+ return {i: Path(r.runpath()) for i, r in scratch_ensemble.realizations.items()}
@@ -11,7 +11,6 @@ from flask_caching import Cache
11
11
 
12
12
  from webviz_subsurface._providers import (
13
13
  EnsembleSurfaceProvider,
14
- EnsembleTableProvider,
15
14
  SimulatedSurfaceAddress,
16
15
  StatisticalSurfaceAddress,
17
16
  SurfaceAddress,
@@ -21,19 +20,29 @@ from webviz_subsurface._providers import (
21
20
  from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_provider import (
22
21
  SurfaceStatistic,
23
22
  )
24
- from webviz_subsurface._utils.webvizstore_functions import read_csv
25
23
  from webviz_subsurface.plugins._co2_leakage._utilities import plume_extent
26
24
  from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import (
25
+ generate_co2_statistics_figure,
27
26
  generate_co2_time_containment_figure,
28
27
  generate_co2_time_containment_one_realization_figure,
29
28
  generate_co2_volume_figure,
30
29
  )
30
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
31
+ ContainmentDataProvider,
32
+ )
33
+ from webviz_subsurface.plugins._co2_leakage._utilities.ensemble_well_picks import (
34
+ EnsembleWellPicks,
35
+ )
31
36
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
32
37
  Co2MassScale,
33
38
  Co2VolumeScale,
39
+ FilteredMapAttribute,
34
40
  GraphSource,
35
41
  LayoutLabels,
36
42
  MapAttribute,
43
+ MapGroup,
44
+ MapType,
45
+ MenuOptions,
37
46
  )
38
47
  from webviz_subsurface.plugins._co2_leakage._utilities.summary_graphs import (
39
48
  generate_summary_figure,
@@ -42,21 +51,22 @@ from webviz_subsurface.plugins._co2_leakage._utilities.surface_publishing import
42
51
  TruncatedSurfaceAddress,
43
52
  publish_and_get_surface_metadata,
44
53
  )
45
- from webviz_subsurface.plugins._map_viewer_fmu._tmp_well_pick_provider import (
46
- WellPickProvider,
54
+ from webviz_subsurface.plugins._co2_leakage._utilities.unsmry_data_provider import (
55
+ UnsmryDataProvider,
47
56
  )
48
57
 
49
58
 
50
59
  def property_origin(
51
- attribute: MapAttribute, map_attribute_names: Dict[MapAttribute, str]
60
+ attribute: MapAttribute, map_attribute_names: FilteredMapAttribute
52
61
  ) -> str:
53
- if attribute in map_attribute_names:
54
- return map_attribute_names[attribute]
55
- if attribute == MapAttribute.SGAS_PLUME:
56
- return map_attribute_names[MapAttribute.MAX_SGAS]
57
- if attribute == MapAttribute.AMFG_PLUME:
58
- return map_attribute_names[MapAttribute.MAX_AMFG]
59
- raise AssertionError(f"Map attribute name not found for property: {attribute}")
62
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
63
+ return [
64
+ map_attribute_names[attr]
65
+ for attr in MapAttribute
66
+ if MapGroup[attr.name].value == MapGroup[MapAttribute(attribute).name].value
67
+ and MapType[attr.name] == "MAX"
68
+ ][0]
69
+ return map_attribute_names[attribute]
60
70
 
61
71
 
62
72
  @dataclass
@@ -77,7 +87,7 @@ class SurfaceData:
77
87
  color_map_name: str,
78
88
  readable_name_: str,
79
89
  visualization_info: Dict[str, Any],
80
- map_attribute_names: Dict[MapAttribute, str],
90
+ map_attribute_names: FilteredMapAttribute,
81
91
  ) -> Tuple[Any, Optional[Any]]:
82
92
  surf_meta, img_url, summed_mass = publish_and_get_surface_metadata(
83
93
  server,
@@ -115,17 +125,14 @@ def derive_surface_address(
115
125
  attribute: MapAttribute,
116
126
  date: Optional[str],
117
127
  realization: List[int],
118
- map_attribute_names: Dict[MapAttribute, str],
128
+ map_attribute_names: FilteredMapAttribute,
119
129
  statistic: str,
120
130
  contour_data: Optional[Dict[str, Any]],
121
131
  ) -> Union[SurfaceAddress, TruncatedSurfaceAddress]:
122
- if attribute in (MapAttribute.SGAS_PLUME, MapAttribute.AMFG_PLUME):
132
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
133
+ max_attr_name = f"MAX_{MapGroup[MapAttribute(attribute).name]}"
123
134
  assert date is not None
124
- basis = (
125
- MapAttribute.MAX_SGAS
126
- if attribute == MapAttribute.SGAS_PLUME
127
- else MapAttribute.MAX_AMFG
128
- )
135
+ basis = getattr(MapAttribute, max_attr_name)
129
136
  return TruncatedSurfaceAddress(
130
137
  name=surface_name,
131
138
  datestr=date,
@@ -136,11 +143,7 @@ def derive_surface_address(
136
143
  )
137
144
  date = (
138
145
  None
139
- if attribute
140
- in [
141
- MapAttribute.MIGRATION_TIME_SGAS,
142
- MapAttribute.MIGRATION_TIME_AMFG,
143
- ]
146
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME"
144
147
  else date
145
148
  )
146
149
  if len(realization) == 1:
@@ -161,12 +164,9 @@ def derive_surface_address(
161
164
 
162
165
  def readable_name(attribute: MapAttribute) -> str:
163
166
  unit = ""
164
- if attribute in [
165
- MapAttribute.MIGRATION_TIME_SGAS,
166
- MapAttribute.MIGRATION_TIME_AMFG,
167
- ]:
167
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
168
168
  unit = " [year]"
169
- elif attribute in (MapAttribute.AMFG_PLUME, MapAttribute.SGAS_PLUME):
169
+ elif MapType[MapAttribute(attribute).name].value == "PLUME":
170
170
  unit = " [# real.]"
171
171
  return f"{attribute.value}{unit}"
172
172
 
@@ -211,12 +211,9 @@ def get_plume_polygon(
211
211
 
212
212
 
213
213
  def _find_legend_title(attribute: MapAttribute, unit: str) -> str:
214
- if attribute in [
215
- MapAttribute.MIGRATION_TIME_SGAS,
216
- MapAttribute.MIGRATION_TIME_AMFG,
217
- ]:
214
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
218
215
  return "years"
219
- if attribute in [MapAttribute.MASS, MapAttribute.DISSOLVED, MapAttribute.FREE]:
216
+ if MapType[MapAttribute(attribute).name].value == "MASS":
220
217
  return unit
221
218
  return ""
222
219
 
@@ -234,7 +231,8 @@ def create_map_annotations(
234
231
  and surface_data.color_map_range[0] is not None
235
232
  and surface_data.color_map_range[1] is not None
236
233
  ):
237
- num_digits = np.ceil(np.log(surface_data.color_map_range[1]) / np.log(10))
234
+ max_value = surface_data.color_map_range[1]
235
+ num_digits = 4 if max_value < 1 else np.ceil(np.log(max_value) / np.log(10))
238
236
  numbersize = max((6, min((17 - num_digits, 11))))
239
237
  annotations.append(
240
238
  wsc.ViewAnnotation(
@@ -283,12 +281,13 @@ def create_map_viewports() -> Dict:
283
281
 
284
282
  # pylint: disable=too-many-arguments
285
283
  def create_map_layers(
284
+ realizations: List[int],
286
285
  formation: str,
287
286
  surface_data: Optional[SurfaceData],
288
287
  fault_polygon_url: Optional[str],
289
- file_containment_boundary: Optional[str],
290
- file_hazardous_boundary: Optional[str],
291
- well_pick_provider: Optional[WellPickProvider],
288
+ containment_bounds_url: Optional[str],
289
+ haz_bounds_url: Optional[str],
290
+ well_pick_provider: Optional[EnsembleWellPicks],
292
291
  plume_extent_data: Optional[geojson.FeatureCollection],
293
292
  options_dialog_options: List[int],
294
293
  selected_wells: List[str],
@@ -322,8 +321,9 @@ def create_map_layers(
322
321
  "data": fault_polygon_url,
323
322
  }
324
323
  )
324
+
325
325
  if (
326
- file_containment_boundary is not None
326
+ containment_bounds_url is not None
327
327
  and LayoutLabels.SHOW_CONTAINMENT_POLYGON in options_dialog_options
328
328
  ):
329
329
  layers.append(
@@ -331,14 +331,15 @@ def create_map_layers(
331
331
  "@@type": "GeoJsonLayer",
332
332
  "name": "Containment Polygon",
333
333
  "id": "license-boundary-layer",
334
- "data": _parse_polygon_file(file_containment_boundary),
334
+ "data": containment_bounds_url,
335
335
  "stroked": False,
336
336
  "getFillColor": [0, 172, 0, 120],
337
337
  "visible": True,
338
338
  }
339
339
  )
340
+
340
341
  if (
341
- file_hazardous_boundary is not None
342
+ haz_bounds_url is not None
342
343
  and LayoutLabels.SHOW_HAZARDOUS_POLYGON in options_dialog_options
343
344
  ):
344
345
  layers.append(
@@ -346,48 +347,25 @@ def create_map_layers(
346
347
  "@@type": "GeoJsonLayer",
347
348
  "name": "Hazardous Polygon",
348
349
  "id": "hazardous-boundary-layer",
349
- "data": _parse_polygon_file(file_hazardous_boundary),
350
+ "data": haz_bounds_url,
350
351
  "stroked": False,
351
352
  "getFillColor": [200, 0, 0, 120],
352
353
  "visible": True,
353
354
  }
354
355
  )
356
+
355
357
  if (
356
358
  well_pick_provider is not None
357
359
  and formation is not None
360
+ and len(realizations) > 0
358
361
  and LayoutLabels.SHOW_WELLS in options_dialog_options
359
362
  ):
360
- well_data = dict(well_pick_provider.get_geojson(selected_wells, formation))
361
- if "features" in well_data:
362
- if len(well_data["features"]) == 0:
363
- wellstring = "well: " if len(selected_wells) == 1 else "wells: "
364
- wellstring += ", ".join(selected_wells)
365
- warnings.warn(
366
- f"Combination of formation: {formation} and "
367
- f"{wellstring} not found in well picks file."
368
- )
369
- for i in range(len(well_data["features"])):
370
- current_attribute = well_data["features"][i]["properties"]["attribute"]
371
- well_data["features"][i]["properties"]["attribute"] = (
372
- " " + current_attribute
373
- )
374
- layers.append(
375
- {
376
- "@@type": "GeoJsonLayer",
377
- "name": "Well Picks",
378
- "id": "well-picks-layer",
379
- "data": well_data,
380
- "visible": True,
381
- "getText": "@@=properties.attribute",
382
- "getTextSize": 12,
383
- "getTextAnchor": "start",
384
- "pointType": "circle+text",
385
- "lineWidthMinPixels": 2,
386
- "pointRadiusMinPixels": 2,
387
- "pickable": True,
388
- "parameters": {"depthTest": False},
389
- }
363
+ layer = well_pick_provider.geojson_layer(
364
+ realizations[0], selected_wells, formation
390
365
  )
366
+ if layer is not None:
367
+ layers.append(layer)
368
+
391
369
  if plume_extent_data is not None:
392
370
  layers.append(
393
371
  {
@@ -403,28 +381,45 @@ def create_map_layers(
403
381
 
404
382
 
405
383
  def generate_containment_figures(
406
- table_provider: EnsembleTableProvider,
384
+ table_provider: ContainmentDataProvider,
407
385
  co2_scale: Union[Co2MassScale, Co2VolumeScale],
408
- realization: int,
386
+ realizations: List[int],
409
387
  y_limits: List[Optional[float]],
410
388
  containment_info: Dict[str, Union[str, None, List[str], int]],
411
389
  ) -> Tuple[go.Figure, go.Figure, go.Figure]:
412
390
  try:
413
- fig0 = generate_co2_volume_figure(
414
- table_provider,
415
- table_provider.realizations(),
416
- co2_scale,
417
- containment_info,
391
+ fig0 = (
392
+ no_update
393
+ if not containment_info["update_first_figure"]
394
+ else generate_co2_volume_figure(
395
+ table_provider,
396
+ table_provider.realizations,
397
+ co2_scale,
398
+ containment_info,
399
+ )
418
400
  )
419
- fig1 = generate_co2_time_containment_figure(
401
+ fig1 = (
402
+ generate_co2_time_containment_figure(
403
+ table_provider,
404
+ realizations,
405
+ co2_scale,
406
+ containment_info,
407
+ )
408
+ if len(realizations) > 1
409
+ else generate_co2_time_containment_one_realization_figure(
410
+ table_provider,
411
+ co2_scale,
412
+ realizations[0],
413
+ y_limits,
414
+ containment_info,
415
+ )
416
+ )
417
+ fig2 = generate_co2_statistics_figure(
420
418
  table_provider,
421
- table_provider.realizations(),
419
+ realizations,
422
420
  co2_scale,
423
421
  containment_info,
424
422
  )
425
- fig2 = generate_co2_time_containment_one_realization_figure(
426
- table_provider, co2_scale, realization, y_limits, containment_info
427
- )
428
423
  except KeyError as exc:
429
424
  warnings.warn(f"Could not generate CO2 figures: {exc}")
430
425
  raise exc
@@ -432,58 +427,20 @@ def generate_containment_figures(
432
427
 
433
428
 
434
429
  def generate_unsmry_figures(
435
- table_provider_unsmry: EnsembleTableProvider,
430
+ table_provider_unsmry: UnsmryDataProvider,
436
431
  co2_mass_scale: Union[Co2MassScale, Co2VolumeScale],
437
- table_provider_containment: EnsembleTableProvider,
438
- ) -> Tuple[go.Figure]:
439
- return (
440
- generate_summary_figure(
441
- table_provider_unsmry,
442
- table_provider_unsmry.realizations(),
443
- co2_mass_scale,
444
- table_provider_containment,
445
- table_provider_containment.realizations(),
446
- ),
432
+ table_provider_containment: ContainmentDataProvider,
433
+ ) -> go.Figure:
434
+ return generate_summary_figure(
435
+ table_provider_unsmry,
436
+ co2_mass_scale,
437
+ table_provider_containment,
447
438
  )
448
439
 
449
440
 
450
- def _parse_polygon_file(filename: str) -> Dict[str, Any]:
451
- df = read_csv(filename)
452
- if "x" in df.columns:
453
- xyz = df[["x", "y"]].values
454
- elif "X_UTME" in df.columns:
455
- if "POLY_ID" in df.columns:
456
- xyz = [gf[["X_UTME", "Y_UTMN"]].values for _, gf in df.groupby("POLY_ID")]
457
- else:
458
- xyz = df[["X_UTME", "Y_UTMN"]].values
459
- else:
460
- # Attempt to use the first two columns as the x and y coordinates
461
- xyz = df.values[:, :2]
462
- if isinstance(xyz, list):
463
- poly_type = "MultiPolygon"
464
- coords = [[arr.tolist()] for arr in xyz]
465
- else:
466
- poly_type = "Polygon"
467
- coords = [xyz.tolist()]
468
- as_geojson = {
469
- "type": "FeatureCollection",
470
- "features": [
471
- {
472
- "type": "Feature",
473
- "properties": {},
474
- "geometry": {
475
- "type": poly_type,
476
- "coordinates": coords,
477
- },
478
- }
479
- ],
480
- }
481
- return as_geojson
482
-
483
-
484
441
  def process_visualization_info(
485
- n_clicks: int,
486
- threshold: Optional[float],
442
+ attribute: str,
443
+ thresholds: dict,
487
444
  unit: str,
488
445
  stored_info: Dict[str, Any],
489
446
  cache: Cache,
@@ -491,17 +448,21 @@ def process_visualization_info(
491
448
  """
492
449
  Clear surface cache if the threshold for visualization or mass unit is changed
493
450
  """
451
+ stored_info["attribute"] = attribute
494
452
  stored_info["change"] = False
495
- stored_info["n_clicks"] = n_clicks
496
- if unit != stored_info["unit"]:
453
+ if (
454
+ MapType[MapAttribute(attribute).name].value not in ["PLUME", "MIGRATION_TIME"]
455
+ and unit != stored_info["unit"]
456
+ ):
497
457
  stored_info["unit"] = unit
498
458
  stored_info["change"] = True
499
- if threshold is not None and threshold != stored_info["threshold"]:
500
- stored_info["threshold"] = threshold
501
- stored_info["change"] = True
459
+ if thresholds is not None:
460
+ for att in stored_info["thresholds"].keys():
461
+ if stored_info["thresholds"][att] != thresholds[att]:
462
+ stored_info["change"] = True
463
+ stored_info["thresholds"][att] = thresholds[att]
502
464
  if stored_info["change"]:
503
465
  cache.clear()
504
- # stored_info["n_clicks"] = n_clicks
505
466
  return stored_info
506
467
 
507
468
 
@@ -510,21 +471,33 @@ def process_containment_info(
510
471
  region: Optional[str],
511
472
  phase: str,
512
473
  containment: str,
474
+ plume_group: str,
513
475
  color_choice: str,
514
476
  mark_choice: Optional[str],
515
477
  sorting: str,
516
- menu_options: Dict[str, List[str]],
478
+ lines_to_show: str,
479
+ date_option: str,
480
+ menu_options: MenuOptions,
517
481
  ) -> Dict[str, Union[str, None, List[str], int]]:
518
482
  if mark_choice is None:
519
483
  mark_choice = "phase"
520
484
  zones = menu_options["zones"]
521
485
  regions = menu_options["regions"]
486
+ plume_groups = menu_options["plume_groups"]
522
487
  if len(zones) > 0:
523
488
  zones = [zone_name for zone_name in zones if zone_name != "all"]
524
489
  if len(regions) > 0:
525
490
  regions = [reg_name for reg_name in regions if reg_name != "all"]
526
- containments = ["hazardous", "outside", "contained"]
527
- phases = [phase for phase in menu_options["phases"] if phase != "total"]
491
+ if len(plume_groups) > 0:
492
+ plume_groups = [pg_name for pg_name in plume_groups if pg_name != "all"]
493
+
494
+ def plume_sort_key(name: str) -> int:
495
+ if name == "undetermined":
496
+ return 999
497
+ return name.count("+")
498
+
499
+ plume_groups = sorted(plume_groups, key=plume_sort_key)
500
+
528
501
  if "zone" in [mark_choice, color_choice]:
529
502
  region = "all"
530
503
  if "region" in [mark_choice, color_choice]:
@@ -536,45 +509,72 @@ def process_containment_info(
536
509
  "regions": regions,
537
510
  "phase": phase,
538
511
  "containment": containment,
512
+ "plume_group": plume_group,
539
513
  "color_choice": color_choice,
540
514
  "mark_choice": mark_choice,
541
515
  "sorting": sorting,
542
- "phases": phases,
543
- "containments": containments,
516
+ "phases": [phase for phase in menu_options["phases"] if phase != "total"],
517
+ "containments": ["hazardous", "outside", "contained"],
518
+ "plume_groups": plume_groups,
519
+ "use_stats": lines_to_show == "stat",
520
+ "date_option": date_option,
544
521
  }
545
522
 
546
523
 
547
- def set_plot_ids(
548
- figs: List[go.Figure],
524
+ def make_plot_ids(
525
+ ensemble: str,
549
526
  source: GraphSource,
550
527
  scale: Union[Co2MassScale, Co2VolumeScale],
551
528
  containment_info: Dict,
552
529
  realizations: List[int],
553
- ) -> None:
554
- if figs[0] != no_update:
555
- zone_str = (
556
- containment_info["zone"] if containment_info["zone"] is not None else "None"
557
- )
558
- region_str = (
559
- containment_info["region"]
560
- if containment_info["region"] is not None
561
- else "None"
562
- )
563
- plot_id = "-".join(
564
- (
565
- source,
566
- scale,
567
- zone_str,
568
- region_str,
569
- str(containment_info["phase"]),
570
- str(containment_info["containment"]),
571
- containment_info["color_choice"],
572
- containment_info["mark_choice"],
573
- )
530
+ lines_to_show: str,
531
+ num_figs: int,
532
+ ) -> List[str]:
533
+ zone_str = (
534
+ containment_info["zone"] if containment_info["zone"] is not None else "None"
535
+ )
536
+ region_str = (
537
+ containment_info["region"] if containment_info["region"] is not None else "None"
538
+ )
539
+ plume_group_str = (
540
+ containment_info["plume_group"]
541
+ if containment_info["plume_group"] is not None
542
+ else "None"
543
+ )
544
+ mark_choice_str = (
545
+ containment_info["mark_choice"]
546
+ if containment_info["mark_choice"] is not None
547
+ else "None"
548
+ )
549
+ plot_id = "-".join(
550
+ (
551
+ ensemble,
552
+ source,
553
+ scale,
554
+ zone_str,
555
+ region_str,
556
+ plume_group_str,
557
+ str(containment_info["phase"]),
558
+ str(containment_info["containment"]),
559
+ containment_info["color_choice"],
560
+ mark_choice_str,
561
+ containment_info["sorting"],
562
+ containment_info["date_option"],
574
563
  )
575
- for fig in figs:
564
+ )
565
+ ids = [plot_id]
566
+ ids += [plot_id + f"-{realizations}"] * (num_figs - 1)
567
+ ids[1] += f"-{lines_to_show}"
568
+ return ids
569
+
570
+
571
+ def set_plot_ids(
572
+ figs: List[go.Figure],
573
+ plot_ids: List[str],
574
+ ) -> None:
575
+ for fig, plot_id in zip(figs, plot_ids):
576
+ if fig != no_update:
576
577
  fig["layout"]["uirevision"] = plot_id
577
- figs[-1]["layout"]["uirevision"] += f"-{realizations}"
578
578
 
579
579
 
580
580
  def process_summed_mass(
@@ -589,11 +589,7 @@ def process_summed_mass(
589
589
  ) -> Tuple[Optional[SurfaceData], Dict[str, float]]:
590
590
  summed_co2_key = f"{formation}-{realization[0]}-{datestr}-{attribute}-{unit}"
591
591
  if len(realization) == 1:
592
- if attribute in [
593
- MapAttribute.MASS,
594
- MapAttribute.DISSOLVED,
595
- MapAttribute.FREE,
596
- ]:
592
+ if MapType[MapAttribute(attribute).name].value == "MASS":
597
593
  if summed_mass is not None and summed_co2_key not in summed_co2:
598
594
  summed_co2[summed_co2_key] = summed_mass
599
595
  if summed_co2_key in summed_co2 and surf_data is not None: