webviz-subsurface 0.2.29__py3-none-any.whl → 0.2.31__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 (20) hide show
  1. webviz_subsurface/_components/tornado/_tornado_data.py +3 -0
  2. webviz_subsurface/_providers/ensemble_surface_provider/surface_array_server.py +0 -1
  3. webviz_subsurface/_providers/ensemble_surface_provider/surface_image_server.py +0 -1
  4. webviz_subsurface/plugins/_co2_leakage/_plugin.py +79 -37
  5. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +99 -38
  6. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +417 -355
  7. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +2 -7
  8. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +15 -11
  9. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +13 -4
  10. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +93 -33
  11. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +301 -116
  12. webviz_subsurface/plugins/_seismic_misfit.py +1 -1
  13. webviz_subsurface/plugins/_volumetric_analysis/controllers/tornado_controllers.py +5 -1
  14. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/METADATA +34 -34
  15. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/RECORD +20 -20
  16. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/WHEEL +1 -1
  17. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/LICENSE +0 -0
  18. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/LICENSE.chromedriver +0 -0
  19. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/entry_points.txt +0 -0
  20. {webviz_subsurface-0.2.29.dist-info → webviz_subsurface-0.2.31.dist-info}/top_level.txt +0 -0
@@ -25,6 +25,9 @@ class TornadoData:
25
25
  self._cut_sensitivities_by_ref()
26
26
  self._sort_sensitivities_by_max()
27
27
  self._real_df = self._create_real_df(dframe)
28
+ self.mean_per_mc_sens = (
29
+ dframe[dframe["SENSTYPE"] == "mc"].groupby("SENSNAME")["VALUE"].mean()
30
+ )
28
31
 
29
32
  def _validate_input(self, dframe: pd.DataFrame) -> None:
30
33
  for col in self.REQUIRED_COLUMNS:
@@ -61,7 +61,6 @@ class SurfaceArrayServer:
61
61
  "CACHE_TYPE": "FileSystemCache",
62
62
  "CACHE_DIR": cache_dir,
63
63
  "CACHE_DEFAULT_TIMEOUT": 0,
64
- "CACHE_OPTIONS": {"mode": 0o660},
65
64
  }
66
65
  )
67
66
  self._array_cache.init_app(app.server)
@@ -56,7 +56,6 @@ class SurfaceImageServer:
56
56
  "CACHE_TYPE": "FileSystemCache",
57
57
  "CACHE_DIR": cache_dir,
58
58
  "CACHE_DEFAULT_TIMEOUT": 0,
59
- "CACHE_OPTIONS": {"mode": 0o660},
60
59
  }
61
60
  )
62
61
  self._image_cache.init_app(app.server)
@@ -22,6 +22,7 @@ from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import (
22
22
  process_visualization_info,
23
23
  property_origin,
24
24
  readable_name,
25
+ set_plot_ids,
25
26
  )
26
27
  from webviz_subsurface.plugins._co2_leakage._utilities.fault_polygons import (
27
28
  FaultPolygonsHandler,
@@ -34,10 +35,10 @@ from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
34
35
  )
35
36
  from webviz_subsurface.plugins._co2_leakage._utilities.initialization import (
36
37
  init_map_attribute_names,
38
+ init_menu_options,
37
39
  init_surface_providers,
38
40
  init_table_provider,
39
41
  init_well_pick_provider,
40
- init_zone_and_region_options,
41
42
  process_files,
42
43
  )
43
44
  from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
@@ -165,12 +166,13 @@ class CO2Leakage(WebvizPluginABC):
165
166
  well_pick_dict,
166
167
  map_surface_names_to_well_pick_names,
167
168
  )
168
- # Zone and region options
169
- self._zone_and_region_options = init_zone_and_region_options(
169
+ # Phase (in case of residual trapping), zone and region options
170
+ self._menu_options = init_menu_options(
170
171
  ensemble_paths,
171
172
  self._co2_table_providers,
172
173
  self._co2_actual_volume_table_providers,
173
- self._ensemble_surface_providers,
174
+ plume_mass_relpath,
175
+ plume_actual_volume_relpath,
174
176
  )
175
177
  except Exception as err:
176
178
  self._error_message = f"Plugin initialization failed: {err}"
@@ -196,7 +198,7 @@ class CO2Leakage(WebvizPluginABC):
196
198
  self._map_attribute_names,
197
199
  [c["name"] for c in self._color_tables], # type: ignore
198
200
  self._well_pick_names,
199
- self._zone_and_region_options,
201
+ self._menu_options,
200
202
  ),
201
203
  self.Ids.MAIN_SETTINGS,
202
204
  )
@@ -229,25 +231,18 @@ class CO2Leakage(WebvizPluginABC):
229
231
  raise ValueError(f"Failed to fetch dates for attribute '{att_name}'")
230
232
  return dates
231
233
 
234
+ # Might want to do some refactoring if this gets too big
235
+ # pylint: disable=too-many-statements
232
236
  def _set_callbacks(self) -> None:
233
237
  # Cannot avoid many arguments since all the parameters are needed
234
238
  # to determine what to plot
235
239
  # pylint: disable=too-many-arguments
236
- # pylint: disable=too-many-locals
237
240
  @callback(
238
- Output(
239
- self._settings_component(ViewSettings.Ids.CONTAINMENT_VIEW), "value"
240
- ),
241
241
  Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
242
242
  Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
243
243
  Output(
244
244
  self._view_component(MapViewElement.Ids.TIME_PLOT_ONE_REAL), "figure"
245
245
  ),
246
- Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "style"),
247
- Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "style"),
248
- Output(
249
- self._view_component(MapViewElement.Ids.TIME_PLOT_ONE_REAL), "style"
250
- ),
251
246
  Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
252
247
  Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
253
248
  Input(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
@@ -258,7 +253,11 @@ class CO2Leakage(WebvizPluginABC):
258
253
  Input(self._settings_component(ViewSettings.Ids.Y_MAX_GRAPH), "value"),
259
254
  Input(self._settings_component(ViewSettings.Ids.ZONE), "value"),
260
255
  Input(self._settings_component(ViewSettings.Ids.REGION), "value"),
261
- Input(self._settings_component(ViewSettings.Ids.CONTAINMENT_VIEW), "value"),
256
+ Input(self._settings_component(ViewSettings.Ids.PHASE), "value"),
257
+ Input(self._settings_component(ViewSettings.Ids.CONTAINMENT), "value"),
258
+ Input(self._settings_component(ViewSettings.Ids.COLOR_BY), "value"),
259
+ Input(self._settings_component(ViewSettings.Ids.MARK_BY), "value"),
260
+ Input(self._settings_component(ViewSettings.Ids.SORT_PLOT), "value"),
262
261
  )
263
262
  @callback_typecheck
264
263
  def update_graphs(
@@ -272,15 +271,23 @@ class CO2Leakage(WebvizPluginABC):
272
271
  y_max_val: Optional[float],
273
272
  zone: Optional[str],
274
273
  region: Optional[str],
275
- containment_view: str,
276
- ) -> Tuple[Dict, go.Figure, go.Figure]:
277
- out = {"figs": [no_update] * 3, "styles": [{"display": "none"}] * 3}
274
+ phase: str,
275
+ containment: str,
276
+ color_choice: str,
277
+ mark_choice: Optional[str],
278
+ sorting: str,
279
+ ) -> Tuple[Dict, go.Figure, go.Figure, go.Figure]:
280
+ # pylint: disable=too-many-locals
281
+ figs = [no_update] * 3
278
282
  cont_info = process_containment_info(
279
283
  zone,
280
284
  region,
281
- containment_view,
282
- self._zone_and_region_options[ensemble][source],
283
- source,
285
+ phase,
286
+ containment,
287
+ color_choice,
288
+ mark_choice,
289
+ sorting,
290
+ self._menu_options[ensemble][source],
284
291
  )
285
292
  if source in [
286
293
  GraphSource.CONTAINMENT_MASS,
@@ -290,12 +297,11 @@ class CO2Leakage(WebvizPluginABC):
290
297
  y_min_val if len(y_min_auto) == 0 else None,
291
298
  y_max_val if len(y_max_auto) == 0 else None,
292
299
  ]
293
- out["styles"] = [{}] * 3
294
300
  if (
295
301
  source == GraphSource.CONTAINMENT_MASS
296
302
  and ensemble in self._co2_table_providers
297
303
  ):
298
- out["figs"][: len(out["figs"])] = generate_containment_figures(
304
+ figs[: len(figs)] = generate_containment_figures(
299
305
  self._co2_table_providers[ensemble],
300
306
  co2_scale,
301
307
  realizations[0],
@@ -306,18 +312,14 @@ class CO2Leakage(WebvizPluginABC):
306
312
  source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
307
313
  and ensemble in self._co2_actual_volume_table_providers
308
314
  ):
309
- out["figs"][: len(out["figs"])] = generate_containment_figures(
315
+ figs[: len(figs)] = generate_containment_figures(
310
316
  self._co2_actual_volume_table_providers[ensemble],
311
317
  co2_scale,
312
318
  realizations[0],
313
319
  y_limits,
314
320
  cont_info,
315
321
  )
316
- for fig in out["figs"]:
317
- fig["layout"][
318
- "uirevision"
319
- ] = f"{source}-{co2_scale}-{cont_info['zone']}-{cont_info['region']}"
320
- out["figs"][-1]["layout"]["uirevision"] += f"-{realizations}"
322
+ set_plot_ids(figs, source, co2_scale, cont_info, realizations)
321
323
  elif source == GraphSource.UNSMRY:
322
324
  if self._unsmry_providers is not None:
323
325
  if ensemble in self._unsmry_providers:
@@ -326,14 +328,13 @@ class CO2Leakage(WebvizPluginABC):
326
328
  co2_scale,
327
329
  self._co2_table_providers[ensemble],
328
330
  )
329
- out["figs"][: len(u_figs)] = u_figs
330
- out["styles"][: len(u_figs)] = [{}] * len(u_figs)
331
+ figs = list(u_figs)
331
332
  else:
332
333
  LOGGER.warning(
333
334
  """UNSMRY file has not been specified as input.
334
335
  Please use unsmry_relpath in the configuration."""
335
336
  )
336
- return cont_info["containment_view"], *out["figs"], *out["styles"] # type: ignore
337
+ return figs # type: ignore
337
338
 
338
339
  @callback(
339
340
  Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
@@ -352,15 +353,17 @@ class CO2Leakage(WebvizPluginABC):
352
353
  }
353
354
  for i, d in enumerate(date_list)
354
355
  }
355
- initial_date = max(dates.keys())
356
- return dates, initial_date
356
+ return dates, max(dates.keys())
357
357
 
358
358
  @callback(
359
359
  Output(self._view_component(MapViewElement.Ids.DATE_WRAPPER), "style"),
360
360
  Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
361
361
  )
362
362
  def toggle_date_slider(attribute: str) -> Dict[str, str]:
363
- if MapAttribute(attribute) == MapAttribute.MIGRATION_TIME:
363
+ if MapAttribute(attribute) in [
364
+ MapAttribute.MIGRATION_TIME_SGAS,
365
+ MapAttribute.MIGRATION_TIME_AMFG,
366
+ ]:
364
367
  return {"display": "none"}
365
368
  return {}
366
369
 
@@ -401,7 +404,7 @@ class CO2Leakage(WebvizPluginABC):
401
404
  )
402
405
 
403
406
  # Cannot avoid many arguments and/or locals since all layers of the DeckGL map
404
- # needs to be updated simultaneously
407
+ # need to be updated simultaneously
405
408
  # pylint: disable=too-many-arguments,too-many-locals
406
409
  @callback(
407
410
  Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "layers"),
@@ -512,7 +515,6 @@ class CO2Leakage(WebvizPluginABC):
512
515
  self._summed_co2,
513
516
  self._visualization_info["unit"],
514
517
  )
515
- # Plume polygon
516
518
  plume_polygon = None
517
519
  if contour_data is not None:
518
520
  plume_polygon = get_plume_polygon(
@@ -566,3 +568,43 @@ class CO2Leakage(WebvizPluginABC):
566
568
  if _n_clicks is not None:
567
569
  return _n_clicks > 0
568
570
  raise PreventUpdate
571
+
572
+ @callback(
573
+ Output(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
574
+ Output(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
575
+ Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "style"),
576
+ Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "style"),
577
+ Output(
578
+ self._view_component(MapViewElement.Ids.TIME_PLOT_ONE_REAL), "style"
579
+ ),
580
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
581
+ Input(self._view_component(MapViewElement.Ids.SIZE_SLIDER), "value"),
582
+ State(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
583
+ State(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
584
+ Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
585
+ )
586
+ def resize_plots(
587
+ ensemble: str,
588
+ slider_value: float,
589
+ top_style: Dict,
590
+ bottom_style: Dict,
591
+ source: GraphSource,
592
+ ) -> List[Dict]:
593
+ bottom_style["height"] = f"{slider_value}vh"
594
+ top_style["height"] = f"{80 - slider_value}vh"
595
+
596
+ styles = [{"height": f"{slider_value * 0.9 - 4}vh", "width": "90%"}] * 3
597
+ if source == GraphSource.UNSMRY and self._unsmry_providers is None:
598
+ styles = [{"display": "none"}] * 3
599
+ elif (
600
+ source == GraphSource.CONTAINMENT_MASS
601
+ and ensemble not in self._co2_table_providers
602
+ ):
603
+ styles = [{"display": "none"}] * 3
604
+ elif (
605
+ source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
606
+ and ensemble not in self._co2_actual_volume_table_providers
607
+ ):
608
+ styles = [{"display": "none"}] * 3
609
+
610
+ return [top_style, bottom_style] + styles
@@ -6,6 +6,7 @@ import geojson
6
6
  import numpy as np
7
7
  import plotly.graph_objects as go
8
8
  import webviz_subsurface_components as wsc
9
+ from dash import no_update
9
10
  from flask_caching import Cache
10
11
 
11
12
  from webviz_subsurface._providers import (
@@ -30,7 +31,6 @@ from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import (
30
31
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
31
32
  Co2MassScale,
32
33
  Co2VolumeScale,
33
- ContainmentViews,
34
34
  GraphSource,
35
35
  LayoutLabels,
36
36
  MapAttribute,
@@ -56,7 +56,7 @@ def property_origin(
56
56
  return map_attribute_names[MapAttribute.MAX_SGAS]
57
57
  if attribute == MapAttribute.AMFG_PLUME:
58
58
  return map_attribute_names[MapAttribute.MAX_AMFG]
59
- raise AssertionError(f"No origin defined for property: {attribute}")
59
+ raise AssertionError(f"Map attribute name not found for property: {attribute}")
60
60
 
61
61
 
62
62
  @dataclass
@@ -86,7 +86,9 @@ class SurfaceData:
86
86
  visualization_info,
87
87
  map_attribute_names,
88
88
  )
89
- assert surf_meta is not None # Should not occur
89
+ if surf_meta is None: # Surface file does not exist
90
+ return None, None
91
+ assert isinstance(img_url, str)
90
92
  value_range = (
91
93
  0.0 if np.ma.is_masked(surf_meta.val_min) else surf_meta.val_min,
92
94
  0.0 if np.ma.is_masked(surf_meta.val_max) else surf_meta.val_max,
@@ -132,7 +134,15 @@ def derive_surface_address(
132
134
  threshold=contour_data["threshold"] if contour_data else 0.0,
133
135
  smoothing=contour_data["smoothing"] if contour_data else 0.0,
134
136
  )
135
- date = None if attribute == MapAttribute.MIGRATION_TIME else date
137
+ date = (
138
+ None
139
+ if attribute
140
+ in [
141
+ MapAttribute.MIGRATION_TIME_SGAS,
142
+ MapAttribute.MIGRATION_TIME_AMFG,
143
+ ]
144
+ else date
145
+ )
136
146
  if len(realization) == 1:
137
147
  return SimulatedSurfaceAddress(
138
148
  attribute=map_attribute_names[attribute],
@@ -151,7 +161,10 @@ def derive_surface_address(
151
161
 
152
162
  def readable_name(attribute: MapAttribute) -> str:
153
163
  unit = ""
154
- if attribute == MapAttribute.MIGRATION_TIME:
164
+ if attribute in [
165
+ MapAttribute.MIGRATION_TIME_SGAS,
166
+ MapAttribute.MIGRATION_TIME_AMFG,
167
+ ]:
155
168
  unit = " [year]"
156
169
  elif attribute in (MapAttribute.AMFG_PLUME, MapAttribute.SGAS_PLUME):
157
170
  unit = " [# real.]"
@@ -198,7 +211,10 @@ def get_plume_polygon(
198
211
 
199
212
 
200
213
  def _find_legend_title(attribute: MapAttribute, unit: str) -> str:
201
- if attribute == MapAttribute.MIGRATION_TIME:
214
+ if attribute in [
215
+ MapAttribute.MIGRATION_TIME_SGAS,
216
+ MapAttribute.MIGRATION_TIME_AMFG,
217
+ ]:
202
218
  return "years"
203
219
  if attribute in [MapAttribute.MASS, MapAttribute.DISSOLVED, MapAttribute.FREE]:
204
220
  return unit
@@ -213,7 +229,13 @@ def create_map_annotations(
213
229
  unit: str,
214
230
  ) -> List[wsc.ViewAnnotation]:
215
231
  annotations = []
216
- if surface_data is not None:
232
+ if (
233
+ surface_data is not None
234
+ and surface_data.color_map_range[0] is not None
235
+ and surface_data.color_map_range[1] is not None
236
+ ):
237
+ num_digits = np.ceil(np.log(surface_data.color_map_range[1]) / np.log(10))
238
+ numbersize = max((6, min((17 - num_digits, 11))))
217
239
  annotations.append(
218
240
  wsc.ViewAnnotation(
219
241
  id="1_view",
@@ -227,6 +249,8 @@ def create_map_annotations(
227
249
  openColorSelector=False,
228
250
  legendScaleSize=0.1,
229
251
  legendFontSize=20,
252
+ tickFontSize=numbersize,
253
+ numberOfTicks=2,
230
254
  colorTables=colortables,
231
255
  ),
232
256
  wsc.ViewFooter(children=formation),
@@ -383,7 +407,7 @@ def generate_containment_figures(
383
407
  co2_scale: Union[Co2MassScale, Co2VolumeScale],
384
408
  realization: int,
385
409
  y_limits: List[Optional[float]],
386
- containment_info: Dict[str, Union[str, None, List[str]]],
410
+ containment_info: Dict[str, Union[str, None, List[str], int]],
387
411
  ) -> Tuple[go.Figure, go.Figure, go.Figure]:
388
412
  try:
389
413
  fig0 = generate_co2_volume_figure(
@@ -484,36 +508,73 @@ def process_visualization_info(
484
508
  def process_containment_info(
485
509
  zone: Optional[str],
486
510
  region: Optional[str],
487
- view: Optional[str],
488
- zone_and_region_options: Dict[str, List[str]],
489
- source: str,
490
- ) -> Dict[str, Union[str, None, List[str]]]:
491
- zones = zone_and_region_options["zones"]
492
- regions = zone_and_region_options["regions"]
493
- if source in [
494
- GraphSource.CONTAINMENT_MASS,
495
- GraphSource.CONTAINMENT_ACTUAL_VOLUME,
496
- ]:
497
- if view == ContainmentViews.CONTAINMENTSPLIT:
498
- return {"zone": zone, "region": region, "containment_view": view}
499
- if view == ContainmentViews.ZONESPLIT and len(zones) > 0:
500
- zones = [zone_name for zone_name in zones if zone_name != "all"]
501
- elif view == ContainmentViews.REGIONSPLIT and len(regions) > 0:
502
- regions = [reg_name for reg_name in regions if reg_name != "all"]
503
- else:
504
- return {
505
- "zone": zone,
506
- "region": region,
507
- "containment_view": ContainmentViews.CONTAINMENTSPLIT,
508
- }
509
- return {
510
- "zone": zone,
511
- "region": region,
512
- "containment_view": view,
513
- "zones": zones,
514
- "regions": regions,
515
- }
516
- return {"containment_view": ContainmentViews.CONTAINMENTSPLIT}
511
+ phase: str,
512
+ containment: str,
513
+ color_choice: str,
514
+ mark_choice: Optional[str],
515
+ sorting: str,
516
+ menu_options: Dict[str, List[str]],
517
+ ) -> Dict[str, Union[str, None, List[str], int]]:
518
+ if mark_choice is None:
519
+ mark_choice = "phase"
520
+ zones = menu_options["zones"]
521
+ regions = menu_options["regions"]
522
+ if len(zones) > 0:
523
+ zones = [zone_name for zone_name in zones if zone_name != "all"]
524
+ if len(regions) > 0:
525
+ 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"]
528
+ if "zone" in [mark_choice, color_choice]:
529
+ region = "all"
530
+ if "region" in [mark_choice, color_choice]:
531
+ zone = "all"
532
+ return {
533
+ "zone": zone,
534
+ "region": region,
535
+ "zones": zones,
536
+ "regions": regions,
537
+ "phase": phase,
538
+ "containment": containment,
539
+ "color_choice": color_choice,
540
+ "mark_choice": mark_choice,
541
+ "sorting": sorting,
542
+ "phases": phases,
543
+ "containments": containments,
544
+ }
545
+
546
+
547
+ def set_plot_ids(
548
+ figs: List[go.Figure],
549
+ source: GraphSource,
550
+ scale: Union[Co2MassScale, Co2VolumeScale],
551
+ containment_info: Dict,
552
+ 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
+ )
574
+ )
575
+ for fig in figs:
576
+ fig["layout"]["uirevision"] = plot_id
577
+ figs[-1]["layout"]["uirevision"] += f"-{realizations}"
517
578
 
518
579
 
519
580
  def process_summed_mass(