flood-adapt 1.0.3__py3-none-any.whl → 1.1.0__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.
- flood_adapt/__init__.py +2 -3
- flood_adapt/adapter/fiat_adapter.py +8 -3
- flood_adapt/adapter/sfincs_adapter.py +3 -3
- flood_adapt/adapter/sfincs_offshore.py +1 -1
- flood_adapt/config/fiat.py +1 -1
- flood_adapt/config/gui.py +185 -8
- flood_adapt/database_builder/database_builder.py +155 -129
- flood_adapt/database_builder/metrics_utils.py +1834 -0
- flood_adapt/dbs_classes/database.py +23 -28
- flood_adapt/dbs_classes/dbs_benefit.py +0 -26
- flood_adapt/dbs_classes/dbs_event.py +2 -2
- flood_adapt/dbs_classes/dbs_measure.py +2 -2
- flood_adapt/dbs_classes/dbs_scenario.py +0 -24
- flood_adapt/dbs_classes/dbs_static.py +4 -9
- flood_adapt/dbs_classes/dbs_strategy.py +2 -4
- flood_adapt/dbs_classes/dbs_template.py +65 -25
- flood_adapt/flood_adapt.py +64 -13
- flood_adapt/misc/exceptions.py +43 -6
- {flood_adapt-1.0.3.dist-info → flood_adapt-1.1.0.dist-info}/METADATA +3 -3
- {flood_adapt-1.0.3.dist-info → flood_adapt-1.1.0.dist-info}/RECORD +24 -44
- flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +0 -90
- flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +0 -57
- flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +0 -121
- flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +0 -65
- flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +0 -126
- flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +0 -60
- flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +0 -121
- flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +0 -65
- flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +0 -45
- flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +0 -4
- flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +0 -143
- flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +0 -153
- flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +0 -127
- flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +0 -57
- flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +0 -4
- flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +0 -191
- flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +0 -153
- flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +0 -178
- flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +0 -57
- flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +0 -9
- flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +0 -65
- /flood_adapt/database_builder/templates/infographics/{OSM/styles.css → styles.css} +0 -0
- {flood_adapt-1.0.3.dist-info → flood_adapt-1.1.0.dist-info}/LICENSE +0 -0
- {flood_adapt-1.0.3.dist-info → flood_adapt-1.1.0.dist-info}/WHEEL +0 -0
- {flood_adapt-1.0.3.dist-info → flood_adapt-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1834 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
4
|
+
|
|
5
|
+
import tomli_w
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
|
7
|
+
|
|
8
|
+
from flood_adapt.adapter.fiat_adapter import _IMPACT_COLUMNS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def combine_filters(*filters):
|
|
12
|
+
"""
|
|
13
|
+
Combine multiple SQL filter strings with AND operators.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
*filters : str
|
|
18
|
+
Variable number of filter strings to combine.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
str
|
|
23
|
+
Combined filter string with AND operators, excluding empty filters.
|
|
24
|
+
"""
|
|
25
|
+
return " AND ".join(f for f in filters if f)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pascal_case(s):
|
|
29
|
+
"""
|
|
30
|
+
Convert a string to PascalCase.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
s : str
|
|
35
|
+
Input string to convert.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
str
|
|
40
|
+
String converted to PascalCase.
|
|
41
|
+
"""
|
|
42
|
+
return "".join(word.capitalize() for word in s.split())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FieldMapping(BaseModel):
|
|
46
|
+
"""
|
|
47
|
+
Represents a mapping of a database field to a list of allowed values.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
field_name : str
|
|
52
|
+
The name of the database field/column
|
|
53
|
+
values : List[str]
|
|
54
|
+
List of values that should match this field
|
|
55
|
+
|
|
56
|
+
Methods
|
|
57
|
+
-------
|
|
58
|
+
to_sql_filter()
|
|
59
|
+
Generate SQL filter string for this field mapping.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
field_name: str
|
|
63
|
+
values: List[str]
|
|
64
|
+
|
|
65
|
+
def to_sql_filter(self) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Generate SQL filter string for this field mapping.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
str
|
|
72
|
+
SQL WHERE clause condition string for this field mapping.
|
|
73
|
+
"""
|
|
74
|
+
quoted_values = ", ".join([f"'{v}'" for v in self.values])
|
|
75
|
+
return f"`{self.field_name}` IN ({quoted_values})"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TypeMapping(BaseModel):
|
|
79
|
+
"""
|
|
80
|
+
Container for multiple field mappings that define object type filtering.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
mappings : List[FieldMapping]
|
|
85
|
+
List of field mappings that together define the type criteria
|
|
86
|
+
|
|
87
|
+
Methods
|
|
88
|
+
-------
|
|
89
|
+
add_mapping(field_name, values)
|
|
90
|
+
Add a new field mapping.
|
|
91
|
+
to_sql_filter()
|
|
92
|
+
Generate combined SQL filter string from all mappings.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
mappings: List[FieldMapping] = Field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
def add_mapping(self, field_name: str, values: List[str]) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Add a new field mapping.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
field_name : str
|
|
104
|
+
Name of the database field.
|
|
105
|
+
values : List[str]
|
|
106
|
+
List of allowed values for this field.
|
|
107
|
+
"""
|
|
108
|
+
self.mappings.append(FieldMapping(field_name=field_name, values=values))
|
|
109
|
+
|
|
110
|
+
def to_sql_filter(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Generate combined SQL filter string from all mappings.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
str
|
|
117
|
+
Combined SQL WHERE clause condition string.
|
|
118
|
+
"""
|
|
119
|
+
if not self.mappings:
|
|
120
|
+
return ""
|
|
121
|
+
filter_parts = [mapping.to_sql_filter() for mapping in self.mappings]
|
|
122
|
+
return " AND ".join(filter_parts)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MetricModel(BaseModel):
|
|
126
|
+
"""
|
|
127
|
+
Represents a metric configuration for infometric analysis.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
name : str
|
|
132
|
+
The short name of the metric.
|
|
133
|
+
long_name : Optional[str], default=None
|
|
134
|
+
The long descriptive name of the metric. Defaults to `name` if not provided.
|
|
135
|
+
show_in_metrics_table : Optional[bool], default=True
|
|
136
|
+
Indicates whether the metric should be displayed in the metrics table.
|
|
137
|
+
description : Optional[str], default=None
|
|
138
|
+
A detailed description of the metric. Defaults to `name` if not provided.
|
|
139
|
+
select : str
|
|
140
|
+
The SQL select statement or expression for the metric.
|
|
141
|
+
filter : Optional[str], default=""
|
|
142
|
+
An optional SQL filter to apply to the metric. Defaults to no filter.
|
|
143
|
+
|
|
144
|
+
Methods
|
|
145
|
+
-------
|
|
146
|
+
set_defaults(value, info)
|
|
147
|
+
Sets default values for `long_name` and `description` fields using the `name` field if they are not provided.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
name: str
|
|
151
|
+
long_name: Optional[str] = None
|
|
152
|
+
show_in_metrics_table: Optional[bool] = True
|
|
153
|
+
description: Optional[str] = None
|
|
154
|
+
select: str
|
|
155
|
+
filter: Optional[str] = "" # This defaults to no filter
|
|
156
|
+
|
|
157
|
+
@field_validator("long_name", "description", mode="after")
|
|
158
|
+
@classmethod
|
|
159
|
+
def set_defaults(cls, value, info):
|
|
160
|
+
"""
|
|
161
|
+
Set default values for long_name and description fields.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
value : Any
|
|
166
|
+
The current field value.
|
|
167
|
+
info : Any
|
|
168
|
+
Field validation info containing all field values.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
str
|
|
173
|
+
The field value or the default value from 'name' field.
|
|
174
|
+
"""
|
|
175
|
+
# info.data contains all field values
|
|
176
|
+
if value is None:
|
|
177
|
+
# Use 'name' field as default
|
|
178
|
+
return info.data.get("name")
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ImpactCategoriesModel(BaseModel):
|
|
183
|
+
"""
|
|
184
|
+
Model for defining impact categories with associated colors, field, unit, and bins.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
categories : list[str], default=["Minor", "Major", "Severe"]
|
|
189
|
+
List of impact category names.
|
|
190
|
+
colors : Optional[list[str]], default=["#ffa500", "#ff0000", "#000000"]
|
|
191
|
+
List of colors corresponding to each category.
|
|
192
|
+
field : str
|
|
193
|
+
The database field name used for categorization.
|
|
194
|
+
unit : str
|
|
195
|
+
The unit of measurement for the field.
|
|
196
|
+
bins : list[float]
|
|
197
|
+
List of threshold values for binning the field values.
|
|
198
|
+
|
|
199
|
+
Methods
|
|
200
|
+
-------
|
|
201
|
+
validate_colors_length(colors, info)
|
|
202
|
+
Validate that colors list length matches categories list length.
|
|
203
|
+
validate_bins_length(bins, info)
|
|
204
|
+
Validate that bins list length is one less than categories list length.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
categories: list[str] = Field(default_factory=lambda: ["Minor", "Major", "Severe"])
|
|
208
|
+
colors: Optional[list[str]] = Field(
|
|
209
|
+
default_factory=lambda: ["#ffa500", "#ff0000", "#000000"]
|
|
210
|
+
)
|
|
211
|
+
field: str = _IMPACT_COLUMNS.inundation_depth
|
|
212
|
+
unit: str
|
|
213
|
+
bins: list[float]
|
|
214
|
+
|
|
215
|
+
@field_validator("colors", mode="before")
|
|
216
|
+
@classmethod
|
|
217
|
+
def validate_colors_length(cls, colors, info):
|
|
218
|
+
"""
|
|
219
|
+
Validate that colors list length matches categories list length.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
colors : list[str]
|
|
224
|
+
List of color values.
|
|
225
|
+
info : Any
|
|
226
|
+
Field validation info containing all field values.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
list[str]
|
|
231
|
+
The validated colors list.
|
|
232
|
+
|
|
233
|
+
Raises
|
|
234
|
+
------
|
|
235
|
+
ValueError
|
|
236
|
+
If colors length doesn't match categories length.
|
|
237
|
+
"""
|
|
238
|
+
categories = info.data.get("categories")
|
|
239
|
+
if categories and colors and len(colors) != len(categories):
|
|
240
|
+
raise ValueError("Length of 'colors' must match length of 'categories'.")
|
|
241
|
+
return colors
|
|
242
|
+
|
|
243
|
+
@field_validator("bins", mode="before")
|
|
244
|
+
@classmethod
|
|
245
|
+
def validate_bins_length(cls, bins, info):
|
|
246
|
+
"""
|
|
247
|
+
Validate that bins list length is one less than categories list length.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
bins : list[float]
|
|
252
|
+
List of bin threshold values.
|
|
253
|
+
info : Any
|
|
254
|
+
Field validation info containing all field values.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
list[float]
|
|
259
|
+
The validated bins list.
|
|
260
|
+
|
|
261
|
+
Raises
|
|
262
|
+
------
|
|
263
|
+
ValueError
|
|
264
|
+
If bins length is not one less than categories length.
|
|
265
|
+
"""
|
|
266
|
+
categories = info.data.get("categories")
|
|
267
|
+
if categories and len(bins) != len(categories) - 1:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
"Length of 'bins' must be one less than length of 'categories'."
|
|
270
|
+
)
|
|
271
|
+
return bins
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class BuildingsInfographicModel(BaseModel):
|
|
275
|
+
"""
|
|
276
|
+
Model for building infographic configuration.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
types : list[str]
|
|
281
|
+
List of building types.
|
|
282
|
+
icons : list[str]
|
|
283
|
+
List of icon names corresponding to each building type.
|
|
284
|
+
type_mapping : dict[str, TypeMapping]
|
|
285
|
+
Mapping of building types to their database filtering criteria.
|
|
286
|
+
impact_categories : ImpactCategoriesModel
|
|
287
|
+
Impact categories configuration.
|
|
288
|
+
|
|
289
|
+
Methods
|
|
290
|
+
-------
|
|
291
|
+
validate_icons_length(icons, info)
|
|
292
|
+
Validate that icons list length matches types list length.
|
|
293
|
+
get_template(type)
|
|
294
|
+
Get a pre-configured template for OSM or NSI building types.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
# Define building types
|
|
298
|
+
types: list[str]
|
|
299
|
+
icons: list[str]
|
|
300
|
+
type_mapping: dict[str, TypeMapping]
|
|
301
|
+
# Define impact categories
|
|
302
|
+
impact_categories: ImpactCategoriesModel
|
|
303
|
+
|
|
304
|
+
@field_validator("icons", mode="before")
|
|
305
|
+
@classmethod
|
|
306
|
+
def validate_icons_length(cls, icons, info):
|
|
307
|
+
"""
|
|
308
|
+
Validate that icons list length matches types list length.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
icons : list[str]
|
|
313
|
+
List of icon names.
|
|
314
|
+
info : Any
|
|
315
|
+
Field validation info containing all field values.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
list[str]
|
|
320
|
+
The validated icons list.
|
|
321
|
+
|
|
322
|
+
Raises
|
|
323
|
+
------
|
|
324
|
+
ValueError
|
|
325
|
+
If icons length doesn't match types length.
|
|
326
|
+
"""
|
|
327
|
+
types = info.data.get("types")
|
|
328
|
+
if types and len(icons) != len(types):
|
|
329
|
+
raise ValueError("Length of 'icons' must equal to the length of 'types'.")
|
|
330
|
+
return icons
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def get_template(type: Literal["OSM", "NSI"]):
|
|
334
|
+
"""
|
|
335
|
+
Get a pre-configured template for building infographics.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
type : Literal["OSM", "NSI"]
|
|
340
|
+
The database type to create a template for.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
BuildingsInfographicModel
|
|
345
|
+
Pre-configured building infographic model.
|
|
346
|
+
"""
|
|
347
|
+
if type == "OSM":
|
|
348
|
+
config = BuildingsInfographicModel(
|
|
349
|
+
types=["Residential", "Commercial", "Industrial"],
|
|
350
|
+
icons=["house", "cart", "factory"],
|
|
351
|
+
type_mapping={
|
|
352
|
+
"Residential": TypeMapping(
|
|
353
|
+
mappings=[
|
|
354
|
+
FieldMapping(
|
|
355
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
356
|
+
values=["residential"],
|
|
357
|
+
)
|
|
358
|
+
]
|
|
359
|
+
),
|
|
360
|
+
"Commercial": TypeMapping(
|
|
361
|
+
mappings=[
|
|
362
|
+
FieldMapping(
|
|
363
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
364
|
+
values=["commercial"],
|
|
365
|
+
)
|
|
366
|
+
]
|
|
367
|
+
),
|
|
368
|
+
"Industrial": TypeMapping(
|
|
369
|
+
mappings=[
|
|
370
|
+
FieldMapping(
|
|
371
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
372
|
+
values=["industrial"],
|
|
373
|
+
)
|
|
374
|
+
]
|
|
375
|
+
),
|
|
376
|
+
},
|
|
377
|
+
impact_categories=ImpactCategoriesModel(
|
|
378
|
+
unit="meters", bins=[0.25, 1.5]
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
elif type == "NSI":
|
|
382
|
+
config = BuildingsInfographicModel(
|
|
383
|
+
types=[
|
|
384
|
+
"Residential",
|
|
385
|
+
"Commercial",
|
|
386
|
+
"Health facilities",
|
|
387
|
+
"Schools",
|
|
388
|
+
"Emergency facilities",
|
|
389
|
+
],
|
|
390
|
+
icons=["house", "cart", "hospital", "school", "firetruck"],
|
|
391
|
+
type_mapping={
|
|
392
|
+
"Residential": TypeMapping(
|
|
393
|
+
mappings=[
|
|
394
|
+
FieldMapping(
|
|
395
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
396
|
+
values=["RES"],
|
|
397
|
+
)
|
|
398
|
+
]
|
|
399
|
+
),
|
|
400
|
+
"Commercial": TypeMapping(
|
|
401
|
+
mappings=[
|
|
402
|
+
FieldMapping(
|
|
403
|
+
field_name="Secondary Object Type",
|
|
404
|
+
values=[
|
|
405
|
+
"COM1",
|
|
406
|
+
"COM2",
|
|
407
|
+
"COM3",
|
|
408
|
+
"COM4",
|
|
409
|
+
"COM5",
|
|
410
|
+
"COM8",
|
|
411
|
+
"COM9",
|
|
412
|
+
],
|
|
413
|
+
)
|
|
414
|
+
]
|
|
415
|
+
),
|
|
416
|
+
"Health facilities": TypeMapping(
|
|
417
|
+
mappings=[
|
|
418
|
+
FieldMapping(
|
|
419
|
+
field_name="Secondary Object Type",
|
|
420
|
+
values=["RES6", "COM6", "COM7"],
|
|
421
|
+
)
|
|
422
|
+
]
|
|
423
|
+
),
|
|
424
|
+
"Schools": TypeMapping(
|
|
425
|
+
mappings=[
|
|
426
|
+
FieldMapping(
|
|
427
|
+
field_name="Secondary Object Type",
|
|
428
|
+
values=["EDU1", "EDU2"],
|
|
429
|
+
)
|
|
430
|
+
]
|
|
431
|
+
),
|
|
432
|
+
"Emergency facilities": TypeMapping(
|
|
433
|
+
mappings=[
|
|
434
|
+
FieldMapping(
|
|
435
|
+
field_name="Secondary Object Type", values=["GOV2"]
|
|
436
|
+
)
|
|
437
|
+
]
|
|
438
|
+
),
|
|
439
|
+
},
|
|
440
|
+
impact_categories=ImpactCategoriesModel(unit="feet", bins=[1, 6]),
|
|
441
|
+
)
|
|
442
|
+
return config
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class SviModel(BaseModel):
|
|
446
|
+
"""
|
|
447
|
+
Model for Social Vulnerability Index (SVI) configuration.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
classes : list[str], default=["Low", "High"]
|
|
452
|
+
List of vulnerability class names.
|
|
453
|
+
colors : list[str], default=["#D5DEE1", "#88A2AA"]
|
|
454
|
+
List of colors corresponding to each vulnerability class.
|
|
455
|
+
thresholds : list[float], default=[0.7]
|
|
456
|
+
List of threshold values for vulnerability classification.
|
|
457
|
+
|
|
458
|
+
Methods
|
|
459
|
+
-------
|
|
460
|
+
validate_colors_length(colors, info)
|
|
461
|
+
Validate that colors list length matches classes list length.
|
|
462
|
+
validate_thresholds_length(thresholds, info)
|
|
463
|
+
Validate that thresholds list length is one less than classes list length.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
classes: list[str] = Field(default_factory=lambda: ["Low", "High"])
|
|
467
|
+
colors: list[str] = Field(default_factory=lambda: ["#D5DEE1", "#88A2AA"])
|
|
468
|
+
thresholds: list[float] = Field(default_factory=lambda: [0.7])
|
|
469
|
+
|
|
470
|
+
@field_validator("colors", mode="before")
|
|
471
|
+
@classmethod
|
|
472
|
+
def validate_colors_length(cls, colors, info):
|
|
473
|
+
"""
|
|
474
|
+
Validate that colors list length matches classes list length.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
colors : list[str]
|
|
479
|
+
List of color values.
|
|
480
|
+
info : Any
|
|
481
|
+
Field validation info containing all field values.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
list[str]
|
|
486
|
+
The validated colors list.
|
|
487
|
+
|
|
488
|
+
Raises
|
|
489
|
+
------
|
|
490
|
+
ValueError
|
|
491
|
+
If colors length doesn't match classes length.
|
|
492
|
+
"""
|
|
493
|
+
classes = info.data.get("classes")
|
|
494
|
+
if classes and colors and len(colors) != len(classes):
|
|
495
|
+
raise ValueError("Length of 'colors' must match length of 'classes'.")
|
|
496
|
+
return colors
|
|
497
|
+
|
|
498
|
+
@field_validator("thresholds", mode="before")
|
|
499
|
+
@classmethod
|
|
500
|
+
def validate_thresholds_length(cls, thresholds, info):
|
|
501
|
+
"""
|
|
502
|
+
Validate that thresholds list length is one less than classes list length.
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
thresholds : list[float]
|
|
507
|
+
List of threshold values.
|
|
508
|
+
info : Any
|
|
509
|
+
Field validation info containing all field values.
|
|
510
|
+
|
|
511
|
+
Returns
|
|
512
|
+
-------
|
|
513
|
+
list[float]
|
|
514
|
+
The validated thresholds list.
|
|
515
|
+
|
|
516
|
+
Raises
|
|
517
|
+
------
|
|
518
|
+
ValueError
|
|
519
|
+
If thresholds length is not one less than classes length.
|
|
520
|
+
"""
|
|
521
|
+
classes = info.data.get("classes")
|
|
522
|
+
if classes and len(thresholds) != len(classes) - 1:
|
|
523
|
+
raise ValueError(
|
|
524
|
+
"Length of 'thresholds' must be one less than length of 'classes'."
|
|
525
|
+
)
|
|
526
|
+
return thresholds
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class HomesInfographicModel(BaseModel):
|
|
530
|
+
"""
|
|
531
|
+
Model for Homes and SVI (Social Vulnerability Index) infographic configuration.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
svi : SviModel
|
|
536
|
+
SVI classification configuration.
|
|
537
|
+
mapping : TypeMapping
|
|
538
|
+
Database field mapping for filtering relevant objects.
|
|
539
|
+
impact_categories : ImpactCategoriesModel
|
|
540
|
+
Impact categories configuration.
|
|
541
|
+
|
|
542
|
+
Methods
|
|
543
|
+
-------
|
|
544
|
+
get_template(svi_threshold, type)
|
|
545
|
+
Get a pre-configured template for SVI infographics.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
svi: Optional[SviModel] = None
|
|
549
|
+
mapping: TypeMapping
|
|
550
|
+
impact_categories: ImpactCategoriesModel
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def get_template(
|
|
554
|
+
type: Literal["OSM", "NSI"] = "OSM", svi_threshold: Optional[float] = None
|
|
555
|
+
):
|
|
556
|
+
"""
|
|
557
|
+
Get a pre-configured template for SVI infographics.
|
|
558
|
+
|
|
559
|
+
Parameters
|
|
560
|
+
----------
|
|
561
|
+
svi_threshold : Optional[float], default=None
|
|
562
|
+
The SVI threshold value for vulnerability classification. If not provided, SVI will be None.
|
|
563
|
+
type : Literal["OSM", "NSI"], default="OSM"
|
|
564
|
+
The database type to create a template for.
|
|
565
|
+
|
|
566
|
+
Returns
|
|
567
|
+
-------
|
|
568
|
+
HomesInfographicModel
|
|
569
|
+
Pre-configured Homes infographic model.
|
|
570
|
+
"""
|
|
571
|
+
if svi_threshold is not None:
|
|
572
|
+
svi_model = SviModel(thresholds=[svi_threshold])
|
|
573
|
+
else:
|
|
574
|
+
svi_model = None
|
|
575
|
+
|
|
576
|
+
if type == "OSM":
|
|
577
|
+
config = HomesInfographicModel(
|
|
578
|
+
svi=svi_model,
|
|
579
|
+
mapping=TypeMapping(
|
|
580
|
+
mappings=[
|
|
581
|
+
FieldMapping(
|
|
582
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
583
|
+
values=["residential"],
|
|
584
|
+
)
|
|
585
|
+
]
|
|
586
|
+
),
|
|
587
|
+
impact_categories=ImpactCategoriesModel(
|
|
588
|
+
categories=["Flooded", "Displaced"],
|
|
589
|
+
colors=None,
|
|
590
|
+
unit="meters",
|
|
591
|
+
bins=[1.5],
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
elif type == "NSI":
|
|
595
|
+
config = HomesInfographicModel(
|
|
596
|
+
svi=svi_model,
|
|
597
|
+
mapping=TypeMapping(
|
|
598
|
+
mappings=[
|
|
599
|
+
FieldMapping(
|
|
600
|
+
field_name=_IMPACT_COLUMNS.primary_object_type,
|
|
601
|
+
values=["RES"],
|
|
602
|
+
)
|
|
603
|
+
]
|
|
604
|
+
),
|
|
605
|
+
impact_categories=ImpactCategoriesModel(
|
|
606
|
+
categories=["Flooded", "Displaced"],
|
|
607
|
+
colors=None,
|
|
608
|
+
unit="feet",
|
|
609
|
+
bins=[6],
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
return config
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class RoadsInfographicModel(BaseModel):
|
|
616
|
+
"""
|
|
617
|
+
Model for roads infographic configuration.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
categories : list[str], default=["Slight", "Minor", "Major", "Severe"]
|
|
622
|
+
List of road impact category names.
|
|
623
|
+
colors : list[str], default=["#e0f7fa", "#80deea", "#26c6da", "#006064"]
|
|
624
|
+
List of colors corresponding to each category.
|
|
625
|
+
icons : list[str], default=["walking_person", "car", "truck", "ambulance"]
|
|
626
|
+
List of icon names for each category.
|
|
627
|
+
users : list[str], default=["Pedestrians", "Cars", "Trucks", "Rescue vehicles"]
|
|
628
|
+
List of road user types for each category.
|
|
629
|
+
thresholds : list[float]
|
|
630
|
+
List of threshold values for categorizing road impacts.
|
|
631
|
+
field : str, default=_IMPACT_COLUMNS.inundation_depth
|
|
632
|
+
The database field name used for categorization.
|
|
633
|
+
unit : str
|
|
634
|
+
The unit of measurement for the field.
|
|
635
|
+
road_length_field : str, default=_IMPACT_COLUMNS.segment_length
|
|
636
|
+
The database field name containing road segment lengths.
|
|
637
|
+
|
|
638
|
+
Methods
|
|
639
|
+
-------
|
|
640
|
+
validate_lengths(v, info)
|
|
641
|
+
Validate that all list attributes have the same length.
|
|
642
|
+
get_template(unit_system)
|
|
643
|
+
Get a pre-configured template for metric or imperial units.
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
categories: list[str] = Field(
|
|
647
|
+
default_factory=lambda: ["Slight", "Minor", "Major", "Severe"]
|
|
648
|
+
)
|
|
649
|
+
colors: list[str] = Field(
|
|
650
|
+
default_factory=lambda: ["#D5DEE1", "#D5DEE1", "#D5DEE1", "#D5DEE1"]
|
|
651
|
+
)
|
|
652
|
+
icons: list[str] = Field(
|
|
653
|
+
default_factory=lambda: ["walking_person", "car", "truck", "ambulance"]
|
|
654
|
+
)
|
|
655
|
+
users: list[str] = Field(
|
|
656
|
+
default_factory=lambda: ["Pedestrians", "Cars", "Trucks", "Rescue vehicles"]
|
|
657
|
+
)
|
|
658
|
+
thresholds: list[float]
|
|
659
|
+
field: str = _IMPACT_COLUMNS.inundation_depth
|
|
660
|
+
unit: str
|
|
661
|
+
road_length_field: str = _IMPACT_COLUMNS.segment_length
|
|
662
|
+
|
|
663
|
+
@field_validator("categories", mode="after")
|
|
664
|
+
@classmethod
|
|
665
|
+
def validate_lengths(cls, v, info):
|
|
666
|
+
"""
|
|
667
|
+
Validate that all list attributes have the same length.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
v : list[str]
|
|
672
|
+
The categories list.
|
|
673
|
+
info : Any
|
|
674
|
+
Field validation info containing all field values.
|
|
675
|
+
|
|
676
|
+
Returns
|
|
677
|
+
-------
|
|
678
|
+
list[str]
|
|
679
|
+
The validated categories list.
|
|
680
|
+
|
|
681
|
+
Raises
|
|
682
|
+
------
|
|
683
|
+
ValueError
|
|
684
|
+
If list attributes don't have the same length.
|
|
685
|
+
"""
|
|
686
|
+
# Check that categories, colors, icons, users, thresholds have the same length
|
|
687
|
+
attrs = ["categories", "colors", "icons", "users", "thresholds"]
|
|
688
|
+
lengths = [len(info.data.get(attr, [])) for attr in attrs]
|
|
689
|
+
if len(set(lengths)) > 1:
|
|
690
|
+
raise ValueError(
|
|
691
|
+
f"Attributes {attrs} must all have the same length, got lengths: {lengths}"
|
|
692
|
+
)
|
|
693
|
+
return v
|
|
694
|
+
|
|
695
|
+
@staticmethod
|
|
696
|
+
def get_template(unit_system: Literal["metric", "imperial"]):
|
|
697
|
+
"""
|
|
698
|
+
Get a pre-configured template for roads infographics.
|
|
699
|
+
|
|
700
|
+
Parameters
|
|
701
|
+
----------
|
|
702
|
+
unit_system : Literal["metric", "imperial"]
|
|
703
|
+
The unit system to use for thresholds and measurements.
|
|
704
|
+
|
|
705
|
+
Returns
|
|
706
|
+
-------
|
|
707
|
+
RoadsInfographicModel
|
|
708
|
+
Pre-configured roads infographic model.
|
|
709
|
+
"""
|
|
710
|
+
if unit_system == "metric":
|
|
711
|
+
config = RoadsInfographicModel(
|
|
712
|
+
thresholds=[0.1, 0.2, 0.4, 0.8],
|
|
713
|
+
unit="meters",
|
|
714
|
+
)
|
|
715
|
+
elif unit_system == "imperial":
|
|
716
|
+
config = RoadsInfographicModel(
|
|
717
|
+
thresholds=[0.3, 0.5, 1, 2],
|
|
718
|
+
unit="feet",
|
|
719
|
+
)
|
|
720
|
+
return config
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class EventInfographicModel(BaseModel):
|
|
724
|
+
"""
|
|
725
|
+
Model for event-based infographic configuration.
|
|
726
|
+
|
|
727
|
+
Parameters
|
|
728
|
+
----------
|
|
729
|
+
buildings : Optional[BuildingsInfographicModel], default=None
|
|
730
|
+
Buildings infographic configuration.
|
|
731
|
+
svi : Optional[SviInfographicModel], default=None
|
|
732
|
+
SVI infographic configuration.
|
|
733
|
+
roads : Optional[RoadsInfographicModel], default=None
|
|
734
|
+
Roads infographic configuration.
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
buildings: Optional[BuildingsInfographicModel] = None
|
|
738
|
+
svi: Optional[HomesInfographicModel] = None
|
|
739
|
+
roads: Optional[RoadsInfographicModel] = None
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class FloodExceedanceModel(BaseModel):
|
|
743
|
+
"""
|
|
744
|
+
Model for flood exceedance probability configuration.
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
column : str, default=_IMPACT_COLUMNS.inundation_depth
|
|
749
|
+
The database column name for flood depth measurements.
|
|
750
|
+
threshold : float, default=0.1
|
|
751
|
+
The flood depth threshold value.
|
|
752
|
+
unit : str, default="meters"
|
|
753
|
+
The unit of measurement for the threshold.
|
|
754
|
+
period : int, default=30
|
|
755
|
+
The time period in years for exceedance analysis.
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
column: str = _IMPACT_COLUMNS.inundation_depth
|
|
759
|
+
threshold: float = 0.1
|
|
760
|
+
unit: str = "meters"
|
|
761
|
+
period: int = 30
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class RiskInfographicModel(BaseModel):
|
|
765
|
+
"""
|
|
766
|
+
Model for risk-based infographic configuration.
|
|
767
|
+
|
|
768
|
+
Parameters
|
|
769
|
+
----------
|
|
770
|
+
homes : HomesInfographicModel
|
|
771
|
+
Homes infographic configuration.
|
|
772
|
+
flood_exceedances : FloodExceedanceModel
|
|
773
|
+
Flood exceedance configuration.
|
|
774
|
+
|
|
775
|
+
Methods
|
|
776
|
+
-------
|
|
777
|
+
get_template(type, svi_threshold)
|
|
778
|
+
Get a pre-configured template for risk infographics.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
homes: HomesInfographicModel
|
|
782
|
+
flood_exceedance: FloodExceedanceModel
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def get_template(
|
|
786
|
+
type: Literal["OSM", "NSI"], svi_threshold: Optional[float] = None
|
|
787
|
+
):
|
|
788
|
+
"""
|
|
789
|
+
Get a pre-configured template for risk infographics.
|
|
790
|
+
|
|
791
|
+
Parameters
|
|
792
|
+
----------
|
|
793
|
+
type : Literal["OSM", "NSI"]
|
|
794
|
+
The database type to create a template for.
|
|
795
|
+
svi_threshold : Optional[float], default=None
|
|
796
|
+
The SVI threshold value for vulnerability classification.
|
|
797
|
+
|
|
798
|
+
Returns
|
|
799
|
+
-------
|
|
800
|
+
RiskInfographicModel
|
|
801
|
+
Pre-configured risk infographic model.
|
|
802
|
+
"""
|
|
803
|
+
if type == "OSM":
|
|
804
|
+
config = RiskInfographicModel(
|
|
805
|
+
homes=HomesInfographicModel.get_template(
|
|
806
|
+
type="OSM", svi_threshold=svi_threshold
|
|
807
|
+
),
|
|
808
|
+
flood_exceedance=FloodExceedanceModel(),
|
|
809
|
+
)
|
|
810
|
+
elif type == "NSI":
|
|
811
|
+
config = RiskInfographicModel(
|
|
812
|
+
homes=HomesInfographicModel.get_template(
|
|
813
|
+
type="NSI", svi_threshold=svi_threshold
|
|
814
|
+
),
|
|
815
|
+
flood_exceedance=FloodExceedanceModel(unit="feet", threshold=0.2),
|
|
816
|
+
)
|
|
817
|
+
return config
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def get_filter(
|
|
821
|
+
type_mapping: TypeMapping,
|
|
822
|
+
cat_field: str,
|
|
823
|
+
cat_idx: int,
|
|
824
|
+
bins: list[float],
|
|
825
|
+
base_filt="",
|
|
826
|
+
) -> str:
|
|
827
|
+
"""
|
|
828
|
+
Construct a SQL filter string based on provided type mapping and category criteria.
|
|
829
|
+
|
|
830
|
+
Parameters
|
|
831
|
+
----------
|
|
832
|
+
type_mapping : TypeMapping
|
|
833
|
+
TypeMapping object containing field mappings to filter on.
|
|
834
|
+
cat_field : str
|
|
835
|
+
Name of the field representing the category in the database.
|
|
836
|
+
cat_idx : int
|
|
837
|
+
Index indicating which category bin to use for filtering.
|
|
838
|
+
bins : list[float]
|
|
839
|
+
List of bin thresholds for the category field.
|
|
840
|
+
base_filt : str, default=""
|
|
841
|
+
Additional base filter string to prepend.
|
|
842
|
+
|
|
843
|
+
Returns
|
|
844
|
+
-------
|
|
845
|
+
str
|
|
846
|
+
A SQL filter string combining type and category conditions.
|
|
847
|
+
"""
|
|
848
|
+
# Build type filters using TypeMapping
|
|
849
|
+
type_filter = type_mapping.to_sql_filter()
|
|
850
|
+
|
|
851
|
+
# Add category filter
|
|
852
|
+
if cat_idx == 0:
|
|
853
|
+
cat_filter = f"`{cat_field}` <= {bins[0]}"
|
|
854
|
+
elif cat_idx == len(bins):
|
|
855
|
+
cat_filter = f"`{cat_field}` > {bins[-1]}"
|
|
856
|
+
else:
|
|
857
|
+
cat_filter = (
|
|
858
|
+
f"`{cat_field}` <= {bins[cat_idx]} AND `{cat_field}` > {bins[cat_idx-1]}"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
return combine_filters(base_filt, type_filter, cat_filter)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
class Metrics:
|
|
865
|
+
"""Main class for managing impact metrics configuration and generation."""
|
|
866
|
+
|
|
867
|
+
def __init__(self, dmg_unit: str, return_periods: list[float]):
|
|
868
|
+
"""
|
|
869
|
+
Initialize the Metrics class.
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
dmg_unit : str
|
|
874
|
+
The unit of measurement for damage values.
|
|
875
|
+
return_periods : list[float]
|
|
876
|
+
List of return periods in years for risk analysis.
|
|
877
|
+
"""
|
|
878
|
+
self.dmg_unit = dmg_unit
|
|
879
|
+
self.return_periods = return_periods
|
|
880
|
+
|
|
881
|
+
# Initialize all metric lists as empty instance attributes
|
|
882
|
+
self.mandatory_metrics_event: list[MetricModel] = []
|
|
883
|
+
self.mandatory_metrics_risk: list[MetricModel] = []
|
|
884
|
+
self.additional_metrics_event: list[MetricModel] = []
|
|
885
|
+
self.additional_metrics_risk: list[MetricModel] = []
|
|
886
|
+
self.infographics_metrics_event: list[MetricModel] = []
|
|
887
|
+
self.infographics_metrics_risk: list[MetricModel] = []
|
|
888
|
+
self.additional_risk_configs: dict = {}
|
|
889
|
+
self.infographics_config: dict = {}
|
|
890
|
+
|
|
891
|
+
@staticmethod
|
|
892
|
+
def write_metrics(metrics, path, aggr_levels=[]):
|
|
893
|
+
"""
|
|
894
|
+
Write metrics configuration to a TOML file.
|
|
895
|
+
|
|
896
|
+
Parameters
|
|
897
|
+
----------
|
|
898
|
+
metrics : list[MetricModel]
|
|
899
|
+
List of metric models to write.
|
|
900
|
+
path : Union[str, Path]
|
|
901
|
+
Path to the output TOML file.
|
|
902
|
+
aggr_levels : list[str], default=[]
|
|
903
|
+
List of aggregation levels.
|
|
904
|
+
"""
|
|
905
|
+
attrs = {}
|
|
906
|
+
attrs["aggregateBy"] = aggr_levels
|
|
907
|
+
attrs["queries"] = [metric.model_dump() for metric in metrics]
|
|
908
|
+
|
|
909
|
+
# Save metrics configuration
|
|
910
|
+
with open(path, "wb") as f:
|
|
911
|
+
tomli_w.dump(attrs, f)
|
|
912
|
+
|
|
913
|
+
def write(
|
|
914
|
+
self,
|
|
915
|
+
metrics_path: Union[str, Path, PathLike],
|
|
916
|
+
aggregation_levels: List[str],
|
|
917
|
+
infographics_path: Optional[Union[str, Path, PathLike]] = None,
|
|
918
|
+
) -> None:
|
|
919
|
+
"""
|
|
920
|
+
Write all metrics (mandatory, additional, and infographics) to TOML files.
|
|
921
|
+
|
|
922
|
+
Parameters
|
|
923
|
+
----------
|
|
924
|
+
metrics_path : Union[str, Path, PathLike]
|
|
925
|
+
The directory path where the metrics configuration files will be saved.
|
|
926
|
+
aggregation_levels : List[str]
|
|
927
|
+
A list of aggregation levels to include in the metrics configuration files.
|
|
928
|
+
infographics_path : Optional[Union[str, Path, PathLike]], default=None
|
|
929
|
+
The directory path where infographics configuration files will be saved.
|
|
930
|
+
Required if infographics configurations are present.
|
|
931
|
+
|
|
932
|
+
Raises
|
|
933
|
+
------
|
|
934
|
+
ValueError
|
|
935
|
+
If infographics_path is None but infographics configurations exist.
|
|
936
|
+
"""
|
|
937
|
+
path_im = Path(metrics_path)
|
|
938
|
+
path_im.mkdir(parents=True, exist_ok=True)
|
|
939
|
+
|
|
940
|
+
# Write mandatory event metrics
|
|
941
|
+
self.write_metrics(
|
|
942
|
+
self.mandatory_metrics_event,
|
|
943
|
+
path_im / "mandatory_metrics_config.toml",
|
|
944
|
+
aggregation_levels,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
# Write mandatory risk metrics
|
|
948
|
+
if self.mandatory_metrics_risk:
|
|
949
|
+
self.write_metrics(
|
|
950
|
+
self.mandatory_metrics_risk,
|
|
951
|
+
path_im / "mandatory_metrics_config_risk.toml",
|
|
952
|
+
aggregation_levels,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
# Write additional event metrics if any
|
|
956
|
+
if self.additional_metrics_event:
|
|
957
|
+
self.write_metrics(
|
|
958
|
+
self.additional_metrics_event,
|
|
959
|
+
path_im / "additional_metrics_config.toml",
|
|
960
|
+
aggregation_levels,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Write additional risk metrics if any
|
|
964
|
+
if self.additional_metrics_risk:
|
|
965
|
+
self.write_metrics(
|
|
966
|
+
self.additional_metrics_risk,
|
|
967
|
+
path_im / "additional_metrics_config_risk.toml",
|
|
968
|
+
aggregation_levels,
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
# Write infographics event metrics if any
|
|
972
|
+
if (
|
|
973
|
+
hasattr(self, "infographics_metrics_event")
|
|
974
|
+
and self.infographics_metrics_event
|
|
975
|
+
):
|
|
976
|
+
self.write_metrics(
|
|
977
|
+
self.infographics_metrics_event,
|
|
978
|
+
path_im / "infographic_metrics_config.toml",
|
|
979
|
+
aggregation_levels,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
# Write infographics risk metrics if any
|
|
983
|
+
if (
|
|
984
|
+
hasattr(self, "infographics_metrics_risk")
|
|
985
|
+
and self.infographics_metrics_risk
|
|
986
|
+
):
|
|
987
|
+
self.write_metrics(
|
|
988
|
+
self.infographics_metrics_risk,
|
|
989
|
+
path_im / "infographic_metrics_config_risk.toml",
|
|
990
|
+
aggregation_levels,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Save additional risk configurations if available
|
|
994
|
+
if hasattr(self, "additional_risk_configs") and self.additional_risk_configs:
|
|
995
|
+
with open(path_im / "metrics_additional_risk_configs.toml", "wb") as f:
|
|
996
|
+
tomli_w.dump(self.additional_risk_configs, f)
|
|
997
|
+
|
|
998
|
+
# Save infographics configuration if available
|
|
999
|
+
if self.infographics_config:
|
|
1000
|
+
if infographics_path is None:
|
|
1001
|
+
raise ValueError(
|
|
1002
|
+
"infographics_path must be provided to save infographics configuration."
|
|
1003
|
+
)
|
|
1004
|
+
infographics_path = Path(infographics_path)
|
|
1005
|
+
infographics_path.mkdir(parents=True, exist_ok=True)
|
|
1006
|
+
if "buildings" in self.infographics_config:
|
|
1007
|
+
with open(infographics_path / "config_charts.toml", "wb") as f:
|
|
1008
|
+
tomli_w.dump(self.infographics_config["buildings"], f)
|
|
1009
|
+
if "svi" in self.infographics_config:
|
|
1010
|
+
with open(infographics_path / "config_people.toml", "wb") as f:
|
|
1011
|
+
tomli_w.dump(self.infographics_config["svi"], f)
|
|
1012
|
+
if "roads" in self.infographics_config:
|
|
1013
|
+
with open(infographics_path / "config_roads.toml", "wb") as f:
|
|
1014
|
+
tomli_w.dump(self.infographics_config["roads"], f)
|
|
1015
|
+
if "risk" in self.infographics_config:
|
|
1016
|
+
with open(infographics_path / "config_risk_charts.toml", "wb") as f:
|
|
1017
|
+
tomli_w.dump(self.infographics_config["risk"], f)
|
|
1018
|
+
|
|
1019
|
+
def create_mandatory_metrics_event(self) -> list[MetricModel]:
|
|
1020
|
+
"""
|
|
1021
|
+
Create mandatory metrics for event analysis.
|
|
1022
|
+
|
|
1023
|
+
Returns
|
|
1024
|
+
-------
|
|
1025
|
+
list[MetricModel]
|
|
1026
|
+
List of mandatory event metrics.
|
|
1027
|
+
"""
|
|
1028
|
+
self.mandatory_metrics_event.append(
|
|
1029
|
+
MetricModel(
|
|
1030
|
+
name="TotalDamageEvent",
|
|
1031
|
+
description="Total building damage",
|
|
1032
|
+
long_name=f"Total building damage ({self.dmg_unit})",
|
|
1033
|
+
select=f"SUM(`{_IMPACT_COLUMNS.total_damage}`)",
|
|
1034
|
+
filter="",
|
|
1035
|
+
show_in_metrics_table=True,
|
|
1036
|
+
)
|
|
1037
|
+
)
|
|
1038
|
+
return self.mandatory_metrics_event
|
|
1039
|
+
|
|
1040
|
+
def create_mandatory_metrics_risk(self) -> list[MetricModel]:
|
|
1041
|
+
"""
|
|
1042
|
+
Create mandatory metrics for risk analysis.
|
|
1043
|
+
|
|
1044
|
+
Returns
|
|
1045
|
+
-------
|
|
1046
|
+
list[MetricModel]
|
|
1047
|
+
List of mandatory risk metrics.
|
|
1048
|
+
"""
|
|
1049
|
+
self.mandatory_metrics_risk.append(
|
|
1050
|
+
MetricModel(
|
|
1051
|
+
name="ExpectedAnnualDamages",
|
|
1052
|
+
description="Expected annual damages",
|
|
1053
|
+
long_name=f"Expected annual damages ({self.dmg_unit})",
|
|
1054
|
+
select=f"SUM(`{_IMPACT_COLUMNS.risk_ead}`)",
|
|
1055
|
+
filter="",
|
|
1056
|
+
show_in_metrics_table=True,
|
|
1057
|
+
)
|
|
1058
|
+
)
|
|
1059
|
+
for rp in self.return_periods:
|
|
1060
|
+
self.mandatory_metrics_risk.append(
|
|
1061
|
+
MetricModel(
|
|
1062
|
+
name=f"TotalDamageRP{int(rp)}",
|
|
1063
|
+
description=f"Total damage with return period of {int(rp)} years",
|
|
1064
|
+
long_name=f"Total building damage - {int(rp)}Y ({self.dmg_unit})",
|
|
1065
|
+
select=f"SUM(`{_IMPACT_COLUMNS.total_damage_rp.format(years=int(rp))}`)",
|
|
1066
|
+
filter="",
|
|
1067
|
+
show_in_metrics_table=True,
|
|
1068
|
+
)
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
return self.mandatory_metrics_risk
|
|
1072
|
+
|
|
1073
|
+
def add_event_metric(self, metric: MetricModel) -> None:
|
|
1074
|
+
"""
|
|
1075
|
+
Add an additional event metric.
|
|
1076
|
+
|
|
1077
|
+
Parameters
|
|
1078
|
+
----------
|
|
1079
|
+
metric : MetricModel
|
|
1080
|
+
The metric to add to the additional event metrics list.
|
|
1081
|
+
|
|
1082
|
+
Raises
|
|
1083
|
+
------
|
|
1084
|
+
ValueError
|
|
1085
|
+
If a metric with the same name already exists.
|
|
1086
|
+
"""
|
|
1087
|
+
if any(m.name == metric.name for m in self.additional_metrics_event):
|
|
1088
|
+
raise ValueError(f"Event metric with name '{metric.name}' already exists.")
|
|
1089
|
+
self.additional_metrics_event.append(metric)
|
|
1090
|
+
|
|
1091
|
+
def add_risk_metric(self, metric: MetricModel) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Add an additional risk metric.
|
|
1094
|
+
|
|
1095
|
+
Parameters
|
|
1096
|
+
----------
|
|
1097
|
+
metric : MetricModel
|
|
1098
|
+
The metric to add to the additional risk metrics list.
|
|
1099
|
+
|
|
1100
|
+
Raises
|
|
1101
|
+
------
|
|
1102
|
+
ValueError
|
|
1103
|
+
If a metric with the same name already exists.
|
|
1104
|
+
"""
|
|
1105
|
+
if any(m.name == metric.name for m in self.additional_metrics_risk):
|
|
1106
|
+
raise ValueError(f"Risk metric with name '{metric.name}' already exists.")
|
|
1107
|
+
self.additional_metrics_risk.append(metric)
|
|
1108
|
+
|
|
1109
|
+
def create_infographics_metrics_event(
|
|
1110
|
+
self,
|
|
1111
|
+
config: EventInfographicModel,
|
|
1112
|
+
base_filt=f"`{_IMPACT_COLUMNS.total_damage}` > 0",
|
|
1113
|
+
) -> list[MetricModel]:
|
|
1114
|
+
"""
|
|
1115
|
+
Create infographic metrics for event analysis.
|
|
1116
|
+
|
|
1117
|
+
Parameters
|
|
1118
|
+
----------
|
|
1119
|
+
config : EventInfographicModel
|
|
1120
|
+
Configuration for event infographics.
|
|
1121
|
+
base_filt : str, default="`Total Damage` > 0"
|
|
1122
|
+
Base SQL filter to apply to all metrics.
|
|
1123
|
+
|
|
1124
|
+
Returns
|
|
1125
|
+
-------
|
|
1126
|
+
list[MetricModel]
|
|
1127
|
+
List of infographic event metrics.
|
|
1128
|
+
"""
|
|
1129
|
+
# Generate queries for all building types and categories
|
|
1130
|
+
if config.buildings:
|
|
1131
|
+
self._setup_buildings(config.buildings, base_filt)
|
|
1132
|
+
if config.svi:
|
|
1133
|
+
self._setup_svi(config.svi, base_filt)
|
|
1134
|
+
if config.roads:
|
|
1135
|
+
self._setup_roads(config.roads)
|
|
1136
|
+
return self.infographics_metrics_event
|
|
1137
|
+
|
|
1138
|
+
def create_infographics_metrics_risk(
|
|
1139
|
+
self,
|
|
1140
|
+
config: RiskInfographicModel,
|
|
1141
|
+
base_filt=f"`{_IMPACT_COLUMNS.risk_ead}` > 0",
|
|
1142
|
+
) -> list[MetricModel]:
|
|
1143
|
+
"""
|
|
1144
|
+
Create infographic metrics for risk analysis.
|
|
1145
|
+
|
|
1146
|
+
Parameters
|
|
1147
|
+
----------
|
|
1148
|
+
config : RiskInfographicModel
|
|
1149
|
+
Configuration for risk infographics.
|
|
1150
|
+
base_filt : str, default="`Risk (EAD)` > 0"
|
|
1151
|
+
Base SQL filter to apply to all metrics.
|
|
1152
|
+
|
|
1153
|
+
Returns
|
|
1154
|
+
-------
|
|
1155
|
+
list[MetricModel]
|
|
1156
|
+
List of infographic risk metrics.
|
|
1157
|
+
"""
|
|
1158
|
+
infographics_metrics_risk = []
|
|
1159
|
+
|
|
1160
|
+
# Get mapping from config.svi
|
|
1161
|
+
mapping = config.homes.mapping
|
|
1162
|
+
|
|
1163
|
+
# Build type filter string using TypeMapping
|
|
1164
|
+
type_cond = mapping.to_sql_filter()
|
|
1165
|
+
|
|
1166
|
+
# FloodedHomes (Exceedance Probability > 50)
|
|
1167
|
+
fe = config.flood_exceedance
|
|
1168
|
+
filter_str = combine_filters(
|
|
1169
|
+
"`Exceedance Probability` > 50", type_cond, base_filt
|
|
1170
|
+
)
|
|
1171
|
+
infographics_metrics_risk.append(
|
|
1172
|
+
MetricModel(
|
|
1173
|
+
name="LikelyFloodedHomes",
|
|
1174
|
+
description=f"Homes likely to flood ({fe.column} > {fe.threshold} {fe.unit}) in {fe.period} year period",
|
|
1175
|
+
select="COUNT(*)",
|
|
1176
|
+
filter=filter_str,
|
|
1177
|
+
long_name=f"Homes likely to flood in {fe.period}-year period (#)",
|
|
1178
|
+
show_in_metrics_table=True,
|
|
1179
|
+
)
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# ImpactedHomes for each RP - use class return periods and config SVI thresholds
|
|
1183
|
+
rps = self.return_periods
|
|
1184
|
+
if config.homes.svi is not None:
|
|
1185
|
+
svi_thresholds = config.homes.svi.thresholds
|
|
1186
|
+
svi_classes = config.homes.svi.classes
|
|
1187
|
+
else:
|
|
1188
|
+
svi_thresholds = []
|
|
1189
|
+
svi_classes = []
|
|
1190
|
+
|
|
1191
|
+
for rp in rps:
|
|
1192
|
+
# ImpactedHomes{RP} (all homes)
|
|
1193
|
+
filter_str = combine_filters(
|
|
1194
|
+
f"`{fe.column} ({int(rp)}Y)` >= {fe.threshold}", type_cond, base_filt
|
|
1195
|
+
)
|
|
1196
|
+
infographics_metrics_risk.append(
|
|
1197
|
+
MetricModel(
|
|
1198
|
+
name=f"ImpactedHomes{int(rp)}Y",
|
|
1199
|
+
description=f"Number of homes impacted ({fe.column} > {fe.threshold} {fe.unit}) in the {int(rp)}-year event",
|
|
1200
|
+
select="COUNT(*)",
|
|
1201
|
+
filter=filter_str,
|
|
1202
|
+
long_name=f"Flooded homes - RP{int(rp)} (#)",
|
|
1203
|
+
show_in_metrics_table=True,
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
# Create metrics for each SVI class
|
|
1208
|
+
for j, svi_class in enumerate(svi_classes):
|
|
1209
|
+
# Build SVI condition based on thresholds
|
|
1210
|
+
if j == 0:
|
|
1211
|
+
# First class: SVI < first_threshold
|
|
1212
|
+
svi_cond = f"`SVI` < {svi_thresholds[0]}"
|
|
1213
|
+
elif j == len(svi_classes) - 1:
|
|
1214
|
+
# Last class: SVI >= last_threshold
|
|
1215
|
+
svi_cond = f"`SVI` >= {svi_thresholds[-1]}"
|
|
1216
|
+
else:
|
|
1217
|
+
# Middle classes: previous_threshold <= SVI < current_threshold
|
|
1218
|
+
svi_cond = f"`SVI` >= {svi_thresholds[j-1]} AND `SVI` < {svi_thresholds[j]}"
|
|
1219
|
+
|
|
1220
|
+
# Clean class name for metric naming (remove spaces, special chars)
|
|
1221
|
+
clean_class_name = svi_class.replace(" ", "").replace("-", "")
|
|
1222
|
+
|
|
1223
|
+
filter_str = combine_filters(
|
|
1224
|
+
f"`{fe.column} ({int(rp)}Y)` >= {fe.threshold}",
|
|
1225
|
+
type_cond,
|
|
1226
|
+
svi_cond,
|
|
1227
|
+
base_filt,
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
infographics_metrics_risk.append(
|
|
1231
|
+
MetricModel(
|
|
1232
|
+
name=f"ImpactedHomes{int(rp)}Y{clean_class_name}SVI",
|
|
1233
|
+
description=f"{svi_class} vulnerable homes impacted ({fe.column} > {fe.threshold} {fe.unit}) in the {int(rp)}-year event",
|
|
1234
|
+
select="COUNT(*)",
|
|
1235
|
+
filter=filter_str,
|
|
1236
|
+
long_name=f"Flooded homes with {svi_class} vulnerability - RP{int(rp)} (#)",
|
|
1237
|
+
show_in_metrics_table=True,
|
|
1238
|
+
)
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
self.infographics_metrics_risk = infographics_metrics_risk
|
|
1242
|
+
self.additional_risk_configs = {
|
|
1243
|
+
"flood_exceedance": {
|
|
1244
|
+
"column": fe.column,
|
|
1245
|
+
"threshold": fe.threshold,
|
|
1246
|
+
"period": fe.period,
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
self._make_infographics_config_risk(config)
|
|
1250
|
+
return self.infographics_metrics_risk
|
|
1251
|
+
|
|
1252
|
+
def _setup_buildings(self, config: BuildingsInfographicModel, base_filt) -> None:
|
|
1253
|
+
"""
|
|
1254
|
+
Configure building metrics and configuration for infographics.
|
|
1255
|
+
|
|
1256
|
+
Parameters
|
|
1257
|
+
----------
|
|
1258
|
+
config : BuildingsInfographicModel
|
|
1259
|
+
Building infographic configuration.
|
|
1260
|
+
base_filt : str
|
|
1261
|
+
Base SQL filter to apply.
|
|
1262
|
+
"""
|
|
1263
|
+
# Generate queries for all building types and categories
|
|
1264
|
+
building_queries = []
|
|
1265
|
+
for btype in config.types:
|
|
1266
|
+
type_mapping = config.type_mapping.get(btype, TypeMapping())
|
|
1267
|
+
for i, cat in enumerate(config.impact_categories.categories):
|
|
1268
|
+
query_name = f"{pascal_case(btype)}{pascal_case(cat)}Count"
|
|
1269
|
+
desc = (
|
|
1270
|
+
f"Number of {btype.lower()} buildings with {cat.lower()} flooding"
|
|
1271
|
+
)
|
|
1272
|
+
long_name = f"{btype} with {cat.lower()} flooding (#)"
|
|
1273
|
+
filter_str = get_filter(
|
|
1274
|
+
type_mapping=type_mapping,
|
|
1275
|
+
cat_field=config.impact_categories.field,
|
|
1276
|
+
cat_idx=i,
|
|
1277
|
+
bins=config.impact_categories.bins,
|
|
1278
|
+
base_filt=base_filt,
|
|
1279
|
+
)
|
|
1280
|
+
building_queries.append(
|
|
1281
|
+
MetricModel(
|
|
1282
|
+
name=query_name,
|
|
1283
|
+
select="COUNT(*)",
|
|
1284
|
+
filter=filter_str,
|
|
1285
|
+
description=desc,
|
|
1286
|
+
long_name=long_name,
|
|
1287
|
+
)
|
|
1288
|
+
)
|
|
1289
|
+
self.infographics_metrics_event.extend(building_queries)
|
|
1290
|
+
self.infographics_config["buildings"] = (
|
|
1291
|
+
self._make_infographics_config_buildings(config)
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
def _setup_svi(self, config: HomesInfographicModel, base_filt) -> None:
|
|
1295
|
+
"""
|
|
1296
|
+
Configure SVI metrics and configuration for infographics.
|
|
1297
|
+
|
|
1298
|
+
Parameters
|
|
1299
|
+
----------
|
|
1300
|
+
config : SviInfographicModel
|
|
1301
|
+
SVI infographic configuration.
|
|
1302
|
+
base_filt : str
|
|
1303
|
+
Base SQL filter to apply.
|
|
1304
|
+
"""
|
|
1305
|
+
# Generate queries for all SVI categories and vulnerability levels
|
|
1306
|
+
svi_queries = []
|
|
1307
|
+
cat_field = config.impact_categories.field
|
|
1308
|
+
bins = config.impact_categories.bins
|
|
1309
|
+
if config.svi is not None:
|
|
1310
|
+
svi_thresholds = config.svi.thresholds
|
|
1311
|
+
svi_classes = config.svi.classes
|
|
1312
|
+
else:
|
|
1313
|
+
svi_thresholds = []
|
|
1314
|
+
svi_classes = []
|
|
1315
|
+
mapping = config.mapping
|
|
1316
|
+
|
|
1317
|
+
for i, cat in enumerate(config.impact_categories.categories):
|
|
1318
|
+
for j, svi_class in enumerate(svi_classes):
|
|
1319
|
+
# Build SVI condition based on thresholds
|
|
1320
|
+
if j == 0:
|
|
1321
|
+
# First class: SVI < first_threshold
|
|
1322
|
+
svi_cond = f"`SVI` < {svi_thresholds[0]}"
|
|
1323
|
+
elif j == len(svi_classes) - 1:
|
|
1324
|
+
# Last class: SVI >= last_threshold
|
|
1325
|
+
svi_cond = f"`SVI` >= {svi_thresholds[-1]}"
|
|
1326
|
+
else:
|
|
1327
|
+
# Middle classes: previous_threshold <= SVI < current_threshold
|
|
1328
|
+
svi_cond = f"`SVI` >= {svi_thresholds[j-1]} AND `SVI` < {svi_thresholds[j]}"
|
|
1329
|
+
|
|
1330
|
+
# Build impact category condition based on bins
|
|
1331
|
+
if i == 0:
|
|
1332
|
+
# First category: field <= first_bin
|
|
1333
|
+
cat_cond = f"`{cat_field}` <= {bins[0]}"
|
|
1334
|
+
elif i == len(config.impact_categories.categories) - 1:
|
|
1335
|
+
# Last category: field > last_bin
|
|
1336
|
+
cat_cond = f"`{cat_field}` > {bins[-1]}"
|
|
1337
|
+
else:
|
|
1338
|
+
# Middle categories: previous_bin < field <= current_bin
|
|
1339
|
+
cat_cond = (
|
|
1340
|
+
f"`{cat_field}` > {bins[i-1]} AND `{cat_field}` <= {bins[i]}"
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
# Build type filter using TypeMapping
|
|
1344
|
+
type_cond = mapping.to_sql_filter()
|
|
1345
|
+
|
|
1346
|
+
filter_str = combine_filters(type_cond, cat_cond, svi_cond, base_filt)
|
|
1347
|
+
|
|
1348
|
+
name = f"{cat}{svi_class.replace(' ', '')}Vulnerability"
|
|
1349
|
+
desc = f"Number of {cat.lower()} homes with {svi_class.lower()} vulnerability"
|
|
1350
|
+
long_name = f"{cat} Homes - {svi_class} Vulnerability (#)"
|
|
1351
|
+
|
|
1352
|
+
svi_queries.append(
|
|
1353
|
+
MetricModel(
|
|
1354
|
+
name=name,
|
|
1355
|
+
select="COUNT(*)",
|
|
1356
|
+
filter=filter_str,
|
|
1357
|
+
description=desc,
|
|
1358
|
+
long_name=long_name,
|
|
1359
|
+
show_in_metrics_table=True,
|
|
1360
|
+
)
|
|
1361
|
+
)
|
|
1362
|
+
self.infographics_metrics_event.extend(svi_queries)
|
|
1363
|
+
self.infographics_config["svi"] = self._make_infographics_config_svi(config)
|
|
1364
|
+
|
|
1365
|
+
def _setup_roads(self, config: RoadsInfographicModel) -> None:
|
|
1366
|
+
"""
|
|
1367
|
+
Configure roads metrics and configuration for infographics.
|
|
1368
|
+
|
|
1369
|
+
Parameters
|
|
1370
|
+
----------
|
|
1371
|
+
config : RoadsInfographicModel
|
|
1372
|
+
Roads infographic configuration.
|
|
1373
|
+
"""
|
|
1374
|
+
# Generate queries for all road categories
|
|
1375
|
+
road_queries = []
|
|
1376
|
+
cat_field = config.field
|
|
1377
|
+
thresholds = config.thresholds
|
|
1378
|
+
road_length_field = config.road_length_field
|
|
1379
|
+
|
|
1380
|
+
if config.unit == "meters":
|
|
1381
|
+
unit_conversion = 1 / 1000
|
|
1382
|
+
unit = "Kilometers"
|
|
1383
|
+
elif config.unit == "feet":
|
|
1384
|
+
unit_conversion = 1 / 5280
|
|
1385
|
+
unit = "Miles"
|
|
1386
|
+
|
|
1387
|
+
for i, cat in enumerate(config.categories):
|
|
1388
|
+
name = f"{pascal_case(cat)}FloodedRoadsLength"
|
|
1389
|
+
desc = f"{unit} of roads disrupted for {config.users[i].lower()}"
|
|
1390
|
+
long_name = f"Length of roads with {cat.lower()} flooding ({unit})"
|
|
1391
|
+
select = f"SUM(`{road_length_field}`)*{unit_conversion}"
|
|
1392
|
+
filter_str = f"`{cat_field}` >= {thresholds[i]}"
|
|
1393
|
+
road_queries.append(
|
|
1394
|
+
MetricModel(
|
|
1395
|
+
name=name,
|
|
1396
|
+
description=desc,
|
|
1397
|
+
long_name=long_name,
|
|
1398
|
+
select=select,
|
|
1399
|
+
filter=filter_str,
|
|
1400
|
+
show_in_metrics_table=True,
|
|
1401
|
+
)
|
|
1402
|
+
)
|
|
1403
|
+
self.infographics_metrics_event.extend(road_queries)
|
|
1404
|
+
self.infographics_config["roads"] = self._make_infographics_config_roads(config)
|
|
1405
|
+
|
|
1406
|
+
@staticmethod
|
|
1407
|
+
def _make_infographics_config_buildings(
|
|
1408
|
+
buildings_config: BuildingsInfographicModel,
|
|
1409
|
+
) -> Dict[str, Any]:
|
|
1410
|
+
"""
|
|
1411
|
+
Create infographics configuration dictionary for buildings.
|
|
1412
|
+
|
|
1413
|
+
Parameters
|
|
1414
|
+
----------
|
|
1415
|
+
buildings_config : BuildingsInfographicModel
|
|
1416
|
+
Building infographic configuration.
|
|
1417
|
+
|
|
1418
|
+
Returns
|
|
1419
|
+
-------
|
|
1420
|
+
Dict[str, Any]
|
|
1421
|
+
Configuration dictionary for building infographics.
|
|
1422
|
+
"""
|
|
1423
|
+
image_path = "{image_path}"
|
|
1424
|
+
# Default plot configuration, matching your existing template:
|
|
1425
|
+
# Dynamically generate the Info text based on the number of categories and bins
|
|
1426
|
+
info_lines = [f"{buildings_config.impact_categories.field}:<br>"]
|
|
1427
|
+
for idx, cat in enumerate(buildings_config.impact_categories.categories):
|
|
1428
|
+
if idx < len(buildings_config.impact_categories.bins):
|
|
1429
|
+
info_lines.append(
|
|
1430
|
+
f" {cat}: <={buildings_config.impact_categories.bins[idx]} {buildings_config.impact_categories.unit}<br>"
|
|
1431
|
+
)
|
|
1432
|
+
else:
|
|
1433
|
+
# Last category: greater than last bin
|
|
1434
|
+
info_lines.append(
|
|
1435
|
+
f" {cat}: >{buildings_config.impact_categories.bins[-1]} {buildings_config.impact_categories.unit}<br>"
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
other_config: Dict[str, Any] = {
|
|
1439
|
+
"Plot": {
|
|
1440
|
+
"image_scale": 0.15,
|
|
1441
|
+
"numbers_font": 20,
|
|
1442
|
+
"height": 350,
|
|
1443
|
+
"width": 1200,
|
|
1444
|
+
},
|
|
1445
|
+
"Title": {"text": "Building Impacts", "font": 30},
|
|
1446
|
+
"Subtitle": {"font": 25},
|
|
1447
|
+
"Legend": {"font": 20},
|
|
1448
|
+
"Info": {
|
|
1449
|
+
"text": "".join(info_lines),
|
|
1450
|
+
"image": "https://openclipart.org/image/800px/302413",
|
|
1451
|
+
"scale": 0.1,
|
|
1452
|
+
},
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
cfg: Dict[str, Any] = {
|
|
1456
|
+
"Charts": {},
|
|
1457
|
+
"Categories": {},
|
|
1458
|
+
"Slices": {},
|
|
1459
|
+
"Other": other_config,
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
# Categories block - keys have no special characters, Names are original
|
|
1463
|
+
if buildings_config.impact_categories.colors is not None:
|
|
1464
|
+
for k, cat in enumerate(buildings_config.impact_categories.categories):
|
|
1465
|
+
clean_cat_key = pascal_case(cat.replace(" ", "").replace("-", ""))
|
|
1466
|
+
cfg["Categories"][clean_cat_key] = {
|
|
1467
|
+
"Name": cat, # Original name
|
|
1468
|
+
"Color": buildings_config.impact_categories.colors[k],
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
# Charts block - keys have no special characters, Names are original
|
|
1472
|
+
for i, btype in enumerate(buildings_config.types):
|
|
1473
|
+
clean_btype_key = pascal_case(btype.replace(" ", "").replace("-", ""))
|
|
1474
|
+
cfg["Charts"][clean_btype_key] = {
|
|
1475
|
+
"Name": btype, # Original name
|
|
1476
|
+
"Image": f"{image_path}/{buildings_config.icons[i]}.png",
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
# Slices block - reference Chart and Category Names (not keys)
|
|
1480
|
+
for i, btype in enumerate(buildings_config.types):
|
|
1481
|
+
for cat in buildings_config.impact_categories.categories:
|
|
1482
|
+
clean_cat_key = pascal_case(cat.replace(" ", "").replace("-", ""))
|
|
1483
|
+
clean_btype_key = pascal_case(btype.replace(" ", "").replace("-", ""))
|
|
1484
|
+
slice_key = f"{clean_cat_key}{clean_btype_key}"
|
|
1485
|
+
cfg["Slices"][slice_key] = {
|
|
1486
|
+
"Name": f"{cat} {btype}",
|
|
1487
|
+
"Query": f"{pascal_case(btype)}{pascal_case(cat)}Count",
|
|
1488
|
+
"Chart": btype, # Reference Chart Name, not key
|
|
1489
|
+
"Category": cat, # Reference Category Name, not key
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return cfg
|
|
1493
|
+
|
|
1494
|
+
@staticmethod
|
|
1495
|
+
def _make_infographics_config_svi(
|
|
1496
|
+
svi_config: HomesInfographicModel,
|
|
1497
|
+
) -> Dict[str, Any]:
|
|
1498
|
+
"""
|
|
1499
|
+
Create infographics configuration dictionary for SVI.
|
|
1500
|
+
|
|
1501
|
+
Parameters
|
|
1502
|
+
----------
|
|
1503
|
+
svi_config : SviInfographicModel
|
|
1504
|
+
SVI infographic configuration.
|
|
1505
|
+
|
|
1506
|
+
Returns
|
|
1507
|
+
-------
|
|
1508
|
+
Dict[str, Any]
|
|
1509
|
+
Configuration dictionary for SVI infographics.
|
|
1510
|
+
"""
|
|
1511
|
+
image_path = "{image_path}"
|
|
1512
|
+
svi_classes = svi_config.svi.classes if svi_config.svi is not None else []
|
|
1513
|
+
charts = {}
|
|
1514
|
+
slices = {}
|
|
1515
|
+
categories_cfg = {}
|
|
1516
|
+
|
|
1517
|
+
# Charts block - keys have no special characters, Names are original
|
|
1518
|
+
for cat in svi_config.impact_categories.categories:
|
|
1519
|
+
clean_cat_key = pascal_case(cat.replace(" ", "").replace("-", ""))
|
|
1520
|
+
charts[clean_cat_key] = {
|
|
1521
|
+
"Name": cat, # Original name
|
|
1522
|
+
"Image": f"{image_path}/house.png",
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
# Categories block - keys have no special characters, Names are original
|
|
1526
|
+
for idx, svi_class in enumerate(svi_classes):
|
|
1527
|
+
color = (
|
|
1528
|
+
svi_config.svi.colors[idx] if svi_config.svi is not None else "#cccccc"
|
|
1529
|
+
)
|
|
1530
|
+
clean_svi_key = (
|
|
1531
|
+
pascal_case(svi_class.replace(" ", "").replace("-", ""))
|
|
1532
|
+
+ "Vulnerability"
|
|
1533
|
+
)
|
|
1534
|
+
categories_cfg[clean_svi_key] = {
|
|
1535
|
+
"Name": f"{svi_class} Vulnerability", # Original name with "Vulnerability"
|
|
1536
|
+
"Color": color,
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
# Slices block - reference Chart and Category Names (not keys)
|
|
1540
|
+
for cat in svi_config.impact_categories.categories:
|
|
1541
|
+
for svi_class in svi_classes:
|
|
1542
|
+
clean_cat_key = pascal_case(cat.replace(" ", "").replace("-", ""))
|
|
1543
|
+
clean_svi_key = (
|
|
1544
|
+
pascal_case(svi_class.replace(" ", "").replace("-", ""))
|
|
1545
|
+
+ "Vulnerability"
|
|
1546
|
+
)
|
|
1547
|
+
slice_key = f"{clean_cat_key}{clean_svi_key}"
|
|
1548
|
+
name = f"{cat} {svi_class.lower()} vulnerability homes"
|
|
1549
|
+
query = (
|
|
1550
|
+
f"{cat}{svi_class.replace(' ', '').replace('-', '')}Vulnerability"
|
|
1551
|
+
)
|
|
1552
|
+
slices[slice_key] = {
|
|
1553
|
+
"Name": name,
|
|
1554
|
+
"Query": query,
|
|
1555
|
+
"Chart": cat, # Reference Chart Name, not key
|
|
1556
|
+
"Category": f"{svi_class} Vulnerability", # Reference Category Name, not key
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
# Info text - dynamically build threshold information
|
|
1560
|
+
bins = svi_config.impact_categories.bins
|
|
1561
|
+
unit = svi_config.impact_categories.unit
|
|
1562
|
+
thresholds = svi_config.svi.thresholds if svi_config.svi is not None else []
|
|
1563
|
+
|
|
1564
|
+
info_lines = [
|
|
1565
|
+
"Thresholds:<br>",
|
|
1566
|
+
]
|
|
1567
|
+
|
|
1568
|
+
# Add impact category threshold information
|
|
1569
|
+
for i, cat in enumerate(svi_config.impact_categories.categories):
|
|
1570
|
+
if i == 0:
|
|
1571
|
+
info_lines.append(f" {cat}: <= {bins[0]} {unit}<br>")
|
|
1572
|
+
elif i == len(svi_config.impact_categories.categories) - 1:
|
|
1573
|
+
info_lines.append(f" {cat}: > {bins[-1]} {unit}<br>")
|
|
1574
|
+
else:
|
|
1575
|
+
info_lines.append(f" {cat}: {bins[i-1]} - {bins[i]} {unit}<br>")
|
|
1576
|
+
|
|
1577
|
+
info_lines.append("<br>SVI Classes:<br>")
|
|
1578
|
+
|
|
1579
|
+
# Add SVI threshold information
|
|
1580
|
+
for i, svi_class in enumerate(svi_classes):
|
|
1581
|
+
if i == 0:
|
|
1582
|
+
info_lines.append(f" {svi_class}: < {thresholds[0]}<br>")
|
|
1583
|
+
elif i == len(svi_classes) - 1:
|
|
1584
|
+
info_lines.append(f" {svi_class}: >= {thresholds[-1]}<br>")
|
|
1585
|
+
else:
|
|
1586
|
+
info_lines.append(
|
|
1587
|
+
f" {svi_class}: {thresholds[i-1]} - {thresholds[i]}<br>"
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
info_lines.extend(
|
|
1591
|
+
[
|
|
1592
|
+
"'Since some homes do not have an SVI,<br>",
|
|
1593
|
+
"total number of homes might be different <br>",
|
|
1594
|
+
"between this and the above graph.'",
|
|
1595
|
+
]
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
other_config = {
|
|
1599
|
+
"Plot": {
|
|
1600
|
+
"image_scale": 0.15,
|
|
1601
|
+
"numbers_font": 20,
|
|
1602
|
+
"height": 350,
|
|
1603
|
+
"width": 600,
|
|
1604
|
+
},
|
|
1605
|
+
"Title": {"text": "Impacted Homes", "font": 30},
|
|
1606
|
+
"Subtitle": {"font": 25},
|
|
1607
|
+
"Legend": {"font": 20},
|
|
1608
|
+
"Info": {
|
|
1609
|
+
"text": "".join(info_lines),
|
|
1610
|
+
"image": "https://openclipart.org/image/800px/302413",
|
|
1611
|
+
"scale": 0.1,
|
|
1612
|
+
},
|
|
1613
|
+
}
|
|
1614
|
+
cfg = {
|
|
1615
|
+
"Charts": charts,
|
|
1616
|
+
"Categories": categories_cfg,
|
|
1617
|
+
"Slices": slices,
|
|
1618
|
+
"Other": other_config,
|
|
1619
|
+
}
|
|
1620
|
+
return cfg
|
|
1621
|
+
|
|
1622
|
+
@staticmethod
|
|
1623
|
+
def _make_infographics_config_roads(
|
|
1624
|
+
roads_config: RoadsInfographicModel,
|
|
1625
|
+
) -> Dict[str, Any]:
|
|
1626
|
+
"""
|
|
1627
|
+
Create infographics configuration dictionary for roads.
|
|
1628
|
+
|
|
1629
|
+
Parameters
|
|
1630
|
+
----------
|
|
1631
|
+
roads_config : RoadsInfographicModel
|
|
1632
|
+
Roads infographic configuration.
|
|
1633
|
+
|
|
1634
|
+
Returns
|
|
1635
|
+
-------
|
|
1636
|
+
Dict[str, Any]
|
|
1637
|
+
Configuration dictionary for roads infographics.
|
|
1638
|
+
"""
|
|
1639
|
+
image_path = "{image_path}"
|
|
1640
|
+
|
|
1641
|
+
# Charts block - key has no special characters, Name is original
|
|
1642
|
+
chart_key = "FloodedRoads"
|
|
1643
|
+
chart_name = "Flooded Roads"
|
|
1644
|
+
charts = {chart_key: {"Name": chart_name}}
|
|
1645
|
+
|
|
1646
|
+
# Categories block - keys have no special characters, Names are original
|
|
1647
|
+
categories_cfg = {}
|
|
1648
|
+
for idx, cat in enumerate(roads_config.categories):
|
|
1649
|
+
clean_cat_key = (
|
|
1650
|
+
pascal_case(cat.replace(" ", "").replace("-", "")) + "Flooding"
|
|
1651
|
+
)
|
|
1652
|
+
cat_name = f"{cat} Flooding"
|
|
1653
|
+
categories_cfg[clean_cat_key] = {
|
|
1654
|
+
"Name": cat_name, # Original name with "Flooding"
|
|
1655
|
+
"Color": roads_config.colors[idx],
|
|
1656
|
+
"Image": f"{image_path}/{roads_config.icons[idx]}.png",
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
# Slices block - reference Chart and Category Names (not keys)
|
|
1660
|
+
slices = {}
|
|
1661
|
+
for idx, cat in enumerate(roads_config.categories):
|
|
1662
|
+
clean_cat_key = (
|
|
1663
|
+
pascal_case(cat.replace(" ", "").replace("-", "")) + "Flooding"
|
|
1664
|
+
)
|
|
1665
|
+
cat_name = f"{cat} Flooding"
|
|
1666
|
+
query_name = f"{pascal_case(cat)}FloodedRoadsLength"
|
|
1667
|
+
slices[clean_cat_key] = {
|
|
1668
|
+
"Name": cat_name,
|
|
1669
|
+
"Query": query_name,
|
|
1670
|
+
"Chart": chart_name, # Reference Chart Name, not key
|
|
1671
|
+
"Category": cat_name, # Reference Category Name, not key
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
# Info text
|
|
1675
|
+
thresholds = roads_config.thresholds
|
|
1676
|
+
# Consistent unit naming
|
|
1677
|
+
if roads_config.unit == "feet":
|
|
1678
|
+
unit = "Miles"
|
|
1679
|
+
else:
|
|
1680
|
+
unit = "Kilometers"
|
|
1681
|
+
info_lines = ["Thresholds:<br>"]
|
|
1682
|
+
for user, threshold in zip(roads_config.users, thresholds):
|
|
1683
|
+
info_lines.append(f" {user}: {threshold} {roads_config.unit}<br>")
|
|
1684
|
+
other_config = {
|
|
1685
|
+
"Plot": {
|
|
1686
|
+
"image_scale": 0.1,
|
|
1687
|
+
"numbers_font": 20,
|
|
1688
|
+
"height": 350,
|
|
1689
|
+
"width": 600,
|
|
1690
|
+
},
|
|
1691
|
+
"Title": {"text": "Interrupted roads", "font": 30},
|
|
1692
|
+
"Subtitle": {"font": 25},
|
|
1693
|
+
"Y_axis_title": {"text": unit},
|
|
1694
|
+
"Info": {
|
|
1695
|
+
"text": "".join(info_lines),
|
|
1696
|
+
"image": f"{image_path}/info.png",
|
|
1697
|
+
"scale": 0.1,
|
|
1698
|
+
},
|
|
1699
|
+
}
|
|
1700
|
+
cfg = {
|
|
1701
|
+
"Charts": charts,
|
|
1702
|
+
"Categories": categories_cfg,
|
|
1703
|
+
"Slices": slices,
|
|
1704
|
+
"Other": other_config,
|
|
1705
|
+
}
|
|
1706
|
+
return cfg
|
|
1707
|
+
|
|
1708
|
+
def _make_infographics_config_risk(self, risk_config: RiskInfographicModel) -> dict:
|
|
1709
|
+
"""
|
|
1710
|
+
Create infographics configuration dictionary for risk infographics.
|
|
1711
|
+
|
|
1712
|
+
Parameters
|
|
1713
|
+
----------
|
|
1714
|
+
risk_config : RiskInfographicModel
|
|
1715
|
+
Risk infographic configuration.
|
|
1716
|
+
|
|
1717
|
+
Returns
|
|
1718
|
+
-------
|
|
1719
|
+
dict
|
|
1720
|
+
Configuration dictionary for risk infographics.
|
|
1721
|
+
"""
|
|
1722
|
+
image_path = "{image_path}"
|
|
1723
|
+
rps = self.return_periods
|
|
1724
|
+
homes = risk_config.homes
|
|
1725
|
+
svi = homes.svi if homes.svi is not None else None
|
|
1726
|
+
|
|
1727
|
+
# Charts block - keys have no special characters, Names are original
|
|
1728
|
+
charts = {}
|
|
1729
|
+
for rp in rps:
|
|
1730
|
+
chart_key = f"RP{int(rp)}Y"
|
|
1731
|
+
chart_name = f"{int(rp)}Y"
|
|
1732
|
+
charts[chart_key] = {
|
|
1733
|
+
"Name": chart_name, # Original name
|
|
1734
|
+
"Image": f"{image_path}/house.png",
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
# Categories block - keys have no special characters, Names are original
|
|
1738
|
+
categories = {}
|
|
1739
|
+
if svi:
|
|
1740
|
+
for idx, svi_class in enumerate(svi.classes):
|
|
1741
|
+
cat_key = (
|
|
1742
|
+
pascal_case(svi_class.replace(" ", "").replace("-", ""))
|
|
1743
|
+
+ "Vulnerability"
|
|
1744
|
+
)
|
|
1745
|
+
cat_name = f"{svi_class} Vulnerability"
|
|
1746
|
+
categories[cat_key] = {
|
|
1747
|
+
"Name": cat_name, # Original name with "Vulnerability"
|
|
1748
|
+
"Color": svi.colors[idx] if svi.colors else "#cccccc",
|
|
1749
|
+
}
|
|
1750
|
+
else:
|
|
1751
|
+
# Create a single "All Homes" category when no SVI data
|
|
1752
|
+
categories["AllHomes"] = {
|
|
1753
|
+
"Name": "All Homes",
|
|
1754
|
+
"Color": "#88A2AA", # Default color
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
# Slices block - reference Chart and Category Names (not keys)
|
|
1758
|
+
slices = {}
|
|
1759
|
+
if svi:
|
|
1760
|
+
for rp in rps:
|
|
1761
|
+
chart_name = f"{int(rp)}Y"
|
|
1762
|
+
for idx, svi_class in enumerate(svi.classes):
|
|
1763
|
+
clean_svi = svi_class.replace(" ", "").replace("-", "")
|
|
1764
|
+
cat_name = f"{svi_class} Vulnerability"
|
|
1765
|
+
slice_key = f"{pascal_case(svi_class.replace(' ', '').replace('-', ''))}VulnerabilityRP{int(rp)}Y"
|
|
1766
|
+
name = f"{int(rp)}Y {svi_class} Vulnerability"
|
|
1767
|
+
query = f"ImpactedHomes{int(rp)}Y{clean_svi}SVI"
|
|
1768
|
+
slices[slice_key] = {
|
|
1769
|
+
"Name": name,
|
|
1770
|
+
"Query": query,
|
|
1771
|
+
"Chart": chart_name, # Reference Chart Name, not key
|
|
1772
|
+
"Category": cat_name, # Reference Category Name, not key
|
|
1773
|
+
}
|
|
1774
|
+
else:
|
|
1775
|
+
# Create slices for each return period without SVI breakdown
|
|
1776
|
+
for rp in rps:
|
|
1777
|
+
chart_name = f"{int(rp)}Y"
|
|
1778
|
+
slice_key = f"AllHomesRP{int(rp)}Y"
|
|
1779
|
+
name = f"{int(rp)}Y All Homes"
|
|
1780
|
+
query = f"ImpactedHomes{int(rp)}Y"
|
|
1781
|
+
slices[slice_key] = {
|
|
1782
|
+
"Name": name,
|
|
1783
|
+
"Query": query,
|
|
1784
|
+
"Chart": chart_name, # Reference Chart Name, not key
|
|
1785
|
+
"Category": "All Homes", # Reference Category Name, not key
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
# Other block: static info, but use config where possible
|
|
1789
|
+
other = {
|
|
1790
|
+
"Expected_Damages": {
|
|
1791
|
+
"title": "Expected annual damages",
|
|
1792
|
+
"query": "ExpectedAnnualDamages",
|
|
1793
|
+
"image": f"{image_path}/money.png",
|
|
1794
|
+
"image_scale": 1.3,
|
|
1795
|
+
"title_font_size": 25,
|
|
1796
|
+
"numbers_font_size": 20,
|
|
1797
|
+
"height": 300,
|
|
1798
|
+
},
|
|
1799
|
+
"Flooded": {
|
|
1800
|
+
"title": f"Number of homes with a high chance of being flooded in a {risk_config.flood_exceedance.period}-year period",
|
|
1801
|
+
"query": "LikelyFloodedHomes",
|
|
1802
|
+
"image": f"{image_path}/house.png",
|
|
1803
|
+
"image_scale": 0.7,
|
|
1804
|
+
"title_font_size": 25,
|
|
1805
|
+
"numbers_font_size": 20,
|
|
1806
|
+
"height": 300,
|
|
1807
|
+
},
|
|
1808
|
+
"Return_Periods": {
|
|
1809
|
+
"title": "Buildings impacted",
|
|
1810
|
+
"font_size": 25,
|
|
1811
|
+
"image_scale": 0.2,
|
|
1812
|
+
"numbers_font": 15,
|
|
1813
|
+
"subtitle_font": 22,
|
|
1814
|
+
"legend_font": 20,
|
|
1815
|
+
"plot_height": 300,
|
|
1816
|
+
},
|
|
1817
|
+
"Info": {
|
|
1818
|
+
"text": (
|
|
1819
|
+
"Thresholds:<br>"
|
|
1820
|
+
f" Impacted: >= {risk_config.flood_exceedance.threshold} {risk_config.flood_exceedance.unit}<br>"
|
|
1821
|
+
),
|
|
1822
|
+
"image": "https://openclipart.org/image/800px/302413",
|
|
1823
|
+
"scale": 0.1,
|
|
1824
|
+
},
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
cfg = {
|
|
1828
|
+
"Charts": charts,
|
|
1829
|
+
"Categories": categories,
|
|
1830
|
+
"Slices": slices,
|
|
1831
|
+
"Other": other,
|
|
1832
|
+
}
|
|
1833
|
+
self.infographics_config["risk"] = cfg
|
|
1834
|
+
return cfg
|