flood-adapt 0.3.9__py3-none-any.whl → 0.3.11__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 (100) hide show
  1. flood_adapt/__init__.py +26 -22
  2. flood_adapt/adapter/__init__.py +9 -9
  3. flood_adapt/adapter/fiat_adapter.py +1541 -1541
  4. flood_adapt/adapter/interface/hazard_adapter.py +70 -70
  5. flood_adapt/adapter/interface/impact_adapter.py +36 -36
  6. flood_adapt/adapter/interface/model_adapter.py +89 -89
  7. flood_adapt/adapter/interface/offshore.py +19 -19
  8. flood_adapt/adapter/sfincs_adapter.py +1853 -1848
  9. flood_adapt/adapter/sfincs_offshore.py +187 -193
  10. flood_adapt/config/config.py +248 -248
  11. flood_adapt/config/fiat.py +219 -219
  12. flood_adapt/config/gui.py +331 -331
  13. flood_adapt/config/sfincs.py +481 -336
  14. flood_adapt/config/site.py +129 -129
  15. flood_adapt/database_builder/database_builder.py +2210 -2210
  16. flood_adapt/database_builder/templates/default_units/imperial.toml +9 -9
  17. flood_adapt/database_builder/templates/default_units/metric.toml +9 -9
  18. flood_adapt/database_builder/templates/green_infra_table/green_infra_lookup_table.csv +10 -10
  19. flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +90 -90
  20. flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +57 -57
  21. flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +121 -121
  22. flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +65 -65
  23. flood_adapt/database_builder/templates/infographics/OSM/styles.css +45 -45
  24. flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +126 -126
  25. flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +60 -60
  26. flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +121 -121
  27. flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +65 -65
  28. flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +45 -45
  29. flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +4 -4
  30. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +143 -143
  31. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +153 -153
  32. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +127 -127
  33. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +57 -57
  34. flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +4 -4
  35. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +191 -191
  36. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +153 -153
  37. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +178 -178
  38. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +57 -57
  39. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +9 -9
  40. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +65 -65
  41. flood_adapt/database_builder/templates/output_layers/bin_colors.toml +5 -5
  42. flood_adapt/database_builder.py +16 -16
  43. flood_adapt/dbs_classes/__init__.py +21 -21
  44. flood_adapt/dbs_classes/database.py +533 -684
  45. flood_adapt/dbs_classes/dbs_benefit.py +77 -76
  46. flood_adapt/dbs_classes/dbs_event.py +61 -59
  47. flood_adapt/dbs_classes/dbs_measure.py +112 -111
  48. flood_adapt/dbs_classes/dbs_projection.py +34 -34
  49. flood_adapt/dbs_classes/dbs_scenario.py +137 -137
  50. flood_adapt/dbs_classes/dbs_static.py +274 -273
  51. flood_adapt/dbs_classes/dbs_strategy.py +130 -129
  52. flood_adapt/dbs_classes/dbs_template.py +279 -278
  53. flood_adapt/dbs_classes/interface/database.py +107 -139
  54. flood_adapt/dbs_classes/interface/element.py +121 -121
  55. flood_adapt/dbs_classes/interface/static.py +47 -47
  56. flood_adapt/flood_adapt.py +1229 -1178
  57. flood_adapt/misc/database_user.py +16 -16
  58. flood_adapt/misc/exceptions.py +22 -0
  59. flood_adapt/misc/log.py +183 -183
  60. flood_adapt/misc/path_builder.py +54 -54
  61. flood_adapt/misc/utils.py +185 -185
  62. flood_adapt/objects/__init__.py +82 -82
  63. flood_adapt/objects/benefits/benefits.py +61 -61
  64. flood_adapt/objects/events/event_factory.py +135 -135
  65. flood_adapt/objects/events/event_set.py +88 -84
  66. flood_adapt/objects/events/events.py +236 -234
  67. flood_adapt/objects/events/historical.py +58 -58
  68. flood_adapt/objects/events/hurricane.py +68 -67
  69. flood_adapt/objects/events/synthetic.py +46 -50
  70. flood_adapt/objects/forcing/__init__.py +92 -92
  71. flood_adapt/objects/forcing/csv.py +68 -68
  72. flood_adapt/objects/forcing/discharge.py +66 -66
  73. flood_adapt/objects/forcing/forcing.py +150 -150
  74. flood_adapt/objects/forcing/forcing_factory.py +182 -182
  75. flood_adapt/objects/forcing/meteo_handler.py +93 -93
  76. flood_adapt/objects/forcing/netcdf.py +40 -40
  77. flood_adapt/objects/forcing/plotting.py +453 -429
  78. flood_adapt/objects/forcing/rainfall.py +98 -98
  79. flood_adapt/objects/forcing/tide_gauge.py +191 -191
  80. flood_adapt/objects/forcing/time_frame.py +90 -90
  81. flood_adapt/objects/forcing/timeseries.py +564 -564
  82. flood_adapt/objects/forcing/unit_system.py +580 -580
  83. flood_adapt/objects/forcing/waterlevels.py +108 -108
  84. flood_adapt/objects/forcing/wind.py +124 -124
  85. flood_adapt/objects/measures/measure_factory.py +92 -92
  86. flood_adapt/objects/measures/measures.py +551 -529
  87. flood_adapt/objects/object_model.py +74 -68
  88. flood_adapt/objects/projections/projections.py +103 -103
  89. flood_adapt/objects/scenarios/scenarios.py +22 -22
  90. flood_adapt/objects/strategies/strategies.py +89 -89
  91. flood_adapt/workflows/benefit_runner.py +579 -554
  92. flood_adapt/workflows/floodmap.py +85 -85
  93. flood_adapt/workflows/impacts_integrator.py +85 -85
  94. flood_adapt/workflows/scenario_runner.py +70 -70
  95. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/LICENSE +674 -674
  96. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/METADATA +867 -865
  97. flood_adapt-0.3.11.dist-info/RECORD +140 -0
  98. flood_adapt-0.3.9.dist-info/RECORD +0 -139
  99. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/WHEEL +0 -0
  100. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/top_level.txt +0 -0
@@ -1,580 +1,580 @@
1
- import enum
2
- import math
3
- from abc import ABC
4
- from datetime import timedelta
5
- from enum import Enum
6
- from typing import Generic, Optional, Type, TypeVar, Union
7
-
8
- from pydantic import BaseModel, Field, model_validator
9
-
10
- __all__ = [
11
- "ValueUnitPair",
12
- "VerticalReference",
13
- "UnitTypesLength",
14
- "UnitTypesArea",
15
- "UnitTypesVolume",
16
- "UnitTypesVelocity",
17
- "UnitTypesDirection",
18
- "UnitTypesTime",
19
- "UnitTypesDischarge",
20
- "UnitTypesIntensity",
21
- "UnitfulLength",
22
- "UnitfulHeight",
23
- "UnitfulLengthRefValue",
24
- "UnitfulArea",
25
- "UnitfulVelocity",
26
- "UnitfulDirection",
27
- "UnitfulDischarge",
28
- "UnitfulIntensity",
29
- "UnitfulVolume",
30
- "UnitfulTime",
31
- "ValueUnitPairs",
32
- ]
33
-
34
- TUnit = TypeVar("TUnit", bound=enum.Enum)
35
- TClass = TypeVar("TClass", bound="ValueUnitPair")
36
-
37
-
38
- class ValueUnitPair(ABC, BaseModel, Generic[TUnit]):
39
- """
40
- Represents a value with associated units.
41
-
42
- Attributes
43
- ----------
44
- value: float
45
- The numerical value.
46
- units : Unit
47
- The units of the value.
48
- """
49
-
50
- _DEFAULT_UNIT: TUnit
51
- _CONVERSION_FACTORS: dict[TUnit, float]
52
-
53
- value: float
54
- units: TUnit
55
-
56
- def convert(self, new_units: TUnit) -> float:
57
- """Return the value converted to the new units.
58
-
59
- Parameters
60
- ----------
61
- new_units : Unit
62
- The new units.
63
-
64
- Returns
65
- -------
66
- converted_value : float
67
- The converted value.
68
- """
69
- if new_units not in self._CONVERSION_FACTORS:
70
- raise ValueError(f"Invalid units: {new_units}")
71
- in_default_units = self.value / self._CONVERSION_FACTORS[self.units]
72
- return in_default_units * self._CONVERSION_FACTORS[new_units]
73
-
74
- def transform(self: TClass, new_units: TUnit) -> TClass:
75
- """Return a new ValueUnitPair instance with the value converted to the new units.
76
-
77
- Parameters
78
- ----------
79
- new_units : Unit
80
- The new units.
81
-
82
- Returns
83
- -------
84
- value_unit_pair : ValueUnitPair
85
- The new ValueUnitPair instance with the value converted to the new units and the new units.
86
- """
87
- return type(self)(value=self.convert(new_units), units=new_units)
88
-
89
- @model_validator(mode="before")
90
- @classmethod
91
- def extract_unit_class(cls, data: Optional[dict]) -> "ValueUnitPair":
92
- if cls is not ValueUnitPair:
93
- return data # Already in the right subclass, don't interfere
94
-
95
- if not isinstance(data, dict):
96
- raise TypeError("Expected dictionary for deserialization.")
97
-
98
- str_unit = data.get("units")
99
- if not str_unit:
100
- raise ValueError("Missing 'units' field in input data.")
101
-
102
- for unit_enum_cls, vu_cls in UNIT_TO_CLASS.items():
103
- try:
104
- enum_unit = unit_enum_cls(str_unit)
105
- data["units"] = enum_unit
106
- return vu_cls(**data)
107
- except ValueError:
108
- continue
109
-
110
- raise ValueError(f"Unsupported or unknown unit: {str_unit}")
111
-
112
- def __str__(self) -> str:
113
- return f"{self.value} {self.units.value}"
114
-
115
- def __repr__(self) -> str:
116
- return f"{type(self).__name__}(value={self.value}, units={self.units})"
117
-
118
- def __sub__(self: TClass, other: TClass) -> TClass:
119
- if not isinstance(other, type(self)):
120
- raise TypeError(
121
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
122
- )
123
- return type(self)(
124
- value=self.value - other.convert(self.units), units=self.units
125
- )
126
-
127
- def __add__(self: TClass, other: TClass) -> TClass:
128
- if not isinstance(other, type(self)):
129
- raise TypeError(
130
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
131
- )
132
- return type(self)(
133
- value=self.value + other.convert(self.units), units=self.units
134
- )
135
-
136
- def __eq__(self: TClass, other: TClass) -> bool:
137
- if not isinstance(other, ValueUnitPair):
138
- raise TypeError(
139
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
140
- )
141
- if not (hasattr(other, "value") and hasattr(other, "units")):
142
- raise AttributeError(f"Incomplete UnitfulValue instance: {other}")
143
-
144
- if not isinstance(other.units, type(self.units)):
145
- raise TypeError(
146
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
147
- )
148
-
149
- return math.isclose(
150
- self.value, other.convert(self.units), rel_tol=1e-2
151
- ) # 1% relative tolerance for equality. So 1.0 == 1.01 evaluates to True
152
-
153
- def __lt__(self: TClass, other: TClass) -> bool:
154
- if not isinstance(other, type(self)):
155
- raise TypeError(
156
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
157
- )
158
- return self.value < other.convert(self.units)
159
-
160
- def __gt__(self: TClass, other: TClass) -> bool:
161
- if not isinstance(other, type(self)):
162
- raise TypeError(
163
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
164
- )
165
- return self.value > other.convert(self.units)
166
-
167
- def __ge__(self: TClass, other: TClass) -> bool:
168
- if not isinstance(other, type(self)):
169
- raise TypeError(
170
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
171
- )
172
- return (self > other) or (self == other)
173
-
174
- def __le__(self: TClass, other: TClass) -> bool:
175
- if not isinstance(other, type(self)):
176
- raise TypeError(
177
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
178
- )
179
- return (self < other) or (self == other)
180
-
181
- def __ne__(self: TClass, other: TClass) -> bool:
182
- if not isinstance(other, type(self)):
183
- raise TypeError(
184
- f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
185
- )
186
- return not (self == other)
187
-
188
- def __mul__(self: TClass, other: int | float) -> TClass:
189
- if isinstance(other, int) or isinstance(other, float):
190
- return type(self)(value=self.value * other, units=self.units)
191
- else:
192
- raise TypeError(
193
- f"Cannot multiply self: {type(self).__name__} with other: {type(other).__name__}. Only int and float are allowed."
194
- )
195
-
196
- def __div__(self: TClass, other: TClass | int | float) -> TClass | float:
197
- if isinstance(other, int) or isinstance(other, float):
198
- return type(self)(value=self.value / other, units=self.units)
199
- elif isinstance(other, type(self)):
200
- return self.value / other.convert(self.units)
201
- else:
202
- raise TypeError(
203
- f"Cannot divide self: {type(self).__name__} with other: {type(other).__name__}. Only {type(self).__name__}, int and float are allowed."
204
- )
205
-
206
- def __truediv__(self: TClass, other: TClass | int | float) -> TClass | float:
207
- if isinstance(other, int) or isinstance(other, float):
208
- return type(self)(value=self.value / other, units=self.units)
209
- elif isinstance(other, type(self)):
210
- return self.value / other.convert(self.units)
211
- else:
212
- raise TypeError(
213
- f"Cannot divide self: {type(self).__name__} with other: {type(other).__name__}. Only {type(self).__name__}, int and float are allowed."
214
- )
215
-
216
-
217
- class UnitTypesLength(str, Enum):
218
- """Units of length.
219
-
220
- Attributes
221
- ----------
222
- meters : meters
223
- centimeters : centimeters
224
- millimeters : millimeters
225
- feet : feet
226
- inch : inch
227
- miles : miles
228
- """
229
-
230
- meters = "meters"
231
- centimeters = "centimeters"
232
- millimeters = "millimeters"
233
- feet = "feet"
234
- inch = "inch"
235
- miles = "miles"
236
-
237
-
238
- class UnitTypesArea(str, Enum):
239
- """Units of area.
240
-
241
- Attributes
242
- ----------
243
- m2 : square meters
244
- dm2 : square decimeters
245
- cm2 : square centimeters
246
- mm2 : square millimeters
247
- sf : square feet
248
- """
249
-
250
- m2 = "m2"
251
- dm2 = "dm2"
252
- cm2 = "cm2"
253
- mm2 = "mm2"
254
- sf = "sf"
255
-
256
-
257
- class UnitTypesVolume(str, Enum):
258
- """Units of volume.
259
-
260
- Attributes
261
- ----------
262
- m3 : cubic meters
263
- cf : cubic feet
264
- """
265
-
266
- m3 = "m3"
267
- cf = "cf"
268
-
269
-
270
- class UnitTypesVelocity(str, Enum):
271
- """Units of velocity.
272
-
273
- Attributes
274
- ----------
275
- mps : meters per second
276
- knots : nautical miles per hour
277
- mph : miles per hour
278
- """
279
-
280
- mps = "m/s"
281
- knots = "knots"
282
- mph = "mph"
283
-
284
-
285
- class UnitTypesDirection(str, Enum):
286
- """Units of direction.
287
-
288
- Attributes
289
- ----------
290
- degrees : degrees
291
- """
292
-
293
- degrees = "deg N"
294
-
295
-
296
- class UnitTypesTime(str, Enum):
297
- """Units of time.
298
-
299
- Attributes
300
- ----------
301
- seconds : seconds
302
- minutes : minutes
303
- hours : hours
304
- days : days
305
- """
306
-
307
- seconds = "seconds"
308
- minutes = "minutes"
309
- hours = "hours"
310
- days = "days"
311
-
312
-
313
- class UnitTypesDischarge(str, Enum):
314
- """Units of discharge.
315
-
316
- Attributes
317
- ----------
318
- cfs : cubic feet per second
319
- cms : cubic meters per second
320
- """
321
-
322
- cfs = "cfs"
323
- cms = "m3/s"
324
-
325
-
326
- class UnitTypesIntensity(str, Enum):
327
- """Units of intensity.
328
-
329
- Attributes
330
- ----------
331
- inch_hr : inch per hour
332
- mm_hr : millimeter per hour
333
- """
334
-
335
- inch_hr = "inch/hr"
336
- mm_hr = "mm/hr"
337
-
338
-
339
- class VerticalReference(str, Enum):
340
- """Vertical reference for height.
341
-
342
- Attributes
343
- ----------
344
- floodmap : Use the floodmap as reference.
345
- datum : Use the datum as reference.
346
- """
347
-
348
- floodmap = "floodmap"
349
- datum = "datum"
350
-
351
-
352
- class UnitfulLength(ValueUnitPair[UnitTypesLength]):
353
- """Combination of length and unit.
354
-
355
- Attributes
356
- ----------
357
- value : float
358
- The length value.
359
- units : UnitTypesLength
360
- The unit of length.
361
- """
362
-
363
- _CONVERSION_FACTORS: dict[UnitTypesLength, float] = {
364
- UnitTypesLength.meters: 1.0,
365
- UnitTypesLength.centimeters: 100.0,
366
- UnitTypesLength.millimeters: 1000.0,
367
- UnitTypesLength.feet: 3.28084,
368
- UnitTypesLength.inch: 1.0 / 0.0254,
369
- UnitTypesLength.miles: 1 / 1609.344,
370
- }
371
- _DEFAULT_UNIT: UnitTypesLength = UnitTypesLength.meters
372
-
373
-
374
- class UnitfulHeight(UnitfulLength):
375
- """Combination of height and unit.
376
-
377
- Attributes
378
- ----------
379
- value : float
380
- The height value, must be greater than or equal to 0.
381
- units : UnitTypesLength
382
- The unit of height.
383
- """
384
-
385
- value: float = Field(ge=0.0)
386
-
387
-
388
- class UnitfulLengthRefValue(UnitfulLength):
389
- """Combination of length and unit with a reference value.
390
-
391
- Attributes
392
- ----------
393
- value : float
394
- The length value, must be greater than or equal to 0.
395
- units : UnitTypesLength
396
- The unit of length.
397
- type : VerticalReference
398
- The vertical reference for the length.
399
- """
400
-
401
- type: VerticalReference
402
-
403
-
404
- class UnitfulArea(ValueUnitPair[UnitTypesArea]):
405
- """Combination of area and unit.
406
-
407
- Attributes
408
- ----------
409
- value : float
410
- The area value, must be greater than or equal to 0.
411
- units : UnitTypesArea
412
- The unit of area.
413
-
414
- """
415
-
416
- _CONVERSION_FACTORS: dict[UnitTypesArea, float] = {
417
- UnitTypesArea.m2: 1,
418
- UnitTypesArea.dm2: 100,
419
- UnitTypesArea.cm2: 10_000,
420
- UnitTypesArea.mm2: 1_000_000,
421
- UnitTypesArea.sf: 10.764,
422
- }
423
- _DEFAULT_UNIT: UnitTypesArea = UnitTypesArea.m2
424
- value: float = Field(ge=0.0)
425
-
426
-
427
- class UnitfulVelocity(ValueUnitPair[UnitTypesVelocity]):
428
- """Combination of velocity and unit.
429
-
430
- Attributes
431
- ----------
432
- value : float
433
- The velocity value, must be greater than or equal to 0.
434
- units : UnitTypesVelocity
435
- The unit of velocity.
436
- """
437
-
438
- _CONVERSION_FACTORS: dict[UnitTypesVelocity, float] = {
439
- UnitTypesVelocity.mph: 2.236936,
440
- UnitTypesVelocity.mps: 1,
441
- UnitTypesVelocity.knots: 1.943844,
442
- }
443
- _DEFAULT_UNIT: UnitTypesVelocity = UnitTypesVelocity.mps
444
- value: float = Field(ge=0.0)
445
-
446
-
447
- class UnitfulDirection(ValueUnitPair[UnitTypesDirection]):
448
- """Combination of direction and unit.
449
-
450
- Attributes
451
- ----------
452
- value : float
453
- The direction value, must be greater than or equal to 0 and less than or equal to 360.
454
- units : UnitTypesDirection
455
- The unit of direction.
456
- """
457
-
458
- _CONVERSION_FACTORS: dict[UnitTypesDirection, float] = {
459
- UnitTypesDirection.degrees: 1.0,
460
- }
461
- _DEFAULT_UNIT: UnitTypesDirection = UnitTypesDirection.degrees
462
-
463
- value: float = Field(ge=0.0, le=360.0)
464
-
465
-
466
- class UnitfulDischarge(ValueUnitPair[UnitTypesDischarge]):
467
- """Combination of discharge and unit.
468
-
469
- Attributes
470
- ----------
471
- value : float
472
- The discharge value, must be greater than or equal to 0.
473
- units : UnitTypesDischarge
474
- The unit of discharge.
475
- """
476
-
477
- _CONVERSION_FACTORS: dict[UnitTypesDischarge, float] = {
478
- UnitTypesDischarge.cfs: 35.314684921034,
479
- UnitTypesDischarge.cms: 1,
480
- }
481
- _DEFAULT_UNIT: UnitTypesDischarge = UnitTypesDischarge.cms
482
-
483
- value: float = Field(ge=0.0)
484
-
485
-
486
- class UnitfulIntensity(ValueUnitPair[UnitTypesIntensity]):
487
- """Combination of intensity and unit.
488
-
489
- Attributes
490
- ----------
491
- value : float
492
- The intensity value, must be greater than or equal to 0.
493
- units : UnitTypesIntensity
494
- The unit of intensity.
495
- """
496
-
497
- _CONVERSION_FACTORS: dict[UnitTypesIntensity, float] = {
498
- UnitTypesIntensity.inch_hr: 1 / 25.39544832,
499
- UnitTypesIntensity.mm_hr: 1,
500
- }
501
- _DEFAULT_UNIT: UnitTypesIntensity = UnitTypesIntensity.mm_hr
502
- value: float = Field(ge=0.0)
503
-
504
-
505
- class UnitfulVolume(ValueUnitPair[UnitTypesVolume]):
506
- """Combination of volume and unit.
507
-
508
- Attributes
509
- ----------
510
- value : float
511
- The volume value, must be greater than or equal to 0.
512
- units : UnitTypesVolume
513
- The unit of volume.
514
- """
515
-
516
- _CONVERSION_FACTORS: dict[UnitTypesVolume, float] = {
517
- UnitTypesVolume.m3: 1.0,
518
- UnitTypesVolume.cf: 35.3146667,
519
- }
520
- _DEFAULT_UNIT: UnitTypesVolume = UnitTypesVolume.m3
521
-
522
- value: float = Field(ge=0.0)
523
-
524
-
525
- class UnitfulTime(ValueUnitPair[UnitTypesTime]):
526
- """Combination of time and unit.
527
-
528
- Attributes
529
- ----------
530
- value : float
531
- The time value.
532
- units : UnitTypesTime
533
- The unit of time.
534
- """
535
-
536
- _CONVERSION_FACTORS: dict[UnitTypesTime, float] = {
537
- UnitTypesTime.days: 1.0 / 24.0,
538
- UnitTypesTime.hours: 1.0,
539
- UnitTypesTime.minutes: 60.0,
540
- UnitTypesTime.seconds: 60.0 * 60.0,
541
- }
542
- _DEFAULT_UNIT: UnitTypesTime = UnitTypesTime.hours
543
-
544
- @staticmethod
545
- def from_timedelta(td: timedelta) -> "UnitfulTime":
546
- """Convert given timedelta to UnitfulTime object."""
547
- return UnitfulTime(value=td.total_seconds(), units=UnitTypesTime.seconds)
548
-
549
- def to_timedelta(self) -> timedelta:
550
- """Convert given time to datetime.deltatime object, relative to UnitfulTime(0, Any).
551
-
552
- Returns
553
- -------
554
- datetime.timedelta
555
- datetime.timedelta object with representation: (days, seconds, microseconds)
556
- """
557
- return timedelta(seconds=self.convert(UnitTypesTime.seconds))
558
-
559
-
560
- ValueUnitPairs = Union[
561
- UnitfulLength,
562
- UnitfulTime,
563
- UnitfulDischarge,
564
- UnitfulDirection,
565
- UnitfulVelocity,
566
- UnitfulIntensity,
567
- UnitfulArea,
568
- UnitfulVolume,
569
- ]
570
-
571
- UNIT_TO_CLASS: dict[enum.EnumMeta, Type[ValueUnitPairs]] = {
572
- UnitTypesLength: UnitfulLength,
573
- UnitTypesTime: UnitfulTime,
574
- UnitTypesDischarge: UnitfulDischarge,
575
- UnitTypesDirection: UnitfulDirection,
576
- UnitTypesVelocity: UnitfulVelocity,
577
- UnitTypesIntensity: UnitfulIntensity,
578
- UnitTypesArea: UnitfulArea,
579
- UnitTypesVolume: UnitfulVolume,
580
- }
1
+ import enum
2
+ import math
3
+ from abc import ABC
4
+ from datetime import timedelta
5
+ from enum import Enum
6
+ from typing import Generic, Optional, Type, TypeVar, Union
7
+
8
+ from pydantic import BaseModel, Field, model_validator
9
+
10
+ __all__ = [
11
+ "ValueUnitPair",
12
+ "VerticalReference",
13
+ "UnitTypesLength",
14
+ "UnitTypesArea",
15
+ "UnitTypesVolume",
16
+ "UnitTypesVelocity",
17
+ "UnitTypesDirection",
18
+ "UnitTypesTime",
19
+ "UnitTypesDischarge",
20
+ "UnitTypesIntensity",
21
+ "UnitfulLength",
22
+ "UnitfulHeight",
23
+ "UnitfulLengthRefValue",
24
+ "UnitfulArea",
25
+ "UnitfulVelocity",
26
+ "UnitfulDirection",
27
+ "UnitfulDischarge",
28
+ "UnitfulIntensity",
29
+ "UnitfulVolume",
30
+ "UnitfulTime",
31
+ "ValueUnitPairs",
32
+ ]
33
+
34
+ TUnit = TypeVar("TUnit", bound=enum.Enum)
35
+ TClass = TypeVar("TClass", bound="ValueUnitPair")
36
+
37
+
38
+ class ValueUnitPair(ABC, BaseModel, Generic[TUnit]):
39
+ """
40
+ Represents a value with associated units.
41
+
42
+ Attributes
43
+ ----------
44
+ value: float
45
+ The numerical value.
46
+ units : Unit
47
+ The units of the value.
48
+ """
49
+
50
+ _DEFAULT_UNIT: TUnit
51
+ _CONVERSION_FACTORS: dict[TUnit, float]
52
+
53
+ value: float
54
+ units: TUnit
55
+
56
+ def convert(self, new_units: TUnit) -> float:
57
+ """Return the value converted to the new units.
58
+
59
+ Parameters
60
+ ----------
61
+ new_units : Unit
62
+ The new units.
63
+
64
+ Returns
65
+ -------
66
+ converted_value : float
67
+ The converted value.
68
+ """
69
+ if new_units not in self._CONVERSION_FACTORS:
70
+ raise ValueError(f"Invalid units: {new_units}")
71
+ in_default_units = self.value / self._CONVERSION_FACTORS[self.units]
72
+ return in_default_units * self._CONVERSION_FACTORS[new_units]
73
+
74
+ def transform(self: TClass, new_units: TUnit) -> TClass:
75
+ """Return a new ValueUnitPair instance with the value converted to the new units.
76
+
77
+ Parameters
78
+ ----------
79
+ new_units : Unit
80
+ The new units.
81
+
82
+ Returns
83
+ -------
84
+ value_unit_pair : ValueUnitPair
85
+ The new ValueUnitPair instance with the value converted to the new units and the new units.
86
+ """
87
+ return type(self)(value=self.convert(new_units), units=new_units)
88
+
89
+ @model_validator(mode="before")
90
+ @classmethod
91
+ def extract_unit_class(cls, data: Optional[dict]) -> "ValueUnitPair":
92
+ if cls is not ValueUnitPair:
93
+ return data # Already in the right subclass, don't interfere
94
+
95
+ if not isinstance(data, dict):
96
+ raise TypeError("Expected dictionary for deserialization.")
97
+
98
+ str_unit = data.get("units")
99
+ if not str_unit:
100
+ raise ValueError("Missing 'units' field in input data.")
101
+
102
+ for unit_enum_cls, vu_cls in UNIT_TO_CLASS.items():
103
+ try:
104
+ enum_unit = unit_enum_cls(str_unit)
105
+ data["units"] = enum_unit
106
+ return vu_cls(**data)
107
+ except ValueError:
108
+ continue
109
+
110
+ raise ValueError(f"Unsupported or unknown unit: {str_unit}")
111
+
112
+ def __str__(self) -> str:
113
+ return f"{self.value} {self.units.value}"
114
+
115
+ def __repr__(self) -> str:
116
+ return f"{type(self).__name__}(value={self.value}, units={self.units})"
117
+
118
+ def __sub__(self: TClass, other: TClass) -> TClass:
119
+ if not isinstance(other, type(self)):
120
+ raise TypeError(
121
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
122
+ )
123
+ return type(self)(
124
+ value=self.value - other.convert(self.units), units=self.units
125
+ )
126
+
127
+ def __add__(self: TClass, other: TClass) -> TClass:
128
+ if not isinstance(other, type(self)):
129
+ raise TypeError(
130
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
131
+ )
132
+ return type(self)(
133
+ value=self.value + other.convert(self.units), units=self.units
134
+ )
135
+
136
+ def __eq__(self: TClass, other: TClass) -> bool:
137
+ if not isinstance(other, ValueUnitPair):
138
+ raise TypeError(
139
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
140
+ )
141
+ if not (hasattr(other, "value") and hasattr(other, "units")):
142
+ raise AttributeError(f"Incomplete UnitfulValue instance: {other}")
143
+
144
+ if not isinstance(other.units, type(self.units)):
145
+ raise TypeError(
146
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
147
+ )
148
+
149
+ return math.isclose(
150
+ self.value, other.convert(self.units), rel_tol=1e-2
151
+ ) # 1% relative tolerance for equality. So 1.0 == 1.01 evaluates to True
152
+
153
+ def __lt__(self: TClass, other: TClass) -> bool:
154
+ if not isinstance(other, type(self)):
155
+ raise TypeError(
156
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
157
+ )
158
+ return self.value < other.convert(self.units)
159
+
160
+ def __gt__(self: TClass, other: TClass) -> bool:
161
+ if not isinstance(other, type(self)):
162
+ raise TypeError(
163
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
164
+ )
165
+ return self.value > other.convert(self.units)
166
+
167
+ def __ge__(self: TClass, other: TClass) -> bool:
168
+ if not isinstance(other, type(self)):
169
+ raise TypeError(
170
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
171
+ )
172
+ return (self > other) or (self == other)
173
+
174
+ def __le__(self: TClass, other: TClass) -> bool:
175
+ if not isinstance(other, type(self)):
176
+ raise TypeError(
177
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
178
+ )
179
+ return (self < other) or (self == other)
180
+
181
+ def __ne__(self: TClass, other: TClass) -> bool:
182
+ if not isinstance(other, type(self)):
183
+ raise TypeError(
184
+ f"Cannot compare self: {type(self).__name__} to other: {type(other).__name__}"
185
+ )
186
+ return not (self == other)
187
+
188
+ def __mul__(self: TClass, other: int | float) -> TClass:
189
+ if isinstance(other, int) or isinstance(other, float):
190
+ return type(self)(value=self.value * other, units=self.units)
191
+ else:
192
+ raise TypeError(
193
+ f"Cannot multiply self: {type(self).__name__} with other: {type(other).__name__}. Only int and float are allowed."
194
+ )
195
+
196
+ def __div__(self: TClass, other: TClass | int | float) -> TClass | float:
197
+ if isinstance(other, int) or isinstance(other, float):
198
+ return type(self)(value=self.value / other, units=self.units)
199
+ elif isinstance(other, type(self)):
200
+ return self.value / other.convert(self.units)
201
+ else:
202
+ raise TypeError(
203
+ f"Cannot divide self: {type(self).__name__} with other: {type(other).__name__}. Only {type(self).__name__}, int and float are allowed."
204
+ )
205
+
206
+ def __truediv__(self: TClass, other: TClass | int | float) -> TClass | float:
207
+ if isinstance(other, int) or isinstance(other, float):
208
+ return type(self)(value=self.value / other, units=self.units)
209
+ elif isinstance(other, type(self)):
210
+ return self.value / other.convert(self.units)
211
+ else:
212
+ raise TypeError(
213
+ f"Cannot divide self: {type(self).__name__} with other: {type(other).__name__}. Only {type(self).__name__}, int and float are allowed."
214
+ )
215
+
216
+
217
+ class UnitTypesLength(str, Enum):
218
+ """Units of length.
219
+
220
+ Attributes
221
+ ----------
222
+ meters : meters
223
+ centimeters : centimeters
224
+ millimeters : millimeters
225
+ feet : feet
226
+ inch : inch
227
+ miles : miles
228
+ """
229
+
230
+ meters = "meters"
231
+ centimeters = "centimeters"
232
+ millimeters = "millimeters"
233
+ feet = "feet"
234
+ inch = "inch"
235
+ miles = "miles"
236
+
237
+
238
+ class UnitTypesArea(str, Enum):
239
+ """Units of area.
240
+
241
+ Attributes
242
+ ----------
243
+ m2 : square meters
244
+ dm2 : square decimeters
245
+ cm2 : square centimeters
246
+ mm2 : square millimeters
247
+ sf : square feet
248
+ """
249
+
250
+ m2 = "m2"
251
+ dm2 = "dm2"
252
+ cm2 = "cm2"
253
+ mm2 = "mm2"
254
+ sf = "sf"
255
+
256
+
257
+ class UnitTypesVolume(str, Enum):
258
+ """Units of volume.
259
+
260
+ Attributes
261
+ ----------
262
+ m3 : cubic meters
263
+ cf : cubic feet
264
+ """
265
+
266
+ m3 = "m3"
267
+ cf = "cf"
268
+
269
+
270
+ class UnitTypesVelocity(str, Enum):
271
+ """Units of velocity.
272
+
273
+ Attributes
274
+ ----------
275
+ mps : meters per second
276
+ knots : nautical miles per hour
277
+ mph : miles per hour
278
+ """
279
+
280
+ mps = "m/s"
281
+ knots = "knots"
282
+ mph = "mph"
283
+
284
+
285
+ class UnitTypesDirection(str, Enum):
286
+ """Units of direction.
287
+
288
+ Attributes
289
+ ----------
290
+ degrees : degrees
291
+ """
292
+
293
+ degrees = "deg N"
294
+
295
+
296
+ class UnitTypesTime(str, Enum):
297
+ """Units of time.
298
+
299
+ Attributes
300
+ ----------
301
+ seconds : seconds
302
+ minutes : minutes
303
+ hours : hours
304
+ days : days
305
+ """
306
+
307
+ seconds = "seconds"
308
+ minutes = "minutes"
309
+ hours = "hours"
310
+ days = "days"
311
+
312
+
313
+ class UnitTypesDischarge(str, Enum):
314
+ """Units of discharge.
315
+
316
+ Attributes
317
+ ----------
318
+ cfs : cubic feet per second
319
+ cms : cubic meters per second
320
+ """
321
+
322
+ cfs = "cfs"
323
+ cms = "m3/s"
324
+
325
+
326
+ class UnitTypesIntensity(str, Enum):
327
+ """Units of intensity.
328
+
329
+ Attributes
330
+ ----------
331
+ inch_hr : inch per hour
332
+ mm_hr : millimeter per hour
333
+ """
334
+
335
+ inch_hr = "inch/hr"
336
+ mm_hr = "mm/hr"
337
+
338
+
339
+ class VerticalReference(str, Enum):
340
+ """Vertical reference for height.
341
+
342
+ Attributes
343
+ ----------
344
+ floodmap : Use the floodmap as reference.
345
+ datum : Use the datum as reference.
346
+ """
347
+
348
+ floodmap = "floodmap"
349
+ datum = "datum"
350
+
351
+
352
+ class UnitfulLength(ValueUnitPair[UnitTypesLength]):
353
+ """Combination of length and unit.
354
+
355
+ Attributes
356
+ ----------
357
+ value : float
358
+ The length value.
359
+ units : UnitTypesLength
360
+ The unit of length.
361
+ """
362
+
363
+ _CONVERSION_FACTORS: dict[UnitTypesLength, float] = {
364
+ UnitTypesLength.meters: 1.0,
365
+ UnitTypesLength.centimeters: 100.0,
366
+ UnitTypesLength.millimeters: 1000.0,
367
+ UnitTypesLength.feet: 3.28084,
368
+ UnitTypesLength.inch: 1.0 / 0.0254,
369
+ UnitTypesLength.miles: 1 / 1609.344,
370
+ }
371
+ _DEFAULT_UNIT: UnitTypesLength = UnitTypesLength.meters
372
+
373
+
374
+ class UnitfulHeight(UnitfulLength):
375
+ """Combination of height and unit.
376
+
377
+ Attributes
378
+ ----------
379
+ value : float
380
+ The height value, must be greater than or equal to 0.
381
+ units : UnitTypesLength
382
+ The unit of height.
383
+ """
384
+
385
+ value: float = Field(ge=0.0)
386
+
387
+
388
+ class UnitfulLengthRefValue(UnitfulLength):
389
+ """Combination of length and unit with a reference value.
390
+
391
+ Attributes
392
+ ----------
393
+ value : float
394
+ The length value, must be greater than or equal to 0.
395
+ units : UnitTypesLength
396
+ The unit of length.
397
+ type : VerticalReference
398
+ The vertical reference for the length.
399
+ """
400
+
401
+ type: VerticalReference
402
+
403
+
404
+ class UnitfulArea(ValueUnitPair[UnitTypesArea]):
405
+ """Combination of area and unit.
406
+
407
+ Attributes
408
+ ----------
409
+ value : float
410
+ The area value, must be greater than or equal to 0.
411
+ units : UnitTypesArea
412
+ The unit of area.
413
+
414
+ """
415
+
416
+ _CONVERSION_FACTORS: dict[UnitTypesArea, float] = {
417
+ UnitTypesArea.m2: 1,
418
+ UnitTypesArea.dm2: 100,
419
+ UnitTypesArea.cm2: 10_000,
420
+ UnitTypesArea.mm2: 1_000_000,
421
+ UnitTypesArea.sf: 10.764,
422
+ }
423
+ _DEFAULT_UNIT: UnitTypesArea = UnitTypesArea.m2
424
+ value: float = Field(ge=0.0)
425
+
426
+
427
+ class UnitfulVelocity(ValueUnitPair[UnitTypesVelocity]):
428
+ """Combination of velocity and unit.
429
+
430
+ Attributes
431
+ ----------
432
+ value : float
433
+ The velocity value, must be greater than or equal to 0.
434
+ units : UnitTypesVelocity
435
+ The unit of velocity.
436
+ """
437
+
438
+ _CONVERSION_FACTORS: dict[UnitTypesVelocity, float] = {
439
+ UnitTypesVelocity.mph: 2.236936,
440
+ UnitTypesVelocity.mps: 1,
441
+ UnitTypesVelocity.knots: 1.943844,
442
+ }
443
+ _DEFAULT_UNIT: UnitTypesVelocity = UnitTypesVelocity.mps
444
+ value: float = Field(ge=0.0)
445
+
446
+
447
+ class UnitfulDirection(ValueUnitPair[UnitTypesDirection]):
448
+ """Combination of direction and unit.
449
+
450
+ Attributes
451
+ ----------
452
+ value : float
453
+ The direction value, must be greater than or equal to 0 and less than or equal to 360.
454
+ units : UnitTypesDirection
455
+ The unit of direction.
456
+ """
457
+
458
+ _CONVERSION_FACTORS: dict[UnitTypesDirection, float] = {
459
+ UnitTypesDirection.degrees: 1.0,
460
+ }
461
+ _DEFAULT_UNIT: UnitTypesDirection = UnitTypesDirection.degrees
462
+
463
+ value: float = Field(ge=0.0, le=360.0)
464
+
465
+
466
+ class UnitfulDischarge(ValueUnitPair[UnitTypesDischarge]):
467
+ """Combination of discharge and unit.
468
+
469
+ Attributes
470
+ ----------
471
+ value : float
472
+ The discharge value, must be greater than or equal to 0.
473
+ units : UnitTypesDischarge
474
+ The unit of discharge.
475
+ """
476
+
477
+ _CONVERSION_FACTORS: dict[UnitTypesDischarge, float] = {
478
+ UnitTypesDischarge.cfs: 35.314684921034,
479
+ UnitTypesDischarge.cms: 1,
480
+ }
481
+ _DEFAULT_UNIT: UnitTypesDischarge = UnitTypesDischarge.cms
482
+
483
+ value: float = Field(ge=0.0)
484
+
485
+
486
+ class UnitfulIntensity(ValueUnitPair[UnitTypesIntensity]):
487
+ """Combination of intensity and unit.
488
+
489
+ Attributes
490
+ ----------
491
+ value : float
492
+ The intensity value, must be greater than or equal to 0.
493
+ units : UnitTypesIntensity
494
+ The unit of intensity.
495
+ """
496
+
497
+ _CONVERSION_FACTORS: dict[UnitTypesIntensity, float] = {
498
+ UnitTypesIntensity.inch_hr: 1 / 25.39544832,
499
+ UnitTypesIntensity.mm_hr: 1,
500
+ }
501
+ _DEFAULT_UNIT: UnitTypesIntensity = UnitTypesIntensity.mm_hr
502
+ value: float = Field(ge=0.0)
503
+
504
+
505
+ class UnitfulVolume(ValueUnitPair[UnitTypesVolume]):
506
+ """Combination of volume and unit.
507
+
508
+ Attributes
509
+ ----------
510
+ value : float
511
+ The volume value, must be greater than or equal to 0.
512
+ units : UnitTypesVolume
513
+ The unit of volume.
514
+ """
515
+
516
+ _CONVERSION_FACTORS: dict[UnitTypesVolume, float] = {
517
+ UnitTypesVolume.m3: 1.0,
518
+ UnitTypesVolume.cf: 35.3146667,
519
+ }
520
+ _DEFAULT_UNIT: UnitTypesVolume = UnitTypesVolume.m3
521
+
522
+ value: float = Field(ge=0.0)
523
+
524
+
525
+ class UnitfulTime(ValueUnitPair[UnitTypesTime]):
526
+ """Combination of time and unit.
527
+
528
+ Attributes
529
+ ----------
530
+ value : float
531
+ The time value.
532
+ units : UnitTypesTime
533
+ The unit of time.
534
+ """
535
+
536
+ _CONVERSION_FACTORS: dict[UnitTypesTime, float] = {
537
+ UnitTypesTime.days: 1.0 / 24.0,
538
+ UnitTypesTime.hours: 1.0,
539
+ UnitTypesTime.minutes: 60.0,
540
+ UnitTypesTime.seconds: 60.0 * 60.0,
541
+ }
542
+ _DEFAULT_UNIT: UnitTypesTime = UnitTypesTime.hours
543
+
544
+ @staticmethod
545
+ def from_timedelta(td: timedelta) -> "UnitfulTime":
546
+ """Convert given timedelta to UnitfulTime object."""
547
+ return UnitfulTime(value=td.total_seconds(), units=UnitTypesTime.seconds)
548
+
549
+ def to_timedelta(self) -> timedelta:
550
+ """Convert given time to datetime.deltatime object, relative to UnitfulTime(0, Any).
551
+
552
+ Returns
553
+ -------
554
+ datetime.timedelta
555
+ datetime.timedelta object with representation: (days, seconds, microseconds)
556
+ """
557
+ return timedelta(seconds=self.convert(UnitTypesTime.seconds))
558
+
559
+
560
+ ValueUnitPairs = Union[
561
+ UnitfulLength,
562
+ UnitfulTime,
563
+ UnitfulDischarge,
564
+ UnitfulDirection,
565
+ UnitfulVelocity,
566
+ UnitfulIntensity,
567
+ UnitfulArea,
568
+ UnitfulVolume,
569
+ ]
570
+
571
+ UNIT_TO_CLASS: dict[enum.EnumMeta, Type[ValueUnitPairs]] = {
572
+ UnitTypesLength: UnitfulLength,
573
+ UnitTypesTime: UnitfulTime,
574
+ UnitTypesDischarge: UnitfulDischarge,
575
+ UnitTypesDirection: UnitfulDirection,
576
+ UnitTypesVelocity: UnitfulVelocity,
577
+ UnitTypesIntensity: UnitfulIntensity,
578
+ UnitTypesArea: UnitfulArea,
579
+ UnitTypesVolume: UnitfulVolume,
580
+ }