CHEERSafe Exposure Workflow¶
This notebook runs the cheersafe.exposure chain end to end: a building inventory is mapped onto the
structural archetype M and a seeded resistance configuration C, per-event wind and flood intensities
are assigned from hazard control points to building centroids and converted to continuous hazard IDs
(Wind_Tr, Flood_Tr), zone ids are attached, and the STARR-compatible export is written and read back.
Every calculation is called explicitly and its return value assigned by the notebook, following the package's strict-return pattern. See the exposure manual for the methodology and the API reference for signatures.
!!! warning "Execution profiles"
Smoke mode (default) builds a realistic synthetic case with SyntheticExposureData (2,000 buildings,
5 events, EPSG:5070, the real 1 km grid origin) and runs in about 20 seconds with no external data.
**Full mode** (`CHEERSAFE_EXPOSURE_SMOKE=0`) reads the Eastern North Carolina demo inventory
(2,000 points), the 97 `Old97` hazard layers (63,094 control points each) and the LEAPHI zone layer
from the `Loss Estimates` folder (`CHEERSAFE_EXPOSURE_DATA`). It needs about 200 MB of RAM and a
minute. Missing files are reported and the dependent cells are skipped.
Plan¶
- Choose the execution profile and output folder.
- Load (or generate) the inventory, hazard layers and zones.
- Assign archetypes
Mand configurationsC; inspect their distributions. - Map hazard intensities to buildings and transform them to hazard IDs; compare
NearestandInverse_d4. - Assign zone ids.
- Write the STARR export and verify that the files align.
# Repository setup and imports
import json
import os
import pickle
import sys
import time
from pathlib import Path
starting_directory = Path.cwd().resolve()
repo_root = starting_directory
while not (repo_root / "pyproject.toml").exists() and repo_root != repo_root.parent:
repo_root = repo_root.parent
os.chdir(repo_root)
sys.path.insert(0, str(repo_root / "src"))
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from cheersafe.config import Config
from cheersafe.exposure import Exposure, SyntheticExposureData
from cheersafe.loss_estimation import DamageProbabilityProvider, LossEstimation
from cheersafe.tools import Utils
print(f"repo_root={repo_root}")
/home/mammad/.local/lib/python3.12/site-packages/matplotlib/projections/__init__.py:63: UserWarning: Unable to import Axes3D. This may be due to multiple versions of Matplotlib being installed (e.g. as a system package and as a pip package). As a result, the 3D projection is not available.
warnings.warn("Unable to import Axes3D. This may be due to multiple versions of "
repo_root=/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure
# Execution profile: smoke mode on a synthetic case by default; full mode reads the external Eastern North Carolina data
FULL_DATA_ROOT = Path(os.environ.get("CHEERSAFE_EXPOSURE_DATA", "../Loss Estimates"))
FULL_INVENTORY = FULL_DATA_ROOT / "_not_needed_for_reproduction" / "Inventory" / "NC_Inventory_v1_AI_Imputed.parquet"
FULL_HAZARD_DIR = FULL_DATA_ROOT / "Hazards" / "Old97"
FULL_ZONES = FULL_DATA_ROOT / "Zones" / "leaphi_zones_20230920.shp"
SMOKE_N_BUILDINGS = 2000
SMOKE_N_EVENTS = 5
SMOKE_SEED = 42
AVERAGE_MODE = "Nearest" # "Nearest" (the verified run) or "Inverse_d4"
ZONE_ID = "i_lh740"
COAST_THRESHOLD_MILES = 2.0
DROP_UNMATCHED_OCCUPANCY = False # the verified run kept non-wood and non-single-family rows
smoke_flag = os.environ.get("CHEERSAFE_EXPOSURE_SMOKE", "1")
if smoke_flag not in {"0", "1"}:
raise ValueError("CHEERSAFE_EXPOSURE_SMOKE must be '0' or '1'")
smoke_mode = smoke_flag == "1"
output_override = os.environ.get("CHEERSAFE_EXPOSURE_OUTPUT")
output_path = (
Path(output_override).expanduser().resolve()
if output_override
else (repo_root / "Outputs/exposure_workflow").resolve()
)
full_data_root = (repo_root / FULL_DATA_ROOT).resolve() if not FULL_DATA_ROOT.is_absolute() else FULL_DATA_ROOT
print(f"profile={'smoke' if smoke_mode else 'full'}, average_mode={AVERAGE_MODE}, output={output_path}")
profile=smoke, average_mode=Nearest, output=/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow
# Snapshot the global configuration touched below, then switch to manual control
config_snapshot = {
name: getattr(Config, name)
for name in ("AUTO_LOAD_DATASETS", "AUTO_EXECUTE_METHODS", "INPUTS_PATH", "OUTPUTS_PATH", "RANDOM_SEED")
}
def restore_state() -> None:
"""Restore the configuration snapshot and the starting directory."""
for name, value in config_snapshot.items():
setattr(Config, name, value)
os.chdir(starting_directory)
Config.AUTO_LOAD_DATASETS = False # every load_* call below is explicit
Config.AUTO_EXECUTE_METHODS = False # every calculate_* result is assigned by this notebook
Config.INPUTS_PATH = "Inputs" # the package's JSON tables
output_path.mkdir(parents=True, exist_ok=True)
print(config_snapshot)
{'AUTO_LOAD_DATASETS': True, 'AUTO_EXECUTE_METHODS': True, 'INPUTS_PATH': 'Inputs', 'OUTPUTS_PATH': 'Outputs', 'RANDOM_SEED': 42}
Inputs¶
Smoke mode writes a complete synthetic Inputs tree (inventory, hazard events, zones, the four JSON
tables and event probabilities). Full mode points at the external Eastern North Carolina files.
canonical_tables = {
"wind_speeds": Utils.from_json("Inputs/WindSpeed.json"),
"flood_depths": Utils.from_json("Inputs/FloodDepth.json"),
"damage_states": Utils.from_json("Inputs/DamageState.json"),
"component_combinations": Utils.from_json("Inputs/ComponentCombination_All.json"),
}
data_available = True
if smoke_mode:
case_root = output_path / "synthetic_case"
written = SyntheticExposureData.save_inputs(
str(case_root),
inventory=SyntheticExposureData.make_inventory(SMOKE_N_BUILDINGS, seed=SMOKE_SEED),
hazard_events=SyntheticExposureData.make_hazard_events(
tuple(f"h{index}" for index in range(1, SMOKE_N_EVENTS + 1)), nx=4, ny=4, seed=SMOKE_SEED
),
zones=SyntheticExposureData.make_zones(extent_cells=(4, 4)),
hurricane_probabilities=SyntheticExposureData.make_hurricane_probabilities(SMOKE_N_EVENTS, seed=SMOKE_SEED),
**canonical_tables,
)
inventory_path, hazard_dir, zones_path = written["inventory"], written["hazard_dir"], written["zones"]
else:
inventory_path = full_data_root / FULL_INVENTORY.relative_to(FULL_DATA_ROOT)
hazard_dir = full_data_root / FULL_HAZARD_DIR.relative_to(FULL_DATA_ROOT)
zones_path = full_data_root / FULL_ZONES.relative_to(FULL_DATA_ROOT)
missing = [str(p) for p in (inventory_path, hazard_dir, zones_path) if not p.exists()]
if missing:
data_available = False
print("skipped: missing", missing)
print(f"inventory={inventory_path}\nhazards={hazard_dir}\nzones={zones_path}\navailable={data_available}")
inventory=/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/synthetic_case/Inputs/Inventory.parquet hazards=/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/synthetic_case/Inputs/Hazards zones=/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/synthetic_case/Inputs/zones.gpkg available=True
Archetypes M and configurations C¶
M (1-8) is deterministic from stories, roof shape and garage (Peng 2013, Table 3.1). C (1-192) is one
row of ComponentCombination_All.json, drawn per building from the construction-period and coast-distance
rules (Peng 2013, Tables 3.6-3.7) through the seeded Sampling stream, so a seed reproduces the draw.
if data_available:
exposure = Exposure(
inventory_path=str(inventory_path),
hazard_dir=str(hazard_dir),
zones_path=str(zones_path),
average_mode=AVERAGE_MODE,
zone_id_col=ZONE_ID,
coast_threshold_miles=COAST_THRESHOLD_MILES,
drop_unmatched_occupancy=DROP_UNMATCHED_OCCUPANCY,
seed=SMOKE_SEED,
)
exposure.load_hazard_tables()
exposure.load_component_combinations()
exposure.load_inventory()
exposure.load_hazard_events()
exposure.load_zones()
exposure.archetype_inventory = exposure.calculate_archetypes(exposure.inventory)
archetypes = exposure.archetype_inventory
assert archetypes["M"].between(1, 8).all() and archetypes["C"].between(1, 192).all()
print(f"{len(exposure.inventory)} buildings loaded, {len(archetypes)} with complete attributes")
print("M counts:", archetypes["M"].value_counts().sort_index().to_dict())
print("top C:", archetypes["C"].value_counts().head(10).to_dict())
display(archetypes[["building_key", "M", "C", "RC", "RF", "RTW", "O", "W", "F", "year_group", "near_coast"]].head())
2005 buildings loaded, 2004 with complete attributes
M counts: {1: 145, 2: 378, 3: 119, 4: 426, 5: 117, 6: 339, 7: 104, 8: 376}
top C: {1: 1021, 7: 91, 25: 79, 5: 45, 97: 38, 188: 38, 3: 35, 145: 34, 31: 34, 49: 30}
| building_key | M | C | RC | RF | RTW | O | W | F | year_group | near_coast | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 100001 | 5 | 43 | 2 | 2 | 2 | 1 | 1 | 1 | 2 | False |
| 1 | 100002 | 4 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | False |
| 2 | 100003 | 3 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | False |
| 3 | 100004 | 5 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | False |
| 4 | 100005 | 3 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | False |
if data_available:
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
archetypes["M"].value_counts().sort_index().plot.bar(ax=axes[0], color="#4c72b0")
axes[0].set_xlabel("Archetype M"); axes[0].set_ylabel("Buildings"); axes[0].set_title("Structural archetypes")
archetypes["C"].value_counts().head(10).plot.bar(ax=axes[1], color="#dd8452")
axes[1].set_xlabel("Configuration C (ten most common)"); axes[1].set_title("Resistance configurations")
for ax in axes:
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
Hazard mapping and hazard IDs¶
Buildings are clipped to the control-point bounding box, matched to their nearest control points, and each
event's wind (m/s) and flood (m) are assigned and converted to mph and inches before interpolation onto the
package's discrete hazard IDs. Wind_Tr lies in [1, 25] and Flood_Tr in [1, 136].
if data_available:
try:
started = time.perf_counter()
exposure.clipped_inventory = exposure.calculate_clipped_inventory(exposure.archetype_inventory)
centroids = exposure.clipped_inventory.geometry.centroid
building_xy = np.column_stack([centroids.x, centroids.y])
k_neighbors = 1 if AVERAGE_MODE == "Nearest" else exposure.idw_neighbors
exposure.neighbor_index, exposure.neighbor_distance = exposure.calculate_neighbor_match(
building_xy, exposure.hazard_points_xy, k_neighbors
)
exposure.wind_ids, exposure.flood_ids = exposure.calculate_hazard_ids(
exposure.neighbor_index, exposure.neighbor_distance, exposure.hazard_wind, exposure.hazard_flood
)
assert 1.0 <= exposure.wind_ids.min() and exposure.wind_ids.max() <= 25.0
assert 1.0 <= exposure.flood_ids.min() and exposure.flood_ids.max() <= 136.0
print(f"{len(exposure.clipped_inventory)} buildings inside the hazard bounds, {len(exposure.event_names)} events")
print(f"Wind_Tr range {exposure.wind_ids.min():.3f}-{exposure.wind_ids.max():.3f}; "
f"Flood_Tr range {exposure.flood_ids.min():.3f}-{exposure.flood_ids.max():.3f}; "
f"{time.perf_counter() - started:.1f} s")
# Compare the two averaging modes on the first event
index_4, distance_4 = exposure.calculate_neighbor_match(building_xy, exposure.hazard_points_xy, 4)
nearest_wind = exposure.calculate_event_intensity(exposure.hazard_wind[0], index_4, distance_4, mode="Nearest")
idw_wind = exposure.calculate_event_intensity(exposure.hazard_wind[0], index_4, distance_4, mode="Inverse_d4")
print(f"event {exposure.event_names[0]}: mean |Nearest - Inverse_d4| = {np.abs(nearest_wind - idw_wind).mean():.3f} m/s")
except BaseException:
restore_state()
raise
2003 buildings inside the hazard bounds, 5 events Wind_Tr range 1.427-11.530; Flood_Tr range 1.000-79.519; 0.0 s event h1: mean |Nearest - Inverse_d4| = 2.035 m/s
if data_available:
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
scatter = axes[0].scatter(building_xy[:, 0], building_xy[:, 1], c=exposure.wind_ids[:, 0], s=6, cmap="viridis")
axes[0].scatter(exposure.hazard_points_xy[:, 0], exposure.hazard_points_xy[:, 1], marker="+", c="k", s=25, label="control points")
axes[0].set_title(f"Wind_Tr for event {exposure.event_names[0]} ({AVERAGE_MODE})")
axes[0].set_aspect("equal"); axes[0].legend(loc="upper right")
fig.colorbar(scatter, ax=axes[0], label="Wind hazard ID")
axes[1].scatter(nearest_wind, idw_wind, s=6, alpha=0.6)
limits = [min(nearest_wind.min(), idw_wind.min()), max(nearest_wind.max(), idw_wind.max())]
axes[1].plot(limits, limits, "k--", lw=1)
axes[1].set_xlabel("Nearest control point (m/s)"); axes[1].set_ylabel("Inverse-distance (power 4) average (m/s)")
axes[1].set_title("Averaging modes on the first event"); axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Zone assignment¶
Each building takes the id of the zone polygon containing its centroid; buildings outside every zone get -1.
if data_available:
exposure.zone_ids = exposure.calculate_zone_assignment(exposure.clipped_inventory, exposure.zones, ZONE_ID)
zone_counts = pd.Series(exposure.zone_ids).value_counts().sort_index()
print(f"{(exposure.zone_ids == -1).sum()} buildings outside every zone; {zone_counts[zone_counts.index != -1].size} zones hold buildings")
display(zone_counts.head(10).rename("buildings").to_frame().T)
1 buildings outside every zone; 2 zones hold buildings
| -1 | 1 | 2 | |
|---|---|---|---|
| buildings | 1 | 992 | 1010 |
STARR export¶
calculate_starr_inventory assembles the STARR table; save_starr_inputs writes it together with
Wind_Tr.npy, Flood_Tr.npy, Hurricane_name.npy and a metadata file. The rows of the CSV and of the
arrays align, which the reload below verifies.
if data_available:
exposure.starr_inventory = exposure.calculate_starr_inventory(exposure.clipped_inventory, exposure.zone_ids)
export_paths = exposure.save_starr_inputs(str(output_path / "starr"))
reloaded_table = pd.read_csv(export_paths["inventory_csv"])
reloaded_wind = np.load(export_paths["wind_ids"], mmap_mode="r")
reloaded_names = np.load(export_paths["event_names"])
assert len(reloaded_table) == reloaded_wind.shape[0]
assert list(reloaded_names) == exposure.event_names
assert list(reloaded_table.columns[:4]) == ["building_ID", "t", "YearBuilt", "YearBuiltGroup"]
print(json.dumps(export_paths, indent=2))
display(reloaded_table.head())
{
"inventory_csv": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/Inventory.csv",
"wind_ids": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/Wind_Tr.npy",
"flood_ids": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/Flood_Tr.npy",
"event_names": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/Hurricane_name.npy",
"metadata": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/HazardInput_metadata.txt",
"archetype_inventory": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/archetype_inventory.parquet",
"clipped_inventory": "/home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/Outputs/exposure_workflow/starr/clipped_inventory.parquet"
}
| building_ID | t | YearBuilt | YearBuiltGroup | i_lh740 | M | C | RC | RF | RTW | ... | contents_value | home_value | fd_id_nsi | is_vacant | H_TEN_syn | x | y | lon | lat | building_key | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 2004 | 1997to2010 | 1 | 5 | 43 | 2 | 2 | 2 | ... | 216531.04 | 649593.13 | 100001 | False | 2 | 1.480195e+06 | 1.345770e+06 | -79.770848 | 34.036654 | 100001 |
| 1 | 2 | 0 | 1959 | before1968 | 1 | 4 | 1 | 1 | 1 | 1 | ... | 120239.06 | 360717.19 | 100002 | False | 1 | 1.480174e+06 | 1.345760e+06 | -79.771090 | 34.036594 | 100002 |
| 2 | 3 | 0 | 1959 | before1968 | 1 | 3 | 1 | 1 | 1 | 1 | ... | 98113.90 | 294341.71 | 100003 | False | 1 | 1.481036e+06 | 1.345723e+06 | -79.761894 | 34.034963 | 100003 |
| 3 | 4 | 0 | 2014 | 1997to2010 | 2 | 5 | 1 | 1 | 1 | 1 | ... | 132422.94 | 397268.82 | 100004 | False | 2 | 1.479845e+06 | 1.346316e+06 | -79.773594 | 34.041997 | 100004 |
| 4 | 5 | 0 | 1962 | before1968 | 1 | 3 | 1 | 1 | 1 | 1 | ... | 180415.36 | 541246.09 | 100005 | False | 1 | 1.480918e+06 | 1.346053e+06 | -79.762550 | 34.038047 | 100005 |
5 rows × 24 columns
# Restore the configuration changed above and return to the starting directory
restore_state()
print("configuration restored; working directory:", Path.cwd())
configuration restored; working directory: /home/mammad/Documents/EDU/cope/CHEERSafe/LossModel-exposure/examples
Results, pitfalls, and next steps¶
- The export under
Outputs/exposure_workflow/starr/is the input of the loss-estimation workflow;LossEstimation.load_from_exposure(exposure)hands the same tables over in memory. Cis drawn from a seeded stream, so the archetype table differs row by row from the frozen 07/07/2026 Eastern North Carolina export by design; the marginal distributions are the same.- The building-type and occupancy filters are off by default, as in the verified run; set
DROP_UNMATCHED_OCCUPANCY = Trueto keep only wood-frame single-family residences. - Hazard columns are selected by name (
Wind,Flood) and all event files must share the same control points; a mismatch raisesValueErrorinload_hazard_events.