NREL-erad 0.0.0a0__py3-none-any.whl → 0.1.1__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 +80 -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 +300 -0
- erad/models/asset_mapping.py +20 -0
- erad/models/edit_store.py +22 -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 +76 -0
- erad/probability_builder.py +38 -0
- erad/quantities.py +31 -0
- erad/runner.py +125 -0
- erad/systems/__init__.py +2 -0
- erad/systems/asset_system.py +462 -0
- erad/systems/hazard_system.py +122 -0
- nrel_erad-0.1.1.dist-info/METADATA +61 -0
- nrel_erad-0.1.1.dist-info/RECORD +36 -0
- {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.1.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.1.dist-info/licenses}/LICENSE.txt +0 -0
- {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
from infrasys import BaseQuantity
|
2
|
+
import scipy.stats as stats
|
3
|
+
|
4
|
+
|
5
|
+
class ProbabilityFunctionBuilder:
|
6
|
+
"""Class containing utility fuctions for sceario definations."""
|
7
|
+
|
8
|
+
def __init__(self, dist, params: list[float | BaseQuantity]):
|
9
|
+
"""Constructor for BaseScenario class.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
dist (str): Name of teh distribution. Should follow Scipy naming convention
|
13
|
+
params (list): A list of parameters for the chosen distribution function. See Scipy.stats documentation
|
14
|
+
"""
|
15
|
+
base_quantity = [p for p in params if isinstance(p, BaseQuantity)][0]
|
16
|
+
self.quantity = base_quantity.__class__
|
17
|
+
self.units = base_quantity.units
|
18
|
+
self.dist = getattr(stats, dist)
|
19
|
+
self.params = [p.magnitude if isinstance(p, BaseQuantity) else p for p in params]
|
20
|
+
return
|
21
|
+
|
22
|
+
def sample(self):
|
23
|
+
"""Sample the distribution"""
|
24
|
+
return self.quantity(self.dist.rvs(*self.params, size=1)[0], self.units)
|
25
|
+
|
26
|
+
def probability(self, value: BaseQuantity) -> float:
|
27
|
+
"""Calculates survival probability of a given asset.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
value (float): value for vetor of interest. Will change with scenarions
|
31
|
+
"""
|
32
|
+
assert isinstance(value, BaseQuantity), "Value must be a BaseQuantity"
|
33
|
+
|
34
|
+
cdf = self.dist.cdf
|
35
|
+
try:
|
36
|
+
return cdf(value.to(self.units).magnitude, *self.params)
|
37
|
+
except Exception:
|
38
|
+
return cdf(value, *self.params)
|
erad/quantities.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from infrasys.base_quantity import BaseQuantity
|
2
|
+
|
3
|
+
|
4
|
+
class Speed(BaseQuantity):
|
5
|
+
"""Quantity representing speed."""
|
6
|
+
|
7
|
+
__base_unit__ = "m/second"
|
8
|
+
|
9
|
+
|
10
|
+
class Acceleration(BaseQuantity):
|
11
|
+
"""Quantity representing acceleration."""
|
12
|
+
|
13
|
+
__base_unit__ = "m/second**2"
|
14
|
+
|
15
|
+
|
16
|
+
class Temperature(BaseQuantity):
|
17
|
+
"""Quantity representing temperature."""
|
18
|
+
|
19
|
+
__base_unit__ = "degC"
|
20
|
+
|
21
|
+
|
22
|
+
class Pressure(BaseQuantity):
|
23
|
+
"""Quantity representing pressure."""
|
24
|
+
|
25
|
+
__base_unit__ = "millibar"
|
26
|
+
|
27
|
+
|
28
|
+
class Flow(BaseQuantity):
|
29
|
+
"""Quantity representing flow."""
|
30
|
+
|
31
|
+
__base_unit__ = "feet**3/second"
|
erad/runner.py
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from gdm.distribution import DistributionSystem
|
4
|
+
from loguru import logger
|
5
|
+
import numpy as np
|
6
|
+
|
7
|
+
from gdm.tracked_changes import (
|
8
|
+
TrackedChange,
|
9
|
+
PropertyEdit,
|
10
|
+
)
|
11
|
+
|
12
|
+
from erad.default_fragility_curves import DEFAULT_FRAGILTY_CURVES
|
13
|
+
from erad.models.fragility_curve import HazardFragilityCurves
|
14
|
+
from erad.systems.hazard_system import HazardSystem
|
15
|
+
from erad.systems.asset_system import AssetSystem
|
16
|
+
from erad.constants import HAZARD_TYPES
|
17
|
+
from erad.models.asset import Asset
|
18
|
+
|
19
|
+
|
20
|
+
class HarzardSimulator:
|
21
|
+
def __init__(self, asset_system: AssetSystem):
|
22
|
+
self._asset_system = asset_system
|
23
|
+
self._asset_system.auto_add_composed_components = True
|
24
|
+
self.assets: list[Asset] = list(asset_system.get_components(Asset))
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def from_gdm(cls, dist_system: DistributionSystem) -> "HarzardSimulator":
|
28
|
+
"""Create a HarzardSimulator from a DistributionSystem."""
|
29
|
+
asset_system = AssetSystem.from_gdm(dist_system)
|
30
|
+
return cls(asset_system)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def asset_system(self) -> AssetSystem:
|
34
|
+
"""Get the AssetSystem."""
|
35
|
+
return self._asset_system
|
36
|
+
|
37
|
+
def _get_time_stamps(self) -> list[datetime]:
|
38
|
+
timestamps = []
|
39
|
+
for model_type in HAZARD_TYPES:
|
40
|
+
for model in self.hazard_system.get_components(model_type):
|
41
|
+
timestamps.append(model.timestamp)
|
42
|
+
return sorted(timestamps)
|
43
|
+
|
44
|
+
def run(self, hazard_system: HazardSystem, curve_set: str = "DEFAULT_CURVES"):
|
45
|
+
probability_models = list(
|
46
|
+
hazard_system.get_components(
|
47
|
+
HazardFragilityCurves, filter_func=lambda x: x.name == curve_set
|
48
|
+
)
|
49
|
+
)
|
50
|
+
|
51
|
+
if not probability_models:
|
52
|
+
logger.warning(
|
53
|
+
"No HazardFragilityCurves definations found in the passed HazardSystem using default curve definations"
|
54
|
+
)
|
55
|
+
probability_models = DEFAULT_FRAGILTY_CURVES
|
56
|
+
|
57
|
+
self.hazard_system = hazard_system
|
58
|
+
self.timestamps = self._get_time_stamps()
|
59
|
+
for timestamp in self.timestamps:
|
60
|
+
for hazard_type in HAZARD_TYPES:
|
61
|
+
for hazard_model in self.hazard_system.get_components(
|
62
|
+
hazard_type, filter_func=lambda x: x.timestamp == timestamp
|
63
|
+
):
|
64
|
+
for asset in self.assets:
|
65
|
+
assset_state = asset.update_survival_probability(
|
66
|
+
timestamp, hazard_model, probability_models
|
67
|
+
)
|
68
|
+
if not self._asset_system.has_component(assset_state):
|
69
|
+
self._asset_system.add_component(assset_state)
|
70
|
+
|
71
|
+
|
72
|
+
class HazardScenarioGenerator:
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
asset_system: AssetSystem,
|
76
|
+
hazard_system: HazardSystem,
|
77
|
+
curve_set: str = "DEFAULT_CURVES",
|
78
|
+
):
|
79
|
+
self.assets = list(asset_system.get_components(Asset))
|
80
|
+
self.harzard_simulator = HarzardSimulator(asset_system)
|
81
|
+
self.harzard_simulator.run(hazard_system, curve_set)
|
82
|
+
|
83
|
+
def _sample(self, scenario_name: str) -> list[TrackedChange]:
|
84
|
+
outaged_assets = []
|
85
|
+
tracked_changes = []
|
86
|
+
|
87
|
+
n_assets = len(self.assets)
|
88
|
+
n_timestamps = len(self.assets[0].asset_state)
|
89
|
+
|
90
|
+
ramdom_samples = np.random.random((n_assets, n_timestamps))
|
91
|
+
|
92
|
+
for ii, asset in enumerate(self.assets):
|
93
|
+
for jj, state in enumerate(
|
94
|
+
sorted(asset.asset_state, key=lambda asset_state: asset_state.timestamp)
|
95
|
+
):
|
96
|
+
if (
|
97
|
+
ramdom_samples[ii, jj] > state.survival_probability
|
98
|
+
and asset.name not in outaged_assets
|
99
|
+
):
|
100
|
+
tracked_changes.append(
|
101
|
+
TrackedChange(
|
102
|
+
scenario_name=scenario_name,
|
103
|
+
timestamp=state.timestamp,
|
104
|
+
edits=[
|
105
|
+
PropertyEdit(
|
106
|
+
component_uuid=asset.distribution_asset,
|
107
|
+
name="in_service",
|
108
|
+
value=False,
|
109
|
+
)
|
110
|
+
],
|
111
|
+
),
|
112
|
+
)
|
113
|
+
outaged_assets.append(asset.name)
|
114
|
+
|
115
|
+
return tracked_changes
|
116
|
+
|
117
|
+
def samples(self, number_of_samples: int = 1, seed: int = 0) -> list[TrackedChange]:
|
118
|
+
if number_of_samples < 1:
|
119
|
+
raise ValueError("number_of_samples should be a positive integer")
|
120
|
+
np.random.seed(seed)
|
121
|
+
tracked_changes = []
|
122
|
+
for i in range(number_of_samples):
|
123
|
+
scenario_name = f"sample_{i}"
|
124
|
+
tracked_changes.extend(self._sample(scenario_name))
|
125
|
+
return tracked_changes
|
erad/systems/__init__.py
ADDED
@@ -0,0 +1,462 @@
|
|
1
|
+
from collections import defaultdict, Counter
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from gdm.distribution.enums import PlotingStyle, MapType
|
5
|
+
from gdm.distribution import DistributionSystem
|
6
|
+
from shapely.geometry import Point, LineString
|
7
|
+
import gdm.distribution.components as gdc
|
8
|
+
from gdm.quantities import Distance
|
9
|
+
import plotly.graph_objects as go
|
10
|
+
from infrasys import System
|
11
|
+
from loguru import logger
|
12
|
+
import geopandas as gpd
|
13
|
+
import networkx as nx
|
14
|
+
import pandas as pd
|
15
|
+
import numpy as np
|
16
|
+
import elevation
|
17
|
+
|
18
|
+
|
19
|
+
from erad.constants import ASSET_TYPES, DEFAULT_TIME_STAMP, RASTER_DOWNLOAD_PATH, DEFAULT_HEIGHTS_M
|
20
|
+
from erad.gdm_mapping import asset_to_gdm_mapping
|
21
|
+
from erad.models.asset import Asset, AssetState
|
22
|
+
from erad.enums import AssetTypes, NodeTypes
|
23
|
+
|
24
|
+
|
25
|
+
class AssetSystem(System):
|
26
|
+
def __init__(self, *args, **kwargs):
|
27
|
+
super().__init__(*args, **kwargs)
|
28
|
+
|
29
|
+
def add_component(self, component, **kwargs):
|
30
|
+
assert isinstance(
|
31
|
+
component, ASSET_TYPES
|
32
|
+
), f"Unsupported model type {component.__class__.__name__}"
|
33
|
+
return super().add_component(component, **kwargs)
|
34
|
+
|
35
|
+
def add_components(self, *components, **kwargs):
|
36
|
+
assert all(isinstance(component, ASSET_TYPES) for component in components), (
|
37
|
+
"Unsupported model types in passed component. Valid types are: \n"
|
38
|
+
+ "\n".join([s.__name__ for s in ASSET_TYPES])
|
39
|
+
)
|
40
|
+
return super().add_components(*components, **kwargs)
|
41
|
+
|
42
|
+
def get_dircted_graph(self):
|
43
|
+
"""Get the directed graph of the AssetSystem."""
|
44
|
+
|
45
|
+
graph = self.get_undirected_graph()
|
46
|
+
substations = list(
|
47
|
+
self.get_components(Asset, filter_func=lambda x: x.asset_type == AssetTypes.substation)
|
48
|
+
)
|
49
|
+
|
50
|
+
assert len(substations) <= 1, "There should be at most one substation in the asset system."
|
51
|
+
|
52
|
+
tree = nx.dfs_tree(graph, str(substations[0].connections[0]))
|
53
|
+
return tree
|
54
|
+
|
55
|
+
def get_undirected_graph(self):
|
56
|
+
"""Get the undirected graph of the AssetSystem."""
|
57
|
+
g = nx.Graph()
|
58
|
+
graph_data = []
|
59
|
+
for asset in self.get_components(Asset):
|
60
|
+
if len(asset.connections) == 2:
|
61
|
+
u, v = asset.connections
|
62
|
+
g.add_edge(str(u), str(v), **asset.model_dump())
|
63
|
+
else:
|
64
|
+
if asset.asset_type in NodeTypes:
|
65
|
+
g.add_node(str(asset.distribution_asset), **asset.model_dump())
|
66
|
+
else:
|
67
|
+
graph_data.append(asset.model_dump())
|
68
|
+
|
69
|
+
g.graph["metadata"] = graph_data
|
70
|
+
return g
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def from_gdm(cls, dist_system: DistributionSystem) -> "AssetSystem":
|
74
|
+
"""Create a AssetSystem from a DistributionSystem."""
|
75
|
+
asset_map = AssetSystem.map_asets(dist_system)
|
76
|
+
# list_of_assets = AssetSystem._build_assets(asset_map)
|
77
|
+
system = AssetSystem(auto_add_composed_components=True)
|
78
|
+
list_of_assets = system._build_assets(asset_map)
|
79
|
+
system.add_components(*list_of_assets)
|
80
|
+
return system
|
81
|
+
|
82
|
+
def _add_node_data(
|
83
|
+
self, node_data: dict[str, list], asset: Asset, asset_state: AssetState | None = None
|
84
|
+
):
|
85
|
+
node_data["name"].append(asset.name)
|
86
|
+
node_data["type"].append(asset.asset_type.name)
|
87
|
+
node_data["height"].append(asset.height.to("meter").magnitude)
|
88
|
+
node_data["elevation"].append(asset.elevation.to("meter").magnitude)
|
89
|
+
node_data["latitude"].append(asset.latitude)
|
90
|
+
node_data["longitude"].append(asset.longitude)
|
91
|
+
node_data["timestamp"].append(asset_state.timestamp if asset_state else DEFAULT_TIME_STAMP)
|
92
|
+
node_data["survival_prob"].append(asset_state.survival_probability if asset_state else 1.0)
|
93
|
+
node_data["wind_speed"].append(asset_state.wind_speed if asset_state else None)
|
94
|
+
node_data["fire_boundary_dist"].append(
|
95
|
+
asset_state.fire_boundary_dist if asset_state else None
|
96
|
+
)
|
97
|
+
node_data["flood_depth"].append(asset_state.flood_depth if asset_state else None)
|
98
|
+
node_data["flood_velocity"].append(asset_state.flood_velocity if asset_state else None)
|
99
|
+
node_data["peak_ground_acceleration"].append(
|
100
|
+
asset_state.peak_ground_acceleration if asset_state else None
|
101
|
+
)
|
102
|
+
node_data["peak_ground_velocity"].append(
|
103
|
+
asset_state.peak_ground_velocity if asset_state else None
|
104
|
+
)
|
105
|
+
return node_data
|
106
|
+
|
107
|
+
def _add_edge_data(
|
108
|
+
self, edge_data: dict[str, list], asset: Asset, asset_state: AssetState | None = None
|
109
|
+
):
|
110
|
+
u, v = set(asset.connections)
|
111
|
+
buses = list(
|
112
|
+
self.get_components(Asset, filter_func=lambda x: x.distribution_asset in [u, v])
|
113
|
+
)
|
114
|
+
edge_data["name"].append(asset.name)
|
115
|
+
edge_data["type"].append(asset.asset_type.name)
|
116
|
+
edge_data["height"].append(asset.height.to("meter").magnitude)
|
117
|
+
edge_data["elevation"].append(asset.elevation.to("meter").magnitude)
|
118
|
+
edge_data["latitude"].append([b.latitude for b in buses])
|
119
|
+
edge_data["longitude"].append([b.longitude for b in buses])
|
120
|
+
edge_data["timestamp"].append(asset_state.timestamp if asset_state else DEFAULT_TIME_STAMP)
|
121
|
+
edge_data["survival_prob"].append(asset_state.survival_probability if asset_state else 1.0)
|
122
|
+
edge_data["wind_speed"].append(asset_state.wind_speed if asset_state else None)
|
123
|
+
edge_data["fire_boundary_dist"].append(
|
124
|
+
asset_state.fire_boundary_dist if asset_state else None
|
125
|
+
)
|
126
|
+
edge_data["flood_depth"].append(asset_state.flood_depth if asset_state else None)
|
127
|
+
edge_data["flood_velocity"].append(asset_state.flood_velocity if asset_state else None)
|
128
|
+
edge_data["peak_ground_acceleration"].append(
|
129
|
+
asset_state.peak_ground_acceleration if asset_state else None
|
130
|
+
)
|
131
|
+
edge_data["peak_ground_velocity"].append(
|
132
|
+
asset_state.peak_ground_velocity if asset_state else None
|
133
|
+
)
|
134
|
+
return edge_data
|
135
|
+
|
136
|
+
def to_gdf(self):
|
137
|
+
node_data = defaultdict(list)
|
138
|
+
edge_data = defaultdict(list)
|
139
|
+
assets: list[Asset] = self.get_components(Asset)
|
140
|
+
|
141
|
+
for asset in assets:
|
142
|
+
if len(set(asset.connections)) < 2:
|
143
|
+
if asset.asset_state:
|
144
|
+
for asset_state in asset.asset_state:
|
145
|
+
node_data = self._add_node_data(node_data, asset, asset_state)
|
146
|
+
else:
|
147
|
+
node_data = self._add_node_data(node_data, asset, None)
|
148
|
+
else:
|
149
|
+
if asset.asset_state:
|
150
|
+
for asset_state in asset.asset_state:
|
151
|
+
edge_data = self._add_edge_data(edge_data, asset, asset_state)
|
152
|
+
else:
|
153
|
+
edge_data = self._add_edge_data(edge_data, asset, None)
|
154
|
+
|
155
|
+
nodes_df = pd.DataFrame(node_data)
|
156
|
+
gdf_nodes = gpd.GeoDataFrame(
|
157
|
+
nodes_df,
|
158
|
+
geometry=gpd.points_from_xy(nodes_df.longitude, nodes_df.latitude),
|
159
|
+
crs="EPSG:4326",
|
160
|
+
)
|
161
|
+
edge_df = pd.DataFrame(edge_data)
|
162
|
+
edge_df = edge_df[edge_df["longitude"].apply(lambda x: len(x) == 2)]
|
163
|
+
geometry = [
|
164
|
+
LineString([Point(xy) for xy in zip(*xys)])
|
165
|
+
for xys in zip(edge_df["longitude"], edge_df["latitude"])
|
166
|
+
]
|
167
|
+
gdf_edges = gpd.GeoDataFrame(edge_df, geometry=geometry, crs="EPSG:4326")
|
168
|
+
complete_gdf = pd.concat([gdf_nodes, gdf_edges])
|
169
|
+
return complete_gdf
|
170
|
+
|
171
|
+
def to_geojson(self) -> str:
|
172
|
+
"""Create a GeoJSON from an AssetSystem."""
|
173
|
+
gdf_nodes = self.to_gdf()
|
174
|
+
return gdf_nodes.to_json()
|
175
|
+
|
176
|
+
def _prepopulate_bus_assets(
|
177
|
+
self,
|
178
|
+
asset_map: dict[AssetTypes : list[gdc.DistributionComponentBase]],
|
179
|
+
list_of_assets: dict[str, Asset],
|
180
|
+
):
|
181
|
+
for asset_type, components in asset_map.items():
|
182
|
+
for component in components:
|
183
|
+
lat, long = AssetSystem._get_component_coordinate(component)
|
184
|
+
if isinstance(component, gdc.DistributionBus):
|
185
|
+
list_of_assets[str(component.uuid)] = Asset(
|
186
|
+
name=component.name,
|
187
|
+
connections=[],
|
188
|
+
asset_type=asset_type,
|
189
|
+
distribution_asset=component.uuid,
|
190
|
+
height=Distance(DEFAULT_HEIGHTS_M[asset_type], "meter"),
|
191
|
+
latitude=lat,
|
192
|
+
longitude=long,
|
193
|
+
asset_state=[],
|
194
|
+
)
|
195
|
+
return list_of_assets
|
196
|
+
|
197
|
+
def _build_assets(
|
198
|
+
self,
|
199
|
+
asset_map: dict[AssetTypes : list[gdc.DistributionComponentBase]],
|
200
|
+
) -> list[Asset]:
|
201
|
+
list_of_assets: dict[str, Asset] = {}
|
202
|
+
|
203
|
+
self._prepopulate_bus_assets(asset_map, list_of_assets)
|
204
|
+
|
205
|
+
for asset_type, components in asset_map.items():
|
206
|
+
for component in components:
|
207
|
+
if not isinstance(component, gdc.DistributionBus):
|
208
|
+
lat, long = AssetSystem._get_component_coordinate(component)
|
209
|
+
if hasattr(component, "buses"):
|
210
|
+
connections = [c.uuid for c in component.buses]
|
211
|
+
elif hasattr(component, "bus"):
|
212
|
+
connections = [component.bus.uuid]
|
213
|
+
else:
|
214
|
+
connections = []
|
215
|
+
list_of_assets[str(component.uuid)] = Asset(
|
216
|
+
name=component.name,
|
217
|
+
connections=connections,
|
218
|
+
asset_type=asset_type,
|
219
|
+
distribution_asset=component.uuid,
|
220
|
+
height=Distance(DEFAULT_HEIGHTS_M[asset_type], "meter"),
|
221
|
+
latitude=lat,
|
222
|
+
longitude=long,
|
223
|
+
asset_state=[],
|
224
|
+
)
|
225
|
+
|
226
|
+
if len(connections) == 1:
|
227
|
+
if str(connections[0]) in list_of_assets:
|
228
|
+
asset = list_of_assets[str(connections[0])]
|
229
|
+
asset.devices.append(component.uuid)
|
230
|
+
|
231
|
+
return list_of_assets.values()
|
232
|
+
|
233
|
+
@staticmethod
|
234
|
+
def _get_component_coordinate(component: gdc.DistributionComponentBase):
|
235
|
+
if hasattr(component, "buses"):
|
236
|
+
xs = [bus.coordinate.x for bus in component.buses]
|
237
|
+
ys = [bus.coordinate.y for bus in component.buses]
|
238
|
+
if 0 not in xs and 0 not in ys:
|
239
|
+
return (sum(xs) / len(xs), sum(ys) / len(ys))
|
240
|
+
else:
|
241
|
+
return (0, 0)
|
242
|
+
elif hasattr(component, "bus"):
|
243
|
+
return (component.bus.coordinate.x, component.bus.coordinate.y)
|
244
|
+
elif isinstance(component, gdc.DistributionBus):
|
245
|
+
return (component.coordinate.x, component.coordinate.y)
|
246
|
+
|
247
|
+
@staticmethod
|
248
|
+
def map_asets(
|
249
|
+
dist_system: DistributionSystem,
|
250
|
+
) -> dict[AssetTypes : list[gdc.DistributionComponentBase]]:
|
251
|
+
asset_dict = defaultdict(list)
|
252
|
+
for asset, filters in asset_to_gdm_mapping.items():
|
253
|
+
for filter_info in filters:
|
254
|
+
models = dist_system.get_components(
|
255
|
+
filter_info.component_type, filter_func=filter_info.component_filter
|
256
|
+
)
|
257
|
+
asset_dict[asset].extend(list(models))
|
258
|
+
|
259
|
+
AssetSystem._maps_buses(asset_dict, dist_system)
|
260
|
+
AssetSystem._map_transformers(asset_dict, dist_system)
|
261
|
+
return asset_dict
|
262
|
+
|
263
|
+
@staticmethod
|
264
|
+
def _maps_buses(
|
265
|
+
asset_dict: dict[AssetTypes : list[gdc.DistributionComponentBase]],
|
266
|
+
dist_system: DistributionSystem,
|
267
|
+
):
|
268
|
+
for bus in dist_system.get_components(gdc.DistributionBus):
|
269
|
+
asset_type = AssetSystem._get_bus_type(bus, asset_dict, dist_system)
|
270
|
+
if asset_type:
|
271
|
+
asset_dict[asset_type].append(bus)
|
272
|
+
|
273
|
+
@staticmethod
|
274
|
+
def _map_transformers(
|
275
|
+
asset_dict: dict[AssetTypes : list[gdc.DistributionComponentBase]],
|
276
|
+
dist_system: DistributionSystem,
|
277
|
+
):
|
278
|
+
for transformer in dist_system.get_components(gdc.DistributionTransformerBase):
|
279
|
+
bus_types = [
|
280
|
+
AssetSystem._get_bus_type(b, asset_dict, dist_system) for b in transformer.buses
|
281
|
+
]
|
282
|
+
if (
|
283
|
+
AssetTypes.transmission_junction_box in bus_types
|
284
|
+
or AssetTypes.transmission_tower in bus_types
|
285
|
+
):
|
286
|
+
asset_dict[AssetTypes.transformer_mad_mount].append(transformer)
|
287
|
+
elif all(bus_type == AssetTypes.distribution_junction_box for bus_type in bus_types):
|
288
|
+
asset_dict[AssetTypes.transformer_mad_mount].append(transformer)
|
289
|
+
else:
|
290
|
+
asset_dict[AssetTypes.transformer_mad_mount].append(transformer)
|
291
|
+
|
292
|
+
@staticmethod
|
293
|
+
def _get_bus_type(
|
294
|
+
bus: gdc.DistributionBus,
|
295
|
+
asset_dict: dict[AssetTypes : list[gdc.DistributionComponentBase]],
|
296
|
+
dist_system: DistributionSystem,
|
297
|
+
):
|
298
|
+
components = dist_system.get_bus_connected_components(bus.name, gdc.DistributionBranchBase)
|
299
|
+
connected_types = []
|
300
|
+
for component in components:
|
301
|
+
if component in asset_dict[AssetTypes.distribution_overhead_lines]:
|
302
|
+
connected_types.append(AssetTypes.distribution_poles)
|
303
|
+
elif component in asset_dict[AssetTypes.transmission_overhead_lines]:
|
304
|
+
connected_types.append(AssetTypes.transmission_tower)
|
305
|
+
elif component in asset_dict[AssetTypes.transmission_underground_cables]:
|
306
|
+
connected_types.append(AssetTypes.transmission_junction_box)
|
307
|
+
elif component in asset_dict[AssetTypes.distribution_underground_cables]:
|
308
|
+
connected_types.append(AssetTypes.distribution_junction_box)
|
309
|
+
else:
|
310
|
+
...
|
311
|
+
if connected_types:
|
312
|
+
counter = Counter(connected_types)
|
313
|
+
return counter.most_common(1)[0][0]
|
314
|
+
|
315
|
+
def _add_node_traces(
|
316
|
+
self,
|
317
|
+
time_index: int,
|
318
|
+
fig: go.Figure,
|
319
|
+
df_ts: gpd.GeoDataFrame,
|
320
|
+
plotting_object: go.Scattermap | go.Scattergeo,
|
321
|
+
):
|
322
|
+
points_only = df_ts[df_ts.geometry.geom_type == "Point"]
|
323
|
+
for asset_type in set(points_only["type"]):
|
324
|
+
df_filt = points_only[points_only["type"] == asset_type]
|
325
|
+
text = [
|
326
|
+
"<br>".join([f"<b>{kk}:</b> {vv}" for kk, vv in rr.to_dict().items()][:-1])
|
327
|
+
for __, rr in df_filt.iterrows()
|
328
|
+
]
|
329
|
+
trace = plotting_object(
|
330
|
+
lat=df_filt.geometry.y,
|
331
|
+
lon=df_filt.geometry.x,
|
332
|
+
mode="markers",
|
333
|
+
marker=dict(size=10),
|
334
|
+
name=asset_type,
|
335
|
+
hovertext=text,
|
336
|
+
visible=(time_index == 0),
|
337
|
+
)
|
338
|
+
fig.add_trace(trace)
|
339
|
+
return fig
|
340
|
+
|
341
|
+
def _add_edge_traces(
|
342
|
+
self,
|
343
|
+
time_index: int,
|
344
|
+
fig: go.Figure,
|
345
|
+
df_ts: gpd.GeoDataFrame,
|
346
|
+
plotting_object: go.Scattermap | go.Scattergeo,
|
347
|
+
):
|
348
|
+
points_only = df_ts[df_ts.geometry.geom_type == "LineString"]
|
349
|
+
for asset_type in set(points_only["type"]):
|
350
|
+
df_filt = points_only[points_only["type"] == asset_type]
|
351
|
+
features = {k: [] for k in list(df_filt.columns) + ["text"]}
|
352
|
+
for _, row in df_filt.iterrows():
|
353
|
+
for c in df_filt.columns:
|
354
|
+
features[c] = np.append(features[c], row[c])
|
355
|
+
features["text"] = np.append(
|
356
|
+
features["text"],
|
357
|
+
"<br> ".join([f"<b>{kk}:</b> {vv}" for kk, vv in row.to_dict().items()][:-1]),
|
358
|
+
)
|
359
|
+
for c in df_filt.columns:
|
360
|
+
features[c] = np.append(features[c], None)
|
361
|
+
features["text"] = np.append(features["text"], None)
|
362
|
+
|
363
|
+
trace = plotting_object(
|
364
|
+
name=asset_type,
|
365
|
+
lat=features["latitude"],
|
366
|
+
lon=features["longitude"],
|
367
|
+
mode="lines",
|
368
|
+
hovertext=features["text"],
|
369
|
+
visible=(time_index == 0),
|
370
|
+
)
|
371
|
+
fig.add_trace(trace)
|
372
|
+
return fig
|
373
|
+
|
374
|
+
def _has_zero_zero_coords(self, geom):
|
375
|
+
if geom.is_empty or geom is None:
|
376
|
+
return False
|
377
|
+
if geom.geom_type == "Point":
|
378
|
+
return geom.x == 0 and geom.y == 0
|
379
|
+
elif geom.geom_type in ["Polygon", "MultiPolygon"]:
|
380
|
+
return any(x == 0 and y == 0 for x, y in geom.exterior.coords)
|
381
|
+
elif geom.geom_type == "LineString":
|
382
|
+
return any(x == 0 and y == 0 for x, y in geom.coords)
|
383
|
+
elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection":
|
384
|
+
return any(self._has_zero_zero_coords(g) for g in geom.geoms)
|
385
|
+
return False
|
386
|
+
|
387
|
+
def get_elevation_raster(self) -> Path:
|
388
|
+
"""Download and clip elevation raster for the AssetSystem."""
|
389
|
+
if RASTER_DOWNLOAD_PATH.exists():
|
390
|
+
RASTER_DOWNLOAD_PATH.unlink()
|
391
|
+
|
392
|
+
coordinates = np.array(
|
393
|
+
[
|
394
|
+
(asset.latitude, asset.longitude)
|
395
|
+
for asset in self.get_components(Asset)
|
396
|
+
if asset.latitude and asset.latitude
|
397
|
+
]
|
398
|
+
)
|
399
|
+
|
400
|
+
if len(coordinates):
|
401
|
+
lat_min, lat_max = float(coordinates[:, 0].min()), float(coordinates[:, 0].max())
|
402
|
+
lon_min, lon_max = float(coordinates[:, 1].min()), float(coordinates[:, 1].max())
|
403
|
+
bounds = (lon_min, lat_min, lon_max, lat_max)
|
404
|
+
logger.info(f"Downloading raster file to path: {RASTER_DOWNLOAD_PATH}")
|
405
|
+
logger.info(f"Clipping raster for bounds: {bounds}")
|
406
|
+
elevation.clip(bounds=bounds, output=str(RASTER_DOWNLOAD_PATH.name))
|
407
|
+
if not RASTER_DOWNLOAD_PATH.exists():
|
408
|
+
raise FileNotFoundError(f"File path {RASTER_DOWNLOAD_PATH} does not exist")
|
409
|
+
else:
|
410
|
+
logger.info(
|
411
|
+
"No assets found in the AssetSystem, no elevation raster will be downloaded."
|
412
|
+
)
|
413
|
+
return None
|
414
|
+
|
415
|
+
def plot(
|
416
|
+
self,
|
417
|
+
show: bool = True,
|
418
|
+
show_legend: bool = True,
|
419
|
+
map_type: MapType = MapType.SCATTER_MAP,
|
420
|
+
style: PlotingStyle = PlotingStyle.CARTO_POSITRON,
|
421
|
+
zoom_level: int = 11,
|
422
|
+
figure=go.Figure(),
|
423
|
+
):
|
424
|
+
"""Plot the AssetSystem."""
|
425
|
+
plotting_object = getattr(go, map_type.value)
|
426
|
+
gdf = self.to_gdf()
|
427
|
+
gdf["timestamp"] = pd.to_datetime(gdf["timestamp"])
|
428
|
+
gdf = gdf[~gdf.geometry.apply(self._has_zero_zero_coords)]
|
429
|
+
timestamps = sorted(gdf["timestamp"].unique())
|
430
|
+
|
431
|
+
steps = []
|
432
|
+
|
433
|
+
for i, ts in enumerate(timestamps):
|
434
|
+
df_ts = gdf[gdf["timestamp"] == ts]
|
435
|
+
|
436
|
+
figure = self._add_node_traces(i, figure, df_ts, plotting_object)
|
437
|
+
figure = self._add_edge_traces(i, figure, df_ts, plotting_object)
|
438
|
+
|
439
|
+
steps.append(
|
440
|
+
dict(
|
441
|
+
method="update",
|
442
|
+
label=str(ts.date()),
|
443
|
+
args=[{"visible": [j == i for j in range(len(timestamps))]}],
|
444
|
+
)
|
445
|
+
)
|
446
|
+
|
447
|
+
sliders = [dict(active=0, pad={"t": 50}, steps=steps)]
|
448
|
+
|
449
|
+
figure.update_layout(
|
450
|
+
mapbox=dict(
|
451
|
+
style=style.value,
|
452
|
+
zoom=zoom_level,
|
453
|
+
# center=dict(lat=gdf.geometry.y.mean(), lon=gdf.geometry.x.mean()),
|
454
|
+
),
|
455
|
+
sliders=sliders,
|
456
|
+
showlegend=True if show_legend else False,
|
457
|
+
)
|
458
|
+
|
459
|
+
if show:
|
460
|
+
figure.show()
|
461
|
+
|
462
|
+
return figure
|