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.
Files changed (73) hide show
  1. erad/__init__.py +1 -0
  2. erad/constants.py +80 -11
  3. erad/default_fragility_curves/__init__.py +15 -0
  4. erad/default_fragility_curves/default_fire_boundary_dist.py +94 -0
  5. erad/default_fragility_curves/default_flood_depth.py +108 -0
  6. erad/default_fragility_curves/default_flood_velocity.py +101 -0
  7. erad/default_fragility_curves/default_fragility_curves.py +23 -0
  8. erad/default_fragility_curves/default_peak_ground_acceleration.py +163 -0
  9. erad/default_fragility_curves/default_peak_ground_velocity.py +94 -0
  10. erad/default_fragility_curves/default_wind_speed.py +94 -0
  11. erad/enums.py +40 -0
  12. erad/gdm_mapping.py +83 -0
  13. erad/models/__init__.py +1 -0
  14. erad/models/asset.py +300 -0
  15. erad/models/asset_mapping.py +20 -0
  16. erad/models/edit_store.py +22 -0
  17. erad/models/fragility_curve.py +116 -0
  18. erad/models/hazard/__init__.py +5 -0
  19. erad/models/hazard/base_models.py +12 -0
  20. erad/models/hazard/common.py +26 -0
  21. erad/models/hazard/earthquake.py +93 -0
  22. erad/models/hazard/flood.py +83 -0
  23. erad/models/hazard/wild_fire.py +121 -0
  24. erad/models/hazard/wind.py +143 -0
  25. erad/models/probability.py +76 -0
  26. erad/probability_builder.py +38 -0
  27. erad/quantities.py +31 -0
  28. erad/runner.py +125 -0
  29. erad/systems/__init__.py +2 -0
  30. erad/systems/asset_system.py +462 -0
  31. erad/systems/hazard_system.py +122 -0
  32. nrel_erad-0.1.1.dist-info/METADATA +61 -0
  33. nrel_erad-0.1.1.dist-info/RECORD +36 -0
  34. {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.1.dist-info}/WHEEL +1 -1
  35. NREL_erad-0.0.0a0.dist-info/METADATA +0 -61
  36. NREL_erad-0.0.0a0.dist-info/RECORD +0 -42
  37. erad/cypher_queries/load_data_v1.cypher +0 -212
  38. erad/data/World_Earthquakes_1960_2016.csv +0 -23410
  39. erad/db/__init__.py +0 -0
  40. erad/db/assets/__init__.py +0 -0
  41. erad/db/assets/critical_infras.py +0 -171
  42. erad/db/assets/distribution_lines.py +0 -101
  43. erad/db/credential_model.py +0 -20
  44. erad/db/disaster_input_model.py +0 -23
  45. erad/db/inject_earthquake.py +0 -52
  46. erad/db/inject_flooding.py +0 -53
  47. erad/db/neo4j_.py +0 -162
  48. erad/db/utils.py +0 -14
  49. erad/exceptions.py +0 -68
  50. erad/metrics/__init__.py +0 -0
  51. erad/metrics/check_microgrid.py +0 -208
  52. erad/metrics/metric.py +0 -178
  53. erad/programs/__init__.py +0 -0
  54. erad/programs/backup.py +0 -62
  55. erad/programs/microgrid.py +0 -45
  56. erad/scenarios/__init__.py +0 -0
  57. erad/scenarios/abstract_scenario.py +0 -103
  58. erad/scenarios/common.py +0 -93
  59. erad/scenarios/earthquake_scenario.py +0 -161
  60. erad/scenarios/fire_scenario.py +0 -160
  61. erad/scenarios/flood_scenario.py +0 -494
  62. erad/scenarios/utilities.py +0 -76
  63. erad/scenarios/wind_scenario.py +0 -89
  64. erad/utils/__init__.py +0 -0
  65. erad/utils/ditto_utils.py +0 -252
  66. erad/utils/hifld_utils.py +0 -147
  67. erad/utils/opendss_utils.py +0 -357
  68. erad/utils/overpass.py +0 -76
  69. erad/utils/util.py +0 -178
  70. erad/visualization/__init__.py +0 -0
  71. erad/visualization/plot_graph.py +0 -218
  72. {NREL_erad-0.0.0a0.dist-info → nrel_erad-0.1.1.dist-info/licenses}/LICENSE.txt +0 -0
  73. {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
@@ -0,0 +1,2 @@
1
+ from erad.systems.hazard_system import HazardSystem
2
+ from erad.systems.asset_system import AssetSystem
@@ -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