NREL-erad 0.0.0a0__py3-none-any.whl → 0.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.
- erad/__init__.py +1 -0
- erad/constants.py +59 -11
- erad/default_fragility_curves/__init__.py +15 -0
- erad/default_fragility_curves/default_fire_boundary_dist.py +94 -0
- erad/default_fragility_curves/default_flood_depth.py +108 -0
- erad/default_fragility_curves/default_flood_velocity.py +101 -0
- erad/default_fragility_curves/default_fragility_curves.py +23 -0
- erad/default_fragility_curves/default_peak_ground_acceleration.py +163 -0
- erad/default_fragility_curves/default_peak_ground_velocity.py +94 -0
- erad/default_fragility_curves/default_wind_speed.py +94 -0
- erad/enums.py +40 -0
- erad/gdm_mapping.py +83 -0
- erad/models/__init__.py +1 -0
- erad/models/asset.py +287 -0
- erad/models/asset_mapping.py +20 -0
- erad/models/fragility_curve.py +116 -0
- erad/models/hazard/__init__.py +5 -0
- erad/models/hazard/base_models.py +12 -0
- erad/models/hazard/common.py +26 -0
- erad/models/hazard/earthquake.py +93 -0
- erad/models/hazard/flood.py +83 -0
- erad/models/hazard/wild_fire.py +121 -0
- erad/models/hazard/wind.py +143 -0
- erad/models/probability.py +73 -0
- erad/probability_builder.py +35 -0
- erad/quantities.py +25 -0
- erad/runner.py +122 -0
- erad/systems/__init__.py +2 -0
- erad/systems/asset_system.py +414 -0
- erad/systems/hazard_system.py +122 -0
- nrel_erad-0.1.0.dist-info/METADATA +55 -0
- nrel_erad-0.1.0.dist-info/RECORD +35 -0
- {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.0.dist-info}/WHEEL +1 -1
- NREL_erad-0.0.0a0.dist-info/METADATA +0 -61
- NREL_erad-0.0.0a0.dist-info/RECORD +0 -42
- erad/cypher_queries/load_data_v1.cypher +0 -212
- erad/data/World_Earthquakes_1960_2016.csv +0 -23410
- erad/db/__init__.py +0 -0
- erad/db/assets/__init__.py +0 -0
- erad/db/assets/critical_infras.py +0 -171
- erad/db/assets/distribution_lines.py +0 -101
- erad/db/credential_model.py +0 -20
- erad/db/disaster_input_model.py +0 -23
- erad/db/inject_earthquake.py +0 -52
- erad/db/inject_flooding.py +0 -53
- erad/db/neo4j_.py +0 -162
- erad/db/utils.py +0 -14
- erad/exceptions.py +0 -68
- erad/metrics/__init__.py +0 -0
- erad/metrics/check_microgrid.py +0 -208
- erad/metrics/metric.py +0 -178
- erad/programs/__init__.py +0 -0
- erad/programs/backup.py +0 -62
- erad/programs/microgrid.py +0 -45
- erad/scenarios/__init__.py +0 -0
- erad/scenarios/abstract_scenario.py +0 -103
- erad/scenarios/common.py +0 -93
- erad/scenarios/earthquake_scenario.py +0 -161
- erad/scenarios/fire_scenario.py +0 -160
- erad/scenarios/flood_scenario.py +0 -494
- erad/scenarios/utilities.py +0 -76
- erad/scenarios/wind_scenario.py +0 -89
- erad/utils/__init__.py +0 -0
- erad/utils/ditto_utils.py +0 -252
- erad/utils/hifld_utils.py +0 -147
- erad/utils/opendss_utils.py +0 -357
- erad/utils/overpass.py +0 -76
- erad/utils/util.py +0 -178
- erad/visualization/__init__.py +0 -0
- erad/visualization/plot_graph.py +0 -218
- {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.0.dist-info/licenses}/LICENSE.txt +0 -0
- {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
import erad.models.fragility_curve as frag
|
2
|
+
from erad.enums import AssetTypes
|
3
|
+
from erad.quantities import Speed
|
4
|
+
|
5
|
+
|
6
|
+
DEFAULT_WIND_SPEED_FRAGILITY_CURVES = frag.HazardFragilityCurves(
|
7
|
+
asset_state_param="wind_speed",
|
8
|
+
curves=[
|
9
|
+
frag.FragilityCurve(
|
10
|
+
asset_type=AssetTypes.switch,
|
11
|
+
prob_function=frag.ProbabilityFunction(
|
12
|
+
distribution="lognorm", parameters=[Speed(0.45, "m/s"), Speed(50, "m/s"), 1 / 0.45]
|
13
|
+
),
|
14
|
+
),
|
15
|
+
frag.FragilityCurve(
|
16
|
+
asset_type=AssetTypes.battery_storage,
|
17
|
+
prob_function=frag.ProbabilityFunction(
|
18
|
+
distribution="lognorm", parameters=[Speed(0.45, "m/s"), Speed(50, "m/s"), 1 / 0.45]
|
19
|
+
),
|
20
|
+
),
|
21
|
+
frag.FragilityCurve(
|
22
|
+
asset_type=AssetTypes.distribution_junction_box,
|
23
|
+
prob_function=frag.ProbabilityFunction(
|
24
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(50, "m/s"), 1 / 0.4]
|
25
|
+
),
|
26
|
+
),
|
27
|
+
frag.FragilityCurve(
|
28
|
+
asset_type=AssetTypes.distribution_overhead_lines,
|
29
|
+
prob_function=frag.ProbabilityFunction(
|
30
|
+
distribution="lognorm", parameters=[Speed(0.35, "m/s"), Speed(45, "m/s"), 1 / 0.35]
|
31
|
+
),
|
32
|
+
),
|
33
|
+
frag.FragilityCurve(
|
34
|
+
asset_type=AssetTypes.distribution_poles,
|
35
|
+
prob_function=frag.ProbabilityFunction(
|
36
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(45, "m/s"), 1 / 0.4]
|
37
|
+
),
|
38
|
+
),
|
39
|
+
frag.FragilityCurve(
|
40
|
+
asset_type=AssetTypes.distribution_underground_cables,
|
41
|
+
prob_function=frag.ProbabilityFunction(
|
42
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(55, "m/s"), 1 / 0.4]
|
43
|
+
),
|
44
|
+
),
|
45
|
+
frag.FragilityCurve(
|
46
|
+
asset_type=AssetTypes.solar_panels,
|
47
|
+
prob_function=frag.ProbabilityFunction(
|
48
|
+
distribution="lognorm", parameters=[Speed(0.3, "m/s"), Speed(55, "m/s"), 1 / 0.3]
|
49
|
+
),
|
50
|
+
),
|
51
|
+
frag.FragilityCurve(
|
52
|
+
asset_type=AssetTypes.substation,
|
53
|
+
prob_function=frag.ProbabilityFunction(
|
54
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(55, "m/s"), 1 / 0.40]
|
55
|
+
),
|
56
|
+
),
|
57
|
+
frag.FragilityCurve(
|
58
|
+
asset_type=AssetTypes.transformer_mad_mount,
|
59
|
+
prob_function=frag.ProbabilityFunction(
|
60
|
+
distribution="lognorm", parameters=[Speed(0.35, "m/s"), Speed(50, "m/s"), 1 / 0.35]
|
61
|
+
),
|
62
|
+
),
|
63
|
+
frag.FragilityCurve(
|
64
|
+
asset_type=AssetTypes.transformer_pole_mount,
|
65
|
+
prob_function=frag.ProbabilityFunction(
|
66
|
+
distribution="lognorm", parameters=[Speed(0.3, "m/s"), Speed(45, "m/s"), 1 / 0.30]
|
67
|
+
),
|
68
|
+
),
|
69
|
+
frag.FragilityCurve(
|
70
|
+
asset_type=AssetTypes.transmission_junction_box,
|
71
|
+
prob_function=frag.ProbabilityFunction(
|
72
|
+
distribution="lognorm", parameters=[Speed(0.54, "m/s"), Speed(55, "m/s"), 1 / 0.54]
|
73
|
+
),
|
74
|
+
),
|
75
|
+
frag.FragilityCurve(
|
76
|
+
asset_type=AssetTypes.transmission_overhead_lines,
|
77
|
+
prob_function=frag.ProbabilityFunction(
|
78
|
+
distribution="lognorm", parameters=[Speed(0.35, "m/s"), Speed(50, "m/s"), 1 / 0.35]
|
79
|
+
),
|
80
|
+
),
|
81
|
+
frag.FragilityCurve(
|
82
|
+
asset_type=AssetTypes.transmission_tower,
|
83
|
+
prob_function=frag.ProbabilityFunction(
|
84
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(55, "m/s"), 1 / 0.40]
|
85
|
+
),
|
86
|
+
),
|
87
|
+
frag.FragilityCurve(
|
88
|
+
asset_type=AssetTypes.transmission_underground_cables,
|
89
|
+
prob_function=frag.ProbabilityFunction(
|
90
|
+
distribution="lognorm", parameters=[Speed(0.4, "m/s"), Speed(60, "m/s"), 1 / 0.40]
|
91
|
+
),
|
92
|
+
),
|
93
|
+
],
|
94
|
+
)
|
erad/enums.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from enum import IntEnum
|
2
|
+
|
3
|
+
|
4
|
+
class ScenarioTypes(IntEnum):
|
5
|
+
flood_m = 0
|
6
|
+
wind_m_per_s = 1
|
7
|
+
fire_m = 2
|
8
|
+
earthquake_pga = 3
|
9
|
+
|
10
|
+
|
11
|
+
class NodeTypes(IntEnum):
|
12
|
+
transmission_tower = 5
|
13
|
+
distribution_poles = 6
|
14
|
+
transmission_junction_box = 11
|
15
|
+
distribution_junction_box = 12
|
16
|
+
|
17
|
+
|
18
|
+
class AssetTypes(IntEnum):
|
19
|
+
substation = 0
|
20
|
+
solar_panels = 1
|
21
|
+
distribution_underground_cables = 2
|
22
|
+
transmission_underground_cables = 3
|
23
|
+
battery_storage = 4
|
24
|
+
transmission_tower = 5
|
25
|
+
distribution_poles = 6
|
26
|
+
transmission_overhead_lines = 7
|
27
|
+
distribution_overhead_lines = 8
|
28
|
+
transformer_mad_mount = 9
|
29
|
+
transformer_pole_mount = 10
|
30
|
+
transmission_junction_box = 11
|
31
|
+
distribution_junction_box = 12
|
32
|
+
switch = 13
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def has_value(cls, value):
|
36
|
+
return value in cls._value2member_map_
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def has_asset(cls, asset):
|
40
|
+
return asset in cls.__members__
|
erad/gdm_mapping.py
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
import gdm.distribution.components as gdc
|
2
|
+
import gdm.distribution.equipment as gde
|
3
|
+
|
4
|
+
from erad.enums import AssetTypes
|
5
|
+
from erad.models.asset_mapping import ComponentFilterModel
|
6
|
+
|
7
|
+
|
8
|
+
asset_to_gdm_mapping = {
|
9
|
+
AssetTypes.switch: [
|
10
|
+
ComponentFilterModel(
|
11
|
+
component_type=gdc.DistributionSwitchBase,
|
12
|
+
),
|
13
|
+
],
|
14
|
+
AssetTypes.switch: [
|
15
|
+
ComponentFilterModel(
|
16
|
+
component_type=gdc.DistributionSwitchBase,
|
17
|
+
),
|
18
|
+
],
|
19
|
+
AssetTypes.substation: [
|
20
|
+
ComponentFilterModel(
|
21
|
+
component_type=gdc.DistributionVoltageSource,
|
22
|
+
),
|
23
|
+
],
|
24
|
+
AssetTypes.solar_panels: [
|
25
|
+
ComponentFilterModel(
|
26
|
+
component_type=gdc.DistributionSolar,
|
27
|
+
),
|
28
|
+
],
|
29
|
+
AssetTypes.battery_storage: [
|
30
|
+
ComponentFilterModel(
|
31
|
+
component_type=gdc.DistributionBattery,
|
32
|
+
),
|
33
|
+
],
|
34
|
+
AssetTypes.distribution_underground_cables: [
|
35
|
+
ComponentFilterModel(
|
36
|
+
component_type=gdc.GeometryBranch,
|
37
|
+
component_filter=lambda x: x.equipment.conductors[0].__class__
|
38
|
+
== gde.ConcentricCableEquipment
|
39
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude < 35.0,
|
40
|
+
),
|
41
|
+
ComponentFilterModel(
|
42
|
+
component_type=gdc.MatrixImpedanceBranch,
|
43
|
+
component_filter=lambda x: x.equipment.c_matrix[0, 0]
|
44
|
+
.to("microfarad/kilometer")
|
45
|
+
.magnitude
|
46
|
+
> 0.05
|
47
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude < 35.0,
|
48
|
+
),
|
49
|
+
],
|
50
|
+
AssetTypes.distribution_overhead_lines: [
|
51
|
+
ComponentFilterModel(
|
52
|
+
component_type=gdc.GeometryBranch,
|
53
|
+
component_filter=lambda x: (
|
54
|
+
x.equipment.conductors[0].__class__ == gde.BareConductorEquipment
|
55
|
+
)
|
56
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude < 35.0,
|
57
|
+
),
|
58
|
+
ComponentFilterModel(
|
59
|
+
component_type=gdc.MatrixImpedanceBranch,
|
60
|
+
component_filter=lambda x: x.equipment.c_matrix[0, 0]
|
61
|
+
.to("microfarad/kilometer")
|
62
|
+
.magnitude
|
63
|
+
< 0.05
|
64
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude < 35.0,
|
65
|
+
),
|
66
|
+
],
|
67
|
+
AssetTypes.transmission_underground_cables: [
|
68
|
+
ComponentFilterModel(
|
69
|
+
component_type=gdc.GeometryBranch,
|
70
|
+
component_filter=lambda x: x.equipment.conductors[0].__class__
|
71
|
+
== gde.ConcentricCableEquipment
|
72
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude > 35.0,
|
73
|
+
),
|
74
|
+
],
|
75
|
+
AssetTypes.transmission_overhead_lines: [
|
76
|
+
ComponentFilterModel(
|
77
|
+
component_type=gdc.GeometryBranch,
|
78
|
+
component_filter=lambda x: x.equipment.conductors[0].__class__
|
79
|
+
== gde.BareConductorEquipment
|
80
|
+
and x.buses[0].rated_voltage.to("kilovolt").magnitude > 35.0,
|
81
|
+
),
|
82
|
+
],
|
83
|
+
}
|
erad/models/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.0.1"
|
erad/models/asset.py
ADDED
@@ -0,0 +1,287 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from uuid import UUID
|
3
|
+
import math
|
4
|
+
|
5
|
+
from pydantic import computed_field, Field
|
6
|
+
from infrasys.quantities import Distance
|
7
|
+
from geopy.distance import geodesic
|
8
|
+
from shapely.geometry import Point
|
9
|
+
from pyhigh import get_elevation
|
10
|
+
from infrasys import Component
|
11
|
+
|
12
|
+
from erad.quantities import Acceleration, Speed
|
13
|
+
from erad.models.probability import (
|
14
|
+
AccelerationProbability,
|
15
|
+
DistanceProbability,
|
16
|
+
SpeedProbability,
|
17
|
+
)
|
18
|
+
from erad.enums import AssetTypes
|
19
|
+
import erad.models.hazard as hz
|
20
|
+
|
21
|
+
|
22
|
+
class AssetState(Component):
|
23
|
+
name: str = ""
|
24
|
+
timestamp: datetime
|
25
|
+
wind_speed: SpeedProbability | None = None
|
26
|
+
flood_velocity: SpeedProbability | None = None
|
27
|
+
flood_depth: DistanceProbability | None = None
|
28
|
+
fire_boundary_dist: DistanceProbability | None = None
|
29
|
+
peak_ground_velocity: SpeedProbability | None = None
|
30
|
+
peak_ground_acceleration: AccelerationProbability | None = None
|
31
|
+
|
32
|
+
def calculate_earthquake_vectors(
|
33
|
+
self, asset_coordinate: Point, hazard_model: hz.EarthQuakeModel
|
34
|
+
):
|
35
|
+
"""
|
36
|
+
Sources:
|
37
|
+
|
38
|
+
Atkinson, G.M., & Wald, D.J. (2007). “Did You Feel It?” intensity data: A surprisingly good
|
39
|
+
measure of earthquake ground motion. Seismological Research Letters, 78(3), 362–368.
|
40
|
+
"""
|
41
|
+
|
42
|
+
epicenter_distance = Distance(
|
43
|
+
geodesic(
|
44
|
+
(hazard_model.origin.y, hazard_model.origin.x),
|
45
|
+
(asset_coordinate.y, asset_coordinate.x),
|
46
|
+
).km,
|
47
|
+
"km",
|
48
|
+
)
|
49
|
+
|
50
|
+
hypocentral_distance = (hazard_model.depth**2 + epicenter_distance**2) ** 0.5
|
51
|
+
|
52
|
+
mmi = (
|
53
|
+
3.31
|
54
|
+
+ 1.28 * hazard_model.magnitude
|
55
|
+
- 1.42 * math.log10(hypocentral_distance.to("km").magnitude)
|
56
|
+
) # Modified Mercalli Intensity
|
57
|
+
|
58
|
+
log_pgv = (mmi - 3.78) / 1.47
|
59
|
+
pgv = 10**log_pgv # cm/s
|
60
|
+
self.peak_ground_velocity = SpeedProbability(
|
61
|
+
speed=Speed(pgv, "centimeter/second"),
|
62
|
+
survival_probability=1,
|
63
|
+
)
|
64
|
+
|
65
|
+
log_pga = (mmi - 1.78) / 3.70
|
66
|
+
pga_per_g = 10**log_pga # % of g
|
67
|
+
self.peak_ground_acceleration = AccelerationProbability(
|
68
|
+
acceleration=Acceleration(pga_per_g / 100.0 * 9.80665, "meter/second**2"),
|
69
|
+
survival_probability=1,
|
70
|
+
)
|
71
|
+
|
72
|
+
def calculate_fire_vectors(self, asset_coordinate: Point, hazard_model: hz.FireModel):
|
73
|
+
"""
|
74
|
+
Sources:
|
75
|
+
|
76
|
+
“Integrating Wildfire Risk Modeling with Electric Grid Asset Management” (EPRI)
|
77
|
+
“Modeling and Mitigating Wildfire Risk to the Power Grid” (PNNL, 2021)
|
78
|
+
“A Framework for Assessing Wildfire Ignition Risk on Distribution Systems”
|
79
|
+
(IEEE Transactions on Power Systems, 2020)
|
80
|
+
Improving Grid Safety and Resilience to Mitigate Ignition Incident and Fire Risks – EPRI
|
81
|
+
Wildfire Risk Reduction Methods 2024 – EPRI
|
82
|
+
"""
|
83
|
+
|
84
|
+
distances_to_wild_fire_boundaries = []
|
85
|
+
for wild_fire_area in hazard_model.affected_areas:
|
86
|
+
if wild_fire_area.affected_area.contains(asset_coordinate):
|
87
|
+
distance = 0
|
88
|
+
else:
|
89
|
+
distance = wild_fire_area.affected_area.exterior.distance(asset_coordinate)
|
90
|
+
distances_to_wild_fire_boundaries.append(distance)
|
91
|
+
minimum_distance_km = min(distances_to_wild_fire_boundaries)
|
92
|
+
self.fire_boundary_dist = DistanceProbability(
|
93
|
+
distance=Distance(minimum_distance_km, "kilometer"),
|
94
|
+
survival_probability=1,
|
95
|
+
)
|
96
|
+
|
97
|
+
# survival_probability = 1 - 0.95 * math.exp(-k * minimum_distance_km)
|
98
|
+
|
99
|
+
def calculate_wind_vectors(self, asset_coordinate: Point, hazard_model: hz.WindModel):
|
100
|
+
r = Distance(
|
101
|
+
geodesic(
|
102
|
+
(hazard_model.center.y, hazard_model.center.x),
|
103
|
+
(asset_coordinate.y, asset_coordinate.x),
|
104
|
+
).km,
|
105
|
+
"km",
|
106
|
+
)
|
107
|
+
|
108
|
+
r = r.to("nautical_mile")
|
109
|
+
r_max = hazard_model.radius_of_closest_isobar.to("nautical_mile")
|
110
|
+
r_v_max = hazard_model.radius_of_max_wind.to("nautical_mile")
|
111
|
+
v_max = hazard_model.max_wind_speed.to("knot")
|
112
|
+
|
113
|
+
k = 1.14
|
114
|
+
b = 10
|
115
|
+
|
116
|
+
a = math.log(b) / (r_max - r_v_max)
|
117
|
+
m = (1 / r_v_max) * math.log(k / (k + 1))
|
118
|
+
|
119
|
+
if r >= 0 and r < r_v_max:
|
120
|
+
wind_speed = k * v_max * (1 - math.exp(-m * r))
|
121
|
+
elif r >= r_v_max and r < r_max:
|
122
|
+
wind_speed = v_max * math.exp(-a * (r - r_v_max))
|
123
|
+
else:
|
124
|
+
wind_speed = 0
|
125
|
+
|
126
|
+
self.wind_speed = SpeedProbability(
|
127
|
+
speed=wind_speed.to("miles/hour"),
|
128
|
+
survival_probability=1,
|
129
|
+
)
|
130
|
+
|
131
|
+
def calculate_flood_vectors(
|
132
|
+
self, asset_coordinate: Point, hazard_model: hz.FloodModel, asset_total_elevation: Distance
|
133
|
+
):
|
134
|
+
for area in hazard_model.affected_areas:
|
135
|
+
if area.affected_area.contains(asset_coordinate):
|
136
|
+
self.flood_depth = DistanceProbability(
|
137
|
+
distance=area.water_elevation - asset_total_elevation,
|
138
|
+
survival_probability=1,
|
139
|
+
)
|
140
|
+
self.flood_velocity = SpeedProbability(
|
141
|
+
speed=area.water_velocity,
|
142
|
+
survival_probability=1,
|
143
|
+
)
|
144
|
+
else:
|
145
|
+
self.flood_depth = DistanceProbability(
|
146
|
+
distance=Distance(-9999, "meter"),
|
147
|
+
survival_probability=1,
|
148
|
+
)
|
149
|
+
self.flood_velocity = SpeedProbability(
|
150
|
+
speed=Speed(0, "meter / second"),
|
151
|
+
survival_probability=1,
|
152
|
+
)
|
153
|
+
|
154
|
+
@computed_field
|
155
|
+
@property
|
156
|
+
def survival_probability(self) -> float:
|
157
|
+
wind_survival_probability = self.wind_speed.survival_probability if self.wind_speed else 1
|
158
|
+
water_flow_survival_probability = (
|
159
|
+
self.flood_velocity.survival_probability if self.flood_velocity else 1
|
160
|
+
)
|
161
|
+
water_level_survivial_probability = (
|
162
|
+
self.flood_depth.survival_probability if self.flood_depth else 1
|
163
|
+
)
|
164
|
+
fire_survival_probability = (
|
165
|
+
self.fire_boundary_dist.survival_probability if self.fire_boundary_dist else 1
|
166
|
+
)
|
167
|
+
pgv_survival_probability = (
|
168
|
+
self.peak_ground_velocity.survival_probability if self.peak_ground_velocity else 1
|
169
|
+
)
|
170
|
+
pga_survival_probability = (
|
171
|
+
self.peak_ground_acceleration.survival_probability
|
172
|
+
if self.peak_ground_acceleration
|
173
|
+
else 1
|
174
|
+
)
|
175
|
+
|
176
|
+
return (
|
177
|
+
wind_survival_probability
|
178
|
+
* water_flow_survival_probability
|
179
|
+
* water_level_survivial_probability
|
180
|
+
* fire_survival_probability
|
181
|
+
* pgv_survival_probability
|
182
|
+
* pga_survival_probability
|
183
|
+
)
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def example(cls) -> "AssetState":
|
187
|
+
return AssetState(
|
188
|
+
timestamp=datetime.now(),
|
189
|
+
wind_speed=SpeedProbability.example(),
|
190
|
+
flood_velocity=SpeedProbability.example(),
|
191
|
+
flood_depth=DistanceProbability.example(),
|
192
|
+
fire_boundary_dist=DistanceProbability.example(),
|
193
|
+
peak_ground_velocity=SpeedProbability.example(),
|
194
|
+
peak_ground_acceleration=AccelerationProbability.example(),
|
195
|
+
)
|
196
|
+
|
197
|
+
|
198
|
+
class Asset(Component):
|
199
|
+
distribution_asset: UUID = Field(..., description="UUID of the distribution asset")
|
200
|
+
connections: list[UUID] = Field([], description="List of UUIDs of connected assets")
|
201
|
+
devices: list[UUID] = Field(
|
202
|
+
[], description="List of UUIDs of devices associated with the asset"
|
203
|
+
)
|
204
|
+
asset_type: AssetTypes
|
205
|
+
height: Distance = Field(..., gt=0, description="Height of the asset")
|
206
|
+
latitude: float = Field(..., ge=-90, le=90, description="Latitude in degrees")
|
207
|
+
longitude: float = Field(..., ge=-180, le=180, description="Longitude in degrees")
|
208
|
+
asset_state: list[AssetState]
|
209
|
+
|
210
|
+
@computed_field
|
211
|
+
@property
|
212
|
+
def elevation(self) -> Distance:
|
213
|
+
return Distance(get_elevation(lat=self.latitude, lon=self.longitude), "meter")
|
214
|
+
|
215
|
+
def _get_asset_state_at_timestamp(self, timestamp: datetime) -> AssetState | None:
|
216
|
+
for asset_state in self.asset_state:
|
217
|
+
if asset_state.timestamp == timestamp:
|
218
|
+
return asset_state
|
219
|
+
return AssetState(
|
220
|
+
timestamp=timestamp,
|
221
|
+
)
|
222
|
+
|
223
|
+
def update_survival_probability(
|
224
|
+
self, time_stamp: datetime, hazard_model: hz.BaseDisasterModel, frag_curves
|
225
|
+
):
|
226
|
+
asset_state = self._get_asset_state_at_timestamp(time_stamp)
|
227
|
+
asset_location = Point(self.longitude, self.latitude)
|
228
|
+
|
229
|
+
if isinstance(hazard_model, hz.EarthQuakeModel):
|
230
|
+
asset_state.calculate_earthquake_vectors(asset_location, hazard_model)
|
231
|
+
elif isinstance(hazard_model, hz.FireModel):
|
232
|
+
asset_state.calculate_fire_vectors(asset_location, hazard_model)
|
233
|
+
elif isinstance(hazard_model, hz.WindModel):
|
234
|
+
asset_state.calculate_wind_vectors(asset_location, hazard_model)
|
235
|
+
elif isinstance(hazard_model, hz.FloodModel):
|
236
|
+
asset_state.calculate_flood_vectors(
|
237
|
+
asset_location, hazard_model, self.elevation + self.height
|
238
|
+
)
|
239
|
+
else:
|
240
|
+
raise (f"Unsupported hazard type {hazard_model.__class__.__name__}")
|
241
|
+
|
242
|
+
self.calculate_probabilities(asset_state, frag_curves)
|
243
|
+
|
244
|
+
if asset_state not in self.asset_state:
|
245
|
+
self.asset_state.append(asset_state)
|
246
|
+
|
247
|
+
def calculate_probabilities(self, asset_state: AssetState, frag_curves):
|
248
|
+
fields = [
|
249
|
+
model_field
|
250
|
+
for model_field in list(asset_state.model_fields.keys())
|
251
|
+
if model_field not in ["uuid", "name", "timestamp"]
|
252
|
+
]
|
253
|
+
for field in fields:
|
254
|
+
prob_model = getattr(asset_state, field)
|
255
|
+
if prob_model and prob_model.survival_probability == 1:
|
256
|
+
curve = self.get_vadid_curve(frag_curves, field)
|
257
|
+
if curve is None:
|
258
|
+
raise Exception(f"No fragility curve found for field - {field}")
|
259
|
+
prob_inst = curve.prob_model
|
260
|
+
quantity_name = [
|
261
|
+
model_field
|
262
|
+
for model_field in list(prob_model.model_fields.keys())
|
263
|
+
if model_field not in ["uuid", "name", "survival_probability"]
|
264
|
+
][0]
|
265
|
+
quantity = getattr(prob_model, quantity_name)
|
266
|
+
prob_model.survival_probability = 1 - prob_inst.probability(quantity)
|
267
|
+
|
268
|
+
def get_vadid_curve(self, frag_curves, field: SyntaxError) -> float:
|
269
|
+
for frag_curve in frag_curves:
|
270
|
+
if frag_curve.asset_state_param == field:
|
271
|
+
for curve in frag_curve.curves:
|
272
|
+
if curve.asset_type == self.asset_type:
|
273
|
+
return curve.prob_function
|
274
|
+
|
275
|
+
return None
|
276
|
+
|
277
|
+
@classmethod
|
278
|
+
def example(cls) -> "Asset":
|
279
|
+
return Asset(
|
280
|
+
name="Asset 1",
|
281
|
+
asset_type=AssetTypes.distribution_poles,
|
282
|
+
distribution_asset=UUID("123e4567-e89b-12d3-a456-426614174000"),
|
283
|
+
height=Distance(100, "m"),
|
284
|
+
latitude=37.7749,
|
285
|
+
longitude=-122.4194,
|
286
|
+
asset_state=[AssetState.example()],
|
287
|
+
)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import Type, Callable
|
2
|
+
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
4
|
+
from infrasys import Component
|
5
|
+
|
6
|
+
from erad.enums import AssetTypes
|
7
|
+
|
8
|
+
|
9
|
+
class BaseEradScenarioModel(BaseModel):
|
10
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
11
|
+
|
12
|
+
|
13
|
+
class ComponentFilterModel(BaseEradScenarioModel):
|
14
|
+
component_type: Type[Component]
|
15
|
+
component_filter: Callable | None = None
|
16
|
+
|
17
|
+
|
18
|
+
class AssetComponentMap(BaseEradScenarioModel):
|
19
|
+
asset_type: AssetTypes
|
20
|
+
filters: list[ComponentFilterModel] = []
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from functools import cached_property
|
2
|
+
from typing import Literal
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
from infrasys import Component, BaseQuantity
|
7
|
+
from scipy.stats import _continuous_distns
|
8
|
+
from pydantic import field_validator
|
9
|
+
import plotly.express as px
|
10
|
+
import numpy as np
|
11
|
+
|
12
|
+
from erad.probability_builder import ProbabilityFunctionBuilder
|
13
|
+
from erad.models.asset import AssetState
|
14
|
+
from erad.enums import AssetTypes
|
15
|
+
from erad.quantities import Speed
|
16
|
+
|
17
|
+
|
18
|
+
FRAGILITY_CURVE_TYPES = [
|
19
|
+
fc for fc in AssetState.model_fields if fc not in ["name", "uuid", "timestamp"]
|
20
|
+
]
|
21
|
+
SUPPORTED_CONT_DIST = [name for name in dir(_continuous_distns) if not name.startswith("_")]
|
22
|
+
|
23
|
+
|
24
|
+
class ProbabilityFunction(Component):
|
25
|
+
name: str = ""
|
26
|
+
distribution: Literal[*SUPPORTED_CONT_DIST]
|
27
|
+
parameters: list[float | BaseQuantity]
|
28
|
+
|
29
|
+
@field_validator("parameters")
|
30
|
+
def validate_parameters(cls, value):
|
31
|
+
if not any(isinstance(v, BaseQuantity) for v in value):
|
32
|
+
raise ValueError("There should be atleast one BaseQuantity in the parameters")
|
33
|
+
|
34
|
+
units = set([v.units for v in value if isinstance(v, BaseQuantity)])
|
35
|
+
if not len(units) == 1:
|
36
|
+
raise ValueError("All BaseQuantities should have the same units")
|
37
|
+
return value
|
38
|
+
|
39
|
+
@cached_property
|
40
|
+
def prob_model(self) -> ProbabilityFunctionBuilder:
|
41
|
+
return ProbabilityFunctionBuilder(self.distribution, self.parameters)
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def example(cls) -> "ProbabilityFunction":
|
45
|
+
return ProbabilityFunction(
|
46
|
+
distribution="norm",
|
47
|
+
parameters=[Speed(1.5, "m/s"), 2],
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
class FragilityCurve(Component):
|
52
|
+
name: str = ""
|
53
|
+
asset_type: AssetTypes
|
54
|
+
prob_function: ProbabilityFunction
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def example(cls) -> "FragilityCurve":
|
58
|
+
return FragilityCurve(
|
59
|
+
asset_type=AssetTypes.substation,
|
60
|
+
prob_function=ProbabilityFunction.example(),
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
class HazardFragilityCurves(Component):
|
65
|
+
name: str = "DEFAULT_CURVES"
|
66
|
+
asset_state_param: Literal[*FRAGILITY_CURVE_TYPES]
|
67
|
+
curves: list[FragilityCurve]
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def example(cls) -> "HazardFragilityCurves":
|
71
|
+
return HazardFragilityCurves(
|
72
|
+
asset_state_param="peak_ground_acceleration",
|
73
|
+
curves=[FragilityCurve.example()],
|
74
|
+
)
|
75
|
+
|
76
|
+
def plot(
|
77
|
+
self, file_path: Path, x_min: float = 0, x_max: float = None, number_of_points: int = 100
|
78
|
+
):
|
79
|
+
"""Plot the fragility curves."""
|
80
|
+
file_path = Path(file_path)
|
81
|
+
assert file_path.suffix.lower() == ".html", "File path should be an HTML file"
|
82
|
+
|
83
|
+
if not self.curves:
|
84
|
+
raise ValueError("No curves to plot")
|
85
|
+
|
86
|
+
quantities = [
|
87
|
+
p for p in self.curves[0].prob_function.parameters if isinstance(p, BaseQuantity)
|
88
|
+
]
|
89
|
+
|
90
|
+
x_label = self.asset_state_param.replace("_", " ").title()
|
91
|
+
x = np.linspace(x_min, x_max, number_of_points)
|
92
|
+
|
93
|
+
plot_data = {"x": [], "y": [], "Asset Type": []}
|
94
|
+
|
95
|
+
for curve in self.curves:
|
96
|
+
model_class = quantities[0].__class__
|
97
|
+
units = quantities[0].units
|
98
|
+
prob_model = curve.prob_function.prob_model
|
99
|
+
y = prob_model.probability(model_class(x, units))
|
100
|
+
label = curve.asset_type.name.replace("_", " ").title()
|
101
|
+
|
102
|
+
plot_data["x"].extend(x)
|
103
|
+
plot_data["y"].extend(y)
|
104
|
+
plot_data["Asset Type"].extend([label] * len(x))
|
105
|
+
|
106
|
+
fig = px.line(
|
107
|
+
plot_data,
|
108
|
+
x="x",
|
109
|
+
y="y",
|
110
|
+
color="Asset Type",
|
111
|
+
labels={"x": f"{x_label} [{units}]", "y": "Probability of Failure"},
|
112
|
+
title=f"Fragility Curves for {x_label}",
|
113
|
+
)
|
114
|
+
|
115
|
+
fig.show()
|
116
|
+
fig.write_html(file_path)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
from erad.models.hazard.base_models import BaseDisasterModel
|
2
|
+
from erad.models.hazard.earthquake import EarthQuakeModel
|
3
|
+
from erad.models.hazard.wild_fire import FireModel, FireModelArea
|
4
|
+
from erad.models.hazard.wind import WindModel
|
5
|
+
from erad.models.hazard.flood import FloodModel, FloodModelArea
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import plotly.graph_objects as go
|
2
|
+
from pydantic import ConfigDict
|
3
|
+
from infrasys import Component
|
4
|
+
|
5
|
+
|
6
|
+
class BaseDisasterModel(Component):
|
7
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
8
|
+
|
9
|
+
def plot(
|
10
|
+
self, time_index: int, figure: go.Figure, map_obj: go.Scattergeo | go.Scattermap
|
11
|
+
) -> int:
|
12
|
+
raise NotImplementedError("This method should be implemented in the derived classes")
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
import requests
|
5
|
+
|
6
|
+
|
7
|
+
TEST_PATH = Path(__file__).parents[4] / "tests"
|
8
|
+
DATA_FOLDER_NAME = "data"
|
9
|
+
DATA_FOLDER = TEST_PATH / DATA_FOLDER_NAME
|
10
|
+
DB_FILENAME = "erad_data.sqlite"
|
11
|
+
ERAD_DB = TEST_PATH / DATA_FOLDER_NAME / DB_FILENAME
|
12
|
+
BUCKET_NAME = "erad_v2_dataset"
|
13
|
+
|
14
|
+
if not ERAD_DB.exists():
|
15
|
+
logger.info("Erad database not found. Downloading from Google Cloud.")
|
16
|
+
ERAD_DB.parent.mkdir(parents=True, exist_ok=True)
|
17
|
+
url = f"https://storage.googleapis.com/{BUCKET_NAME}/{DB_FILENAME}"
|
18
|
+
response = requests.get(url)
|
19
|
+
|
20
|
+
with open(ERAD_DB, "wb") as f:
|
21
|
+
f.write(response.content)
|
22
|
+
logger.info("Download complete...")
|
23
|
+
|
24
|
+
HISTROIC_EARTHQUAKE_TABLE = "historic_earthquakes"
|
25
|
+
HISTROIC_HURRICANE_TABLE = "historic_hurricanes"
|
26
|
+
HISTROIC_FIRE_TABLE = "historic_fires"
|