Working with GDM models#

ERAD’s AssetSystem provides a class method for users working with a GDM DistributionSystem. Distribution system components are automatically mapped to the asset models and added to the AssetSystem. A hazard simulation can then be set up using the steps listed above. We start by loading a GDM model using the gdmloader package. This package can be installed using the command

pip install gdmloader
from IPython.display import display, HTML
import plotly.graph_objects as go
import plotly.io as pio

pio.renderers.default = "notebook_connected"

from gdm.distribution import DistributionSystem
from gdmloader.constants import GCS_CASE_SOURCE
from gdmloader.source import SystemLoader

gdm_loader = SystemLoader()
gdm_loader.add_source(GCS_CASE_SOURCE)

distribution_system: DistributionSystem = gdm_loader.load_dataset(
    source_name=GCS_CASE_SOURCE.name,
    system_type=DistributionSystem,
    dataset_name="p1rhs7_1247",
    version="2_1_2",
)
distribution_system.name = "p1rhs7_1247"
distribution_system.info()
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[1], line 14
     11 gdm_loader = SystemLoader()
     12 gdm_loader.add_source(GCS_CASE_SOURCE)
---> 14 distribution_system: DistributionSystem = gdm_loader.load_dataset(
     15     source_name=GCS_CASE_SOURCE.name,
     16     system_type=DistributionSystem,
     17     dataset_name="p1rhs7_1247",
     18     version="2_1_2",
     19 )
     20 distribution_system.name = "p1rhs7_1247"
     21 distribution_system.info()

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/gdmloader/source.py:134, in SystemLoader.load_dataset(self, system_type, source_name, dataset_name, version)
    131         raise ValueError(msg)
    133 system_file = list(dataset_folder.rglob("*.json"))[0]
--> 134 return system_type.from_json(system_file)

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/infrasys/system.py:272, in System.from_json(cls, filename, upgrade_handler, **kwargs)
    270     data = orjson.loads(f_in.read())
    271 time_series_parent_dir = Path(filename).parent
--> 272 return cls.from_dict(
    273     data, time_series_parent_dir, upgrade_handler=upgrade_handler, **kwargs
    274 )

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/infrasys/system.py:494, in System.from_dict(cls, data, time_series_parent_dir, upgrade_handler, **kwargs)
    492 if component_needs_metadata_migration(system_data["components"][0]):
    493     system_data["components"] = migrate_component_metadata(system_data["components"])
--> 494 system._deserialize_components(system_data["components"])
    495 system._deserialize_supplemental_attributes(system_data["supplemental_attributes"])
    496 logger.info("Deserialized system {}", system.label)

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/infrasys/system.py:1592, in System._deserialize_components(self, components)
   1590 skipped_types = self._deserialize_components_first_pass(components, cached_types)
   1591 if skipped_types:
-> 1592     self._deserialize_components_nested(skipped_types, cached_types)

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/infrasys/system.py:1626, in System._deserialize_components_nested(self, skipped_types, cached_types)
   1624 if len(components) > 1:
   1625     for component_dict in components[1:]:
-> 1626         component = self._try_deserialize_component(component_dict, cached_types)
   1627         assert component is not None
   1628 deserialized_types.add(component_type)

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/infrasys/system.py:1648, in System._try_deserialize_component(self, component, cached_types)
   1646 metadata = SerializedTypeMetadata.validate_python(component[TYPE_METADATA])
   1647 component_type = cached_types.get_type(metadata)
-> 1648 actual_component = component_type(**values)
   1649 self._components.add(actual_component, deserialization_in_progress=True)
   1650 return actual_component

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/pydantic/main.py:250, in BaseModel.__init__(self, **data)
    248 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    249 __tracebackhide__ = True
--> 250 validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
    251 if self is not validated_self:
    252     warnings.warn(
    253         'A custom validator is returning a value other than `self`.\n'
    254         "Returning anything other than `self` from a top level model validator isn't supported when validating via `__init__`.\n"
    255         'See the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.',
    256         stacklevel=2,
    257     )

ValidationError: 1 validation error for MatrixImpedanceBranch
  Value error, Length of matrix mat=<Quantity([[1.76096667 0.39596667]
 [0.39596667 1.76096667]], 'ohm / kilometer')> did not match number of phases self.phases=[<Phase.S2: 'S2'>, <Phase.S1: 'S1'>, <Phase.N: 'N'>] [type=value_error, input_value={'uuid': '42a039ae-76dd-4...tity(115.0, 'ampere')>)}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.12/v/value_error

Next, we built the asset system from the gdm DistributionSystem using the from_gdm method.

from erad.systems import AssetSystem

asset_system = AssetSystem.from_gdm(distribution_system)
asset_system.info()

for a in asset_system.iter_all_components():
    a.pprint()
    break
System                          
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Property              Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ System name          │       │
│ Data format version  │       │
│ Components attached  │  4987 │
│ Time Series attached │     0 │
│ Description          │       │
└──────────────────────┴───────┘
Component Information           
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Type                  Count ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ Asset                │  4987 │
└──────────────────────┴───────┘
Asset(
    name='sb10_p1rhs7_1247_bus_10_6',
    distribution_asset=UUID('1f95b063-9b69-4331-8f56-6a718161087f'),
    connections=[],
    devices=[],
    asset_type=<AssetTypes.distribution_poles: 6>,
    height=<Quantity(3, 'meter')>,
    latitude=38.64000605387684,
    longitude=-122.50715315149552,
    asset_state=[],
    elevation=<Quantity(460.0, 'meter')>
)

Plotting an AssetSystem#

fig = asset_system.plot(show=False)
display(HTML(pio.to_html(fig, include_plotlyjs="cdn", full_html=False)))
Asset type:  substation
Asset type:  distribution_poles
Asset type:  distribution_junction_box

Building a HazardModel#

In this section, we built a hazard model and apply the model the asset system.

from datetime import datetime

from shapely.geometry import Polygon
from gdm.quantities import Distance

from erad.models.hazard import FloodModelArea, FloodModel
from erad.systems import HazardSystem
from erad.quantities import Speed

flood_area = FloodModelArea(
    affected_area=Polygon(
        [
            (-122.38, 38.70),
            (-122.35, 38.68),
            (-122.343, 38.69),
            (-122.37, 38.7035),
        ]
    ),
    water_velocity=Speed(0, "meter/second"),
    water_elevation=Distance(160, "meter"),
)

flood = FloodModel(
    name="flood 1",
    timestamp=datetime.now(),
    affected_areas=[flood_area],
)

user_defined_flood_event = HazardSystem(auto_add_composed_components=True)
user_defined_flood_event.add_component(flood)

Overlaying the HazardModel#

We can overlay the hazard model on the same plot using the add_trace method. The show method can be used to render the image again.

polygon = flood.affected_areas[0].affected_area
lon, lat = polygon.exterior.xy  # returns x and y sequences

fig.add_trace(
    go.Scattermap(
        fill="toself",
        lon=lon.tolist(),
        lat=lat.tolist(),
        marker={"size": 10, "color": flood.affected_areas[0].water_velocity.magnitude},
    )
)

fig.show()

Finally, we can run the actual simulation using the HazardSimulator class from erad.runner.

from erad.runner import HazardSimulator

user_defined_flood_event.info()

hazard_scenario = HazardSimulator(asset_system=asset_system)
hazard_scenario.run(hazard_system=user_defined_flood_event)
System                          
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Property              Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ System name          │       │
│ Data format version  │       │
│ Components attached  │     2 │
│ Time Series attached │     0 │
│ Description          │       │
└──────────────────────┴───────┘
Component Information           
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Type                  Count ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ FloodModel           │     1 │
│ FloodModelArea       │     1 │
└──────────────────────┴───────┘
2025-07-18 16:06:49.885 | WARNING  | erad.runner:run:51 - No HazardFragilityCurves definations found in the passed HazardSystem using default curve definations

Once the simulation is complete, we can visualize the results by plotting the survival_prob from the updated gdf dataframe.

fig = asset_system.plot(show=False)
display(HTML(pio.to_html(fig, include_plotlyjs="cdn", full_html=False)))
Asset type:  substation
Asset type:  distribution_poles
Asset type:  distribution_junction_box