Skip to content

Exposure#

The exposure module integrates building-level inventories with the damage probability outputs generated by CHEER-Safe, enabling loss estimation across multiple spatial resolutions, including individual structures and aggregated geographic zones. It maps user inventory columns onto the structural archetype M and the resistance configuration C, assigns per-event wind and flood intensities from hazard control points to building centroids, converts them to the package's continuous 1-based hazard IDs (Wind_Tr, Flood_Tr), assigns a zone id per building and writes the STARR-compatible export consumed by Loss Estimation.

The module replaces the standalone Inventory-Loss Link scripts (one Inventory_Loss class that resolved every file from a fixed folder tree under the working directory) with four dataclasses that take explicit path fields, follow Config.AUTO_LOAD_DATASETS / Config.AUTO_EXECUTE_METHODS, and return their results so callers can assign them.

Classes#

Page Use when
BaseExposure You need to load the inventory layer, the per-event hazard control-point layers, an optional zone layer and the discrete tables, or to describe your inventory columns with InventorySchema.
ArchetypeAssigner You need the archetype M (1-8) and a seeded resistance configuration C (1-192) for every building.
HazardMapper You need per-event wind and flood hazard IDs at building centroids, or the grid helpers behind the Eastern North Carolina hazard maps.
Exposure You want zone ids and the STARR export (Inventory.csv, Wind_Tr.npy, Flood_Tr.npy), or an in-memory hand-off to LossEstimation.

The classes form one inheritance chain, BaseExposure -> ArchetypeAssigner -> HazardMapper -> Exposure; constructing Exposure gives access to every loader and calculation of the chain.

Pipeline#

flowchart LR
    I[(Inventory layer)] --> N[Normalized attributes<br>InventorySchema]
    N --> A[Archetype M<br>Configuration C]
    H[(Hazard control points<br>one layer per event)] --> M[Intensity at centroids<br>Nearest or Inverse_d4]
    A --> M
    M --> T[Continuous hazard IDs<br>Wind_Tr and Flood_Tr]
    Z[(Zone layer)] --> S[Zone id per building]
    T --> S
    S --> X[STARR export<br>Inventory.csv, Wind_Tr.npy, Flood_Tr.npy]
    X --> L[Loss Estimation]

Shared Inputs#

File Shape Units or indexing Used by
Inventory layer (inventory_path, default Inputs/Inventory.parquet) one row per building Columns named by InventorySchema; polygon or point geometry with a CRS. GeoParquet, shapefile or GeoPackage. load_inventory
Hazard control-point layers (hazard_dir, default Inputs/Hazards, pattern *.parquet) n_points rows per event file Wind in m/s, Flood in m, -9999 nodata; identical point geometry and order in every file; event order from the integer embedded in the file name. load_hazard_events, HazardMapper
Zone layer (zones_path, optional) polygons LEAPHI columns COUNTYFP00, zone, i_lh503, i_lh1509, i_lh708, i_lh740; any CRS (reprojected on the fly). load_zones, Exposure
Inputs/WindSpeed.json [25, 3] id, mph, m/s; the mph column is the knot table for wind IDs. HazardMapper
Inputs/FloodDepth.json [136, 3] id, inches, m; the inch column is the knot table for flood IDs. HazardMapper
Inputs/ComponentCombination_All.json [192, 6] RC, RF, RTW, O, W, F states; row C defines configuration C. ArchetypeAssigner

Paths left as None resolve under Config.INPUTS_PATH; output_dir resolves to <Config.OUTPUTS_PATH>/exposure. The input manual lists the layer schemas and grid facts in detail.

Synthetic Test Case#

SyntheticExposureData generates a complete, small case whose columns, labels, value ranges, CRS (EPSG:5070) and 1 km hazard grid follow the Eastern North Carolina source data without shipping any of it. The generators are parameterized from the measured demo distributions, not sampled from real rows, and every draw goes through cheersafe.tools.Sampling, so a seed reproduces a case exactly. The test suite and the example notebooks run on it.

Method Returns Notes
make_inventory(n=40, seed=None, ..., spread_full_window=False) GeoDataFrame in the demo schema (bldgtype, roof_shape_1, Story_AI, Garage, PRIM_OCC, med_yr_blt, Coast_D (miles), val_struct, val_cont, fd_id_nsi, is_vacant, H_TEN_syn, geometry) 10 m square footprints inside the 1.5 km strip covered by make_zones (or across the whole grid window with spread_full_window=True); year built 1950-2020 so all three construction periods occur; coast distance on both sides of 2 miles; val_cont = 0.5 x val_struct. With include_edge_cases=True, five rows with fd_id_nsi >= 900000 exercise a missing roof shape, bldgtype='M', PRIM_OCC='Multi Family', a centroid outside the hazard bounding box and a centroid outside every zone.
make_hazard_events(event_names=("h1", "h2", "h10"), nx=3, ny=3, seed=None, ...) dict[name -> GeoDataFrame] with cell_ID, x, y, Wind, Flood, geometry Points exactly on the grid, cell_ID = ix * 350 + iy + 149103; Wind 9.5-44.5 m/s; Flood 0-2 m with about half the points dry and the first point always dry; nodata_cells sets -9999 at chosen rows.
make_zones(...) Two polygons with all six LEAPHI columns i_lh740 = [1, 2], both nesting into i_lh503 = 1; the north-eastern corner of the window is left unassigned.
make_damage_library(keys=((1, 1), (2, 101), (8, 192)), n_wind=25, n_flood=136, n_damage_states=50, seed=None, unnormalized_rows=1) dict[(m, c) -> float32 (25, 136, 50)] Rows sum to one and rise monotonically with hazard; the zero-hazard row is a point mass on the first state; unnormalized_rows rows are scaled by 0.9 to exercise the builder's warning.
make_hurricane_probabilities(n_events=3, seed=None, annual_rate=0.32) (n_events, 2) ids and probabilities Sum equals annual_rate, mirroring the 0.3235 per year of the source catalogue.
save_inputs(directory, inventory, hazard_events, zones, hurricane_probabilities, wind_speeds, flood_depths, damage_states, component_combinations, damage_library=None, ...) dict of written paths Writes <directory>/Inputs/{Inventory.parquet, Hazards/<event>.parquet, zones.gpkg, WindSpeed.json, FloodDepth.json, DamageState.json, ComponentCombination_All.json, Hurricane_ProbWindFlood.json[, damage_library.pkl]}.

Configuration Behavior#

Config.AUTO_LOAD_DATASETS=True makes __post_init__ run the five loaders (load_hazard_tables, load_component_combinations, load_inventory, load_hazard_events, load_zones) and set datasets_loaded. With False, call them yourself before any calculation.

Config.AUTO_EXECUTE_METHODS=True then runs the calculation chain of the class you constructed and assigns the results: archetype_inventory (ArchetypeAssigner), clipped_inventory, neighbor_index, neighbor_distance, wind_ids, flood_ids (HazardMapper), zone_ids and starr_inventory (Exposure). With False, call the calculate_* methods and assign their return values; nothing is written to disk until you call a save_* method. update_outputs() reloads every input and reruns the chain.

Config.RANDOM_SEED is the default seed of the configuration draw when the seed field is None. Config.INPUTS_PATH and Config.OUTPUTS_PATH resolve the path fields left as None.

Minimal Examples#

Automatic initialization (the default flags), with the inputs under Config.INPUTS_PATH:

from cheersafe.config import Config
from cheersafe.exposure import Exposure

Config.AUTO_LOAD_DATASETS = True
Config.AUTO_EXECUTE_METHODS = True
Config.INPUTS_PATH = "Inputs"    # Inventory.parquet, Hazards/, zones.gpkg and the JSON tables
Config.OUTPUTS_PATH = "Outputs"

exposure = Exposure(zones_path="Inputs/zones.gpkg", seed=1)
table, wind_ids, flood_ids, event_names = exposure.calculate_starr_export()
paths = exposure.save_starr_inputs()    # Outputs/exposure/Inventory.csv, Wind_Tr.npy, ...

Manual dependency injection, where the caller assigns every returned value:

import numpy as np

from cheersafe.config import Config
from cheersafe.exposure import Exposure

Config.AUTO_LOAD_DATASETS = False
Config.AUTO_EXECUTE_METHODS = False

exposure = Exposure(
    inventory_path="Inputs/Inventory.parquet",
    hazard_dir="Inputs/Hazards",
    zones_path="Inputs/zones.gpkg",
    average_mode="Nearest",
    seed=1,
)
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)
exposure.clipped_inventory = exposure.calculate_clipped_inventory(exposure.archetype_inventory)
centroids = exposure.clipped_inventory.geometry.centroid
exposure.neighbor_index, exposure.neighbor_distance = exposure.calculate_neighbor_match(
    np.column_stack([centroids.x, centroids.y]), exposure.hazard_points_xy, k=1
)
exposure.wind_ids, exposure.flood_ids = exposure.calculate_hazard_ids(
    exposure.neighbor_index, exposure.neighbor_distance, exposure.hazard_wind, exposure.hazard_flood
)
exposure.zone_ids = exposure.calculate_zone_assignment(
    exposure.clipped_inventory, exposure.zones, "i_lh740"
)
exposure.starr_inventory = exposure.calculate_starr_inventory(
    exposure.clipped_inventory, exposure.zone_ids
)
paths = exposure.save_starr_inputs("Outputs/exposure")

The quickstart runs the same chain on a synthetic case and hands it to LossEstimation without touching the disk.

Differences From The Verified Scripts#

Deliberate departures

The defaults reproduce the verified Eastern North Carolina run; the following points differ by design and are documented here so results can be compared knowingly.

  • The configuration draw C is seeded and vectorized through cheersafe.tools.Sampling (label exposure.component_configuration), whereas the scripts used an unseeded np.random.choice. Archetype tables therefore differ row by row from the frozen 07/07/2026 export even though the marginal distributions are identical.
  • The building-type (bldgtype) and occupancy (PRIM_OCC) filters were no-ops in the verified run: every building of the source inventory passed both. They stay off by default (drop_unmatched_occupancy=False) and only flag rows in is_wood_frame / is_single_family; set the flag to actually drop non-wood or non-single-family rows.
  • Hazard columns are selected by name (wind_col, flood_col), not by position, and every path is an explicit field instead of the nine-folder tree under the working directory.
  • Control-point geometry is checked across all event files and a mismatch raises ValueError; the scripts assumed identical geometry.
  • The linear index convention of the loss module interpolates over the full ID range; the scripts' interpolation clamp never reached the last bin.
  • Damage-library rows that do not sum to one are counted and reported; (m, c) pairs missing from the library yield NaN, not zero, loss ratios.
  • Hurricane_name.npy is saved as a unicode array, so it loads without allow_pickle; no compatibility unpickler is needed for the library.
  • Nodata (-9999) and missing hazard values are set to zero before inverse-distance averaging exactly as in the scripts; this behaviour is replicated but has not been verified against published outputs.

References#

  • Peng, J. (2013). Modeling natural disaster risk management: Integrating the roles of insurance and retrofit and multiple stakeholder perspectives. PhD thesis, University of Delaware. Source of the archetype table and of the component-state probabilities by construction period and coast distance.
  • Apivatanagul, P., Davidson, R., Blanton, B., Nozick, L. (2011). Long-term regional hurricane hazard analysis for wind and storm surge. Coastal Engineering, 58. Source of the hazard event set and its annual probabilities.
  • CHEER-Hub, Inventory-Loss Link documentation (repository Inventory-Loss-link). Describes the superseded Inventory_Loss interfaces and folder layout; the input descriptions remain useful, the calls do not.