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
@@ -1,54 +1,53 @@
1
- import dataclasses
2
- from typing import Iterable, List, Union
1
+ from typing import Union
3
2
 
4
3
  import numpy as np
5
- import pandas as pd
6
4
  import plotly.colors
7
5
  import plotly.graph_objects as go
8
6
 
9
- from webviz_subsurface._providers import EnsembleTableProvider
7
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
8
+ ContainmentDataProvider,
9
+ )
10
10
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
11
11
  Co2MassScale,
12
12
  Co2VolumeScale,
13
13
  )
14
+ from webviz_subsurface.plugins._co2_leakage._utilities.unsmry_data_provider import (
15
+ UnsmryDataProvider,
16
+ )
14
17
 
15
18
 
16
19
  # pylint: disable=too-many-locals
17
20
  def generate_summary_figure(
18
- table_provider_unsmry: EnsembleTableProvider,
19
- realizations_unsmry: List[int],
21
+ unsmry_provider: UnsmryDataProvider,
20
22
  scale: Union[Co2MassScale, Co2VolumeScale],
21
- table_provider_containment: EnsembleTableProvider,
22
- realizations_containment: List[int],
23
+ containment_provider: ContainmentDataProvider,
23
24
  ) -> go.Figure:
24
- columns_unsmry = _column_subset_unsmry(table_provider_unsmry)
25
- columns_containment = _column_subset_containment(table_provider_containment)
26
- df_unsmry = _read_dataframe(
27
- table_provider_unsmry, realizations_unsmry, columns_unsmry, scale
28
- )
29
- df_containment = _read_dataframe_containment(
30
- table_provider_containment, realizations_containment, columns_containment, scale
31
- )
32
- fig = go.Figure()
33
- showlegend = True
25
+ df_unsmry = unsmry_provider.extract(scale)
26
+ df_containment = containment_provider.extract_condensed_dataframe(scale)
34
27
 
28
+ # TODO: expose these directly from data providers?
35
29
  r_min = min(df_unsmry.REAL)
36
- unsmry_last_total = df_unsmry[df_unsmry.REAL == r_min]["total"].iloc[-1]
37
- unsmry_last_mobile = df_unsmry[df_unsmry.REAL == r_min][columns_unsmry.mobile].iloc[
38
- -1
39
- ]
40
- unsmry_last_dissolved = df_unsmry[df_unsmry.REAL == r_min][
41
- columns_unsmry.dissolved
30
+ unsmry_last_total = df_unsmry[df_unsmry.REAL == r_min][
31
+ unsmry_provider.colname_total
42
32
  ].iloc[-1]
43
- containment_last_total = df_containment[df_containment.REAL == r_min]["total"].iloc[
44
- -1
45
- ]
46
- containment_last_mobile = df_containment[df_containment.REAL == r_min][
47
- columns_containment.mobile
33
+ unsmry_last_mobile = df_unsmry[df_unsmry.REAL == r_min][
34
+ unsmry_provider.colname_mobile
48
35
  ].iloc[-1]
49
- containment_last_dissolved = df_containment[df_containment.REAL == r_min][
50
- columns_containment.dissolved
36
+ unsmry_last_dissolved = df_unsmry[df_unsmry.REAL == r_min][
37
+ unsmry_provider.colname_dissolved
51
38
  ].iloc[-1]
39
+
40
+ containment_reference = df_containment[df_containment.REAL == r_min]
41
+ containment_last_total = containment_reference[
42
+ containment_reference["phase"] == "total"
43
+ ]["amount"].iloc[-1]
44
+ containment_last_mobile = containment_reference[
45
+ containment_reference["phase"] == "free_gas"
46
+ ]["amount"].iloc[-1]
47
+ containment_last_dissolved = containment_reference[
48
+ containment_reference["phase"] == "dissolved"
49
+ ]["amount"].iloc[-1]
50
+ # ---
52
51
  last_total_err_percentage = (
53
52
  100.0 * abs(containment_last_total - unsmry_last_total) / unsmry_last_total
54
53
  )
@@ -64,76 +63,75 @@ def generate_summary_figure(
64
63
  last_mobile_err_percentage = np.round(last_mobile_err_percentage, 2)
65
64
  last_dissolved_err_percentage = np.round(last_dissolved_err_percentage, 2)
66
65
 
66
+ _colors = {
67
+ "total": plotly.colors.qualitative.Plotly[3],
68
+ "mobile": plotly.colors.qualitative.Plotly[2],
69
+ "dissolved": plotly.colors.qualitative.Plotly[0],
70
+ "trapped": plotly.colors.qualitative.Plotly[1],
71
+ }
72
+
73
+ fig = go.Figure()
74
+ showlegend = True
67
75
  for _, sub_df in df_unsmry.groupby("realization"):
68
- colors = plotly.colors.qualitative.Plotly
69
76
  fig.add_scatter(
70
- x=sub_df[columns_unsmry.time],
71
- y=sub_df["total"],
77
+ x=sub_df[unsmry_provider.colname_date],
78
+ y=sub_df[unsmry_provider.colname_total],
72
79
  name="UNSMRY",
73
- legendgroup="group_1",
80
+ legendgroup="total",
74
81
  legendgrouptitle_text=f"Total ({last_total_err_percentage} %)",
75
82
  showlegend=showlegend,
76
- marker_color=colors[3],
83
+ marker_color=_colors["total"],
77
84
  )
78
85
  fig.add_scatter(
79
- x=sub_df[columns_unsmry.time],
80
- y=sub_df[columns_unsmry.mobile],
81
- name=f"UNSMRY ({columns_unsmry.mobile})",
82
- legendgroup="group_2",
86
+ x=sub_df[unsmry_provider.colname_date],
87
+ y=sub_df[unsmry_provider.colname_mobile],
88
+ name=f"UNSMRY ({unsmry_provider.colname_mobile})",
89
+ legendgroup="mobile",
83
90
  legendgrouptitle_text=f"Mobile ({last_mobile_err_percentage} %)",
84
91
  showlegend=showlegend,
85
- marker_color=colors[2],
92
+ marker_color=_colors["mobile"],
86
93
  )
87
94
  fig.add_scatter(
88
- x=sub_df[columns_unsmry.time],
89
- y=sub_df[columns_unsmry.dissolved],
90
- name=f"UNSMRY ({columns_unsmry.dissolved})",
91
- legendgroup="group_3",
95
+ x=sub_df[unsmry_provider.colname_date],
96
+ y=sub_df[unsmry_provider.colname_dissolved],
97
+ name=f"UNSMRY ({unsmry_provider.colname_dissolved})",
98
+ legendgroup="dissolved",
92
99
  legendgrouptitle_text=f"Dissolved ({last_dissolved_err_percentage} %)",
93
100
  showlegend=showlegend,
94
- marker_color=colors[0],
101
+ marker_color=_colors["dissolved"],
95
102
  )
96
103
  fig.add_scatter(
97
- x=sub_df[columns_unsmry.time],
98
- y=sub_df[columns_unsmry.trapped],
99
- name=f"UNSMRY ({columns_unsmry.trapped})",
100
- legendgroup="group_4",
104
+ x=sub_df[unsmry_provider.colname_date],
105
+ y=sub_df[unsmry_provider.colname_trapped],
106
+ name=f"UNSMRY ({unsmry_provider.colname_trapped})",
107
+ legendgroup="trapped",
101
108
  legendgrouptitle_text="Trapped",
102
109
  showlegend=showlegend,
103
- marker_color=colors[1],
110
+ marker_color=_colors["trapped"],
104
111
  )
105
112
  showlegend = False
106
- showlegend = True
107
- for _, sub_df in df_containment.groupby("realization"):
108
- colors = plotly.colors.qualitative.Plotly
109
- fig.add_scatter(
110
- x=sub_df[columns_containment.time],
111
- y=sub_df["total"],
112
- name="Containment script",
113
- legendgroup="group_1",
114
- showlegend=showlegend,
115
- marker_color=colors[3],
116
- line_dash="dash",
117
- )
118
- fig.add_scatter(
119
- x=sub_df[columns_containment.time],
120
- y=sub_df[columns_containment.mobile],
121
- name=f"Containment script ({columns_containment.mobile})",
122
- legendgroup="group_2",
123
- showlegend=showlegend,
124
- marker_color=colors[2],
125
- line_dash="dash",
126
- )
113
+
114
+ _col_names = {
115
+ "total": "total",
116
+ "free_gas": "mobile",
117
+ "dissolved": "dissolved",
118
+ "trapped_gas": "trapped",
119
+ }
120
+
121
+ first_real = None
122
+ for (real, phase), sub_df in df_containment.groupby(["REAL", "phase"]):
123
+ if first_real is None:
124
+ first_real = real
127
125
  fig.add_scatter(
128
- x=sub_df[columns_containment.time],
129
- y=sub_df[columns_containment.dissolved],
130
- name=f"Containment script ({columns_containment.dissolved})",
131
- legendgroup="group_3",
132
- showlegend=showlegend,
133
- marker_color=colors[0],
126
+ x=sub_df["date"],
127
+ y=sub_df["amount"],
128
+ name=f"Containment script ({phase})",
129
+ legendgroup=_col_names[phase],
130
+ showlegend=bool(first_real == real),
131
+ marker_color=_colors[_col_names[phase]],
134
132
  line_dash="dash",
135
133
  )
136
- showlegend = False
134
+
137
135
  fig.layout.xaxis.title = "Time"
138
136
  fig.layout.yaxis.title = f"Amount CO2 [{scale.value}]"
139
137
  fig.layout.paper_bgcolor = "rgba(0,0,0,0)"
@@ -142,97 +140,3 @@ def generate_summary_figure(
142
140
  fig.layout.margin.l = 10
143
141
  fig.layout.margin.r = 10
144
142
  return fig
145
-
146
-
147
- @dataclasses.dataclass
148
- class _ColumnNames:
149
- time: str
150
- dissolved: str
151
- trapped: str
152
- mobile: str
153
-
154
- def values(self) -> Iterable[str]:
155
- return dataclasses.asdict(self).values()
156
-
157
-
158
- @dataclasses.dataclass
159
- class _ColumnNamesContainment:
160
- time: str
161
- dissolved: str
162
- mobile: str
163
-
164
- def values(self) -> Iterable[str]:
165
- return dataclasses.asdict(self).values()
166
-
167
-
168
- def _read_dataframe(
169
- table_provider: EnsembleTableProvider,
170
- realizations: List[int],
171
- columns: _ColumnNames,
172
- co2_scale: Union[Co2MassScale, Co2VolumeScale],
173
- ) -> pd.DataFrame:
174
- full = pd.concat(
175
- [
176
- table_provider.get_column_data(list(columns.values()), [real]).assign(
177
- realization=real
178
- )
179
- for real in realizations
180
- ]
181
- )
182
- full["total"] = (
183
- full[columns.dissolved] + full[columns.trapped] + full[columns.mobile]
184
- )
185
- for col in [columns.dissolved, columns.trapped, columns.mobile, "total"]:
186
- if co2_scale == Co2MassScale.MTONS:
187
- full[col] = full[col] / 1e9
188
- elif co2_scale == Co2MassScale.NORMALIZE:
189
- full[col] = full[col] / full["total"].max()
190
- return full
191
-
192
-
193
- def _read_dataframe_containment(
194
- table_provider: EnsembleTableProvider,
195
- realizations: List[int],
196
- columns: _ColumnNamesContainment,
197
- co2_scale: Union[Co2MassScale, Co2VolumeScale],
198
- ) -> pd.DataFrame:
199
- full = pd.concat(
200
- [
201
- table_provider.get_column_data(list(columns.values()), [real]).assign(
202
- realization=real
203
- )
204
- for real in realizations
205
- ]
206
- )
207
- full["total"] = full[columns.dissolved] + full[columns.mobile]
208
- for col in [columns.dissolved, columns.mobile, "total"]:
209
- if co2_scale == Co2MassScale.MTONS:
210
- full[col] = full[col] / 1e9
211
- elif co2_scale == Co2MassScale.NORMALIZE:
212
- full[col] = full[col] / full["total"].max()
213
- return full
214
-
215
-
216
- def _column_subset_unsmry(table_provider: EnsembleTableProvider) -> _ColumnNames:
217
- existing = set(table_provider.column_names())
218
- assert "DATE" in existing
219
- # Try PFLOTRAN names
220
- col_names = _ColumnNames("DATE", "FGMDS", "FGMTR", "FGMGP")
221
- if set(col_names.values()).issubset(existing):
222
- return col_names
223
- # Try Eclipse names
224
- col_names = _ColumnNames("DATE", "FWCD", "FGCDI", "FGCDM")
225
- if set(col_names.values()).issubset(existing):
226
- return col_names
227
- raise KeyError(f"Could not find suitable data columns among: {', '.join(existing)}")
228
-
229
-
230
- def _column_subset_containment(
231
- table_provider: EnsembleTableProvider,
232
- ) -> _ColumnNamesContainment:
233
- existing = set(table_provider.column_names())
234
- assert "date" in existing
235
- col_names = _ColumnNamesContainment("date", "total_aqueous", "total_gas")
236
- if set(col_names.values()).issubset(existing):
237
- return col_names
238
- raise KeyError(f"Could not find suitable data columns among: {', '.join(existing)}")
@@ -17,12 +17,15 @@ from webviz_subsurface._providers import (
17
17
  from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_provider import (
18
18
  SurfaceStatistic,
19
19
  )
20
- from webviz_subsurface.plugins._co2_leakage._utilities.generic import MapAttribute
20
+ from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
21
+ FilteredMapAttribute,
22
+ MapType,
23
+ )
21
24
  from webviz_subsurface.plugins._co2_leakage._utilities.plume_extent import (
22
25
  truncate_surfaces,
23
26
  )
24
27
 
25
- SCALE_DICT = {"kg": 1, "tons": 1000, "M tons": 1000000}
28
+ SCALE_DICT = {"kg": 0.001, "tons": 1, "M tons": 1e6}
26
29
 
27
30
 
28
31
  @dataclass
@@ -44,10 +47,13 @@ def publish_and_get_surface_metadata(
44
47
  provider: EnsembleSurfaceProvider,
45
48
  address: Union[SurfaceAddress, TruncatedSurfaceAddress],
46
49
  visualization_info: Dict[str, Any],
47
- map_attribute_names: Dict[MapAttribute, str],
50
+ map_attribute_names: FilteredMapAttribute,
48
51
  ) -> Tuple[Optional[SurfaceImageMeta], Optional[str], Optional[Any]]:
49
52
  if isinstance(address, TruncatedSurfaceAddress):
50
- return _publish_and_get_truncated_surface_metadata(server, provider, address)
53
+ return (
54
+ *_publish_and_get_truncated_surface_metadata(server, provider, address),
55
+ None,
56
+ )
51
57
  provider_id: str = provider.provider_id()
52
58
  qualified_address = QualifiedSurfaceAddress(provider_id, address)
53
59
  surf_meta = server.get_surface_metadata(qualified_address)
@@ -61,22 +67,26 @@ def publish_and_get_surface_metadata(
61
67
  if not surface:
62
68
  warnings.warn(f"Could not find surface file with properties: {address}")
63
69
  return None, None, None
64
- if address.attribute in [
65
- map_attribute_names[MapAttribute.MASS],
66
- map_attribute_names[MapAttribute.FREE],
67
- map_attribute_names[MapAttribute.DISSOLVED],
68
- ]:
70
+ address_map_attribute = next(
71
+ (
72
+ key
73
+ for key, value in map_attribute_names.filtered_values.items()
74
+ if value == address.attribute
75
+ ),
76
+ None,
77
+ )
78
+ assert address_map_attribute is not None
79
+ if MapType[address_map_attribute.name].value == "MASS":
69
80
  surface.values = surface.values / SCALE_DICT[visualization_info["unit"]]
70
- summed_mass = np.ma.sum(surface.values)
81
+ summed_mass = np.ma.sum(surface.values)
71
82
  if (
72
- address.attribute
73
- not in [
74
- map_attribute_names[MapAttribute.MIGRATION_TIME_SGAS],
75
- map_attribute_names[MapAttribute.MIGRATION_TIME_AMFG],
76
- ]
77
- and visualization_info["threshold"] >= 0
83
+ MapType[address_map_attribute.name].value not in ["PLUME", "MIGRATION_TIME"]
84
+ and visualization_info["thresholds"][visualization_info["attribute"]] >= 0
78
85
  ):
79
- surface.operation("elile", visualization_info["threshold"])
86
+ surface.operation(
87
+ "elile",
88
+ visualization_info["thresholds"][visualization_info["attribute"]],
89
+ )
80
90
  server.publish_surface(qualified_address, surface)
81
91
  surf_meta = server.get_surface_metadata(qualified_address)
82
92
  return surf_meta, server.encode_partial_url(qualified_address), summed_mass
@@ -86,7 +96,7 @@ def _publish_and_get_truncated_surface_metadata(
86
96
  server: SurfaceImageServer,
87
97
  provider: EnsembleSurfaceProvider,
88
98
  address: TruncatedSurfaceAddress,
89
- ) -> Tuple[Optional[SurfaceImageMeta], str, Optional[Any]]:
99
+ ) -> Tuple[Optional[SurfaceImageMeta], str]:
90
100
  qualified_address = QualifiedSurfaceAddress(
91
101
  provider.provider_id(),
92
102
  # TODO: Should probably use a dedicated address type for this. Statistical surface
@@ -102,15 +112,13 @@ def _publish_and_get_truncated_surface_metadata(
102
112
  ),
103
113
  )
104
114
  surf_meta = server.get_surface_metadata(qualified_address)
105
- summed_mass = None
106
115
  if surf_meta is None:
107
116
  surface = _generate_surface(provider, address)
108
117
  if surface is None:
109
118
  raise ValueError(f"Could not generate surface for address: {address}")
110
- summed_mass = np.ma.sum(surface.values)
111
119
  server.publish_surface(qualified_address, surface)
112
120
  surf_meta = server.get_surface_metadata(qualified_address)
113
- return surf_meta, server.encode_partial_url(qualified_address), summed_mass
121
+ return surf_meta, server.encode_partial_url(qualified_address)
114
122
 
115
123
 
116
124
  def _generate_surface(
@@ -0,0 +1,108 @@
1
+ from typing import Tuple, 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
+ _PFLOTRAN_COLNAMES = ("DATE", "FGMDS", "FGMTR", "FGMGP")
13
+ _ECLIPSE_COLNAMES = ("DATE", "FWCD", "FGCDI", "FGCDM")
14
+
15
+
16
+ class UnsmryDataValidationError(Exception):
17
+ pass
18
+
19
+
20
+ class UnsmryDataProvider:
21
+ def __init__(self, provider: EnsembleTableProvider):
22
+ UnsmryDataProvider._validate(provider)
23
+ self._provider = provider
24
+ (
25
+ self._colname_date,
26
+ self._colname_dissolved,
27
+ self._colname_trapped,
28
+ self._colname_mobile,
29
+ ) = UnsmryDataProvider._column_subset_unsmry(provider)
30
+ self._colname_total = "TOTAL"
31
+
32
+ @property
33
+ def menu_options(self) -> MenuOptions:
34
+ return {
35
+ "zones": [],
36
+ "regions": [],
37
+ "phases": ["total", "gas", "dissolved"],
38
+ "plume_groups": [],
39
+ "dates": [],
40
+ }
41
+
42
+ @property
43
+ def colname_date(self) -> str:
44
+ return self._colname_date
45
+
46
+ @property
47
+ def colname_dissolved(self) -> str:
48
+ return self._colname_dissolved
49
+
50
+ @property
51
+ def colname_trapped(self) -> str:
52
+ return self._colname_trapped
53
+
54
+ @property
55
+ def colname_mobile(self) -> str:
56
+ return self._colname_mobile
57
+
58
+ @property
59
+ def colname_total(self) -> str:
60
+ return self._colname_total
61
+
62
+ def extract(self, scale: Union[Co2MassScale, Co2VolumeScale]) -> pd.DataFrame:
63
+ columns = [
64
+ self._colname_date,
65
+ self._colname_dissolved,
66
+ self._colname_trapped,
67
+ self._colname_mobile,
68
+ ]
69
+ full = pd.concat(
70
+ [
71
+ self._provider.get_column_data(columns, [real]).assign(realization=real)
72
+ for real in self._provider.realizations()
73
+ ]
74
+ )
75
+ full[self._colname_total] = (
76
+ full[self._colname_dissolved]
77
+ + full[self._colname_trapped]
78
+ + full[self.colname_mobile]
79
+ )
80
+ total_max = full[self._colname_total].max()
81
+ for col in columns[1:] + [self._colname_total]:
82
+ if scale == Co2MassScale.MTONS:
83
+ full[col] = full[col] / 1e9
84
+ elif scale == Co2MassScale.NORMALIZE:
85
+ full[col] = full[col] / total_max
86
+ return full
87
+
88
+ @staticmethod
89
+ def _column_subset_unsmry(
90
+ provider: EnsembleTableProvider,
91
+ ) -> Tuple[str, str, str, str]:
92
+ existing = set(provider.column_names())
93
+ # Try PFLOTRAN names
94
+ if set(_PFLOTRAN_COLNAMES).issubset(existing):
95
+ return _PFLOTRAN_COLNAMES
96
+ # Try Eclipse names
97
+ if set(_ECLIPSE_COLNAMES).issubset(existing):
98
+ return _ECLIPSE_COLNAMES
99
+ raise KeyError(
100
+ f"Could not find suitable data columns among: {', '.join(existing)}"
101
+ )
102
+
103
+ @staticmethod
104
+ def _validate(provider: EnsembleTableProvider) -> None:
105
+ try:
106
+ UnsmryDataProvider._column_subset_unsmry(provider)
107
+ except KeyError as e:
108
+ raise UnsmryDataValidationError from e
@@ -13,9 +13,9 @@ class MainView(ViewABC):
13
13
  class Ids(StrEnum):
14
14
  MAIN_ELEMENT = "main-element"
15
15
 
16
- def __init__(self, color_scales: List[Dict[str, Any]]):
16
+ def __init__(self, color_scales: List[Dict[str, Any]], content: Dict[str, bool]):
17
17
  super().__init__("Main View")
18
- self._view_element = MapViewElement(color_scales)
18
+ self._view_element = MapViewElement(color_scales, content)
19
19
  self.add_view_element(self._view_element, self.Ids.MAIN_ELEMENT)
20
20
 
21
21
 
@@ -26,20 +26,24 @@ class MapViewElement(ViewElementABC):
26
26
  DATE_WRAPPER = "date-wrapper"
27
27
  BAR_PLOT = "bar-plot"
28
28
  TIME_PLOT = "time-plot"
29
- TIME_PLOT_ONE_REAL = "time-plot-one-realization"
29
+ STATISTICS_PLOT = "statistics-plot"
30
30
  BAR_PLOT_ORDER = "bar-plot-order"
31
31
  CONTAINMENT_COLORS = "containment-order"
32
32
  SIZE_SLIDER = "size-slider"
33
33
  TOP_ELEMENT = "top-element"
34
34
  BOTTOM_ELEMENT = "bottom-element"
35
35
 
36
- def __init__(self, color_scales: List[Dict[str, Any]]) -> None:
36
+ def __init__(
37
+ self, color_scales: List[Dict[str, Any]], content: Dict[str, bool]
38
+ ) -> None:
37
39
  super().__init__()
38
40
  self._color_scales = color_scales
41
+ self._content = content
39
42
 
40
43
  def inner_layout(self) -> Component:
41
- return html.Div(
42
- [
44
+ layout_elements = []
45
+ if self._content["maps"]:
46
+ layout_elements.append(
43
47
  wcc.Frame(
44
48
  # id=self.register_component_unique_id(LayoutElements.MAP_VIEW),
45
49
  id=self.register_component_unique_id(self.Ids.TOP_ELEMENT),
@@ -78,14 +82,17 @@ class MapViewElement(ViewElementABC):
78
82
  ),
79
83
  ],
80
84
  style={
81
- "height": "43vh",
85
+ "height": "43vh" if self._content["any_table"] else "100%",
82
86
  },
83
- ),
87
+ )
88
+ )
89
+ if self._content["any_table"]:
90
+ layout_elements.append(
84
91
  wcc.Frame(
85
92
  # id=get_uuid(LayoutElements.PLOT_VIEW),
86
93
  id=self.register_component_unique_id(self.Ids.BOTTOM_ELEMENT),
87
94
  style={
88
- "height": "37vh",
95
+ "height": "37vh" if self._content["maps"] else "100%",
89
96
  },
90
97
  children=[
91
98
  html.Div(
@@ -93,12 +100,15 @@ class MapViewElement(ViewElementABC):
93
100
  self.register_component_unique_id(self.Ids.BAR_PLOT),
94
101
  self.register_component_unique_id(self.Ids.TIME_PLOT),
95
102
  self.register_component_unique_id(
96
- self.Ids.TIME_PLOT_ONE_REAL
103
+ self.Ids.STATISTICS_PLOT
97
104
  ),
98
105
  )
99
106
  ),
100
107
  ],
101
- ),
108
+ )
109
+ )
110
+ if self._content["maps"] and self._content["any_table"]:
111
+ layout_elements.append(
102
112
  html.Div(
103
113
  [
104
114
  wcc.Slider(
@@ -118,8 +128,10 @@ class MapViewElement(ViewElementABC):
118
128
  style={
119
129
  "width": "100%",
120
130
  },
121
- ),
122
- ],
131
+ )
132
+ )
133
+ return html.Div(
134
+ layout_elements,
123
135
  style={
124
136
  "display": "flex",
125
137
  "flexDirection": "column",
@@ -131,7 +143,7 @@ class MapViewElement(ViewElementABC):
131
143
  def _summary_graph_layout(
132
144
  bar_plot_id: str,
133
145
  time_plot_id: str,
134
- time_plot_one_realization_id: str,
146
+ statistics_plot_id: str,
135
147
  ) -> List:
136
148
  return [
137
149
  wcc.Tabs(
@@ -139,7 +151,7 @@ def _summary_graph_layout(
139
151
  value="tab-1",
140
152
  children=[
141
153
  wcc.Tab(
142
- label="End-state containment (all realizations)",
154
+ label="Containment state",
143
155
  value="tab-1",
144
156
  children=[
145
157
  html.Div(
@@ -154,7 +166,7 @@ def _summary_graph_layout(
154
166
  ],
155
167
  ),
156
168
  wcc.Tab(
157
- label="Containment over time (all realizations)",
169
+ label="Containment over time",
158
170
  value="tab-2",
159
171
  children=[
160
172
  html.Div(
@@ -169,12 +181,12 @@ def _summary_graph_layout(
169
181
  ],
170
182
  ),
171
183
  wcc.Tab(
172
- label="Containment over time (one realization)",
184
+ label="Statistics",
173
185
  value="tab-3",
174
186
  children=[
175
187
  html.Div(
176
188
  wcc.Graph(
177
- id=time_plot_one_realization_id,
189
+ id=statistics_plot_id,
178
190
  figure=go.Figure(),
179
191
  config={
180
192
  "displayModeBar": False,