Wiring a new peril¶
Adding a hazard to Climate-Lama touches an enum, an ingestor, a curve or a score scheme, a map layer, a chunk budget, an engine code, and a UI label. The coupling is real and it is why each new peril has historically been a cross-cutting change rather than a mechanical one. This page is the checklist that makes it mechanical.
Work top to bottom. Every step names the file to edit and the check that proves it landed.
0. Decide the posture first: computable or indicator?¶
This is the only decision on the list that is not mechanical, and everything below depends on it.
A peril is computable when a seeded vulnerability curve consumes the intensity unit the ingested data actually publishes. It is an indicator peril when no such curve exists — either because the science has not produced one (heatwave, drought: no published building-damage function) or because the obtainable data publishes a quantity no damage function can consume in principle (wildfire on FWI, a weather-only fire-danger index — see #481; do not re-open that search, two investigations have already dead-ended there).
You do not set the posture; you set its inputs. There is no
is_indicator column. core/eai_eligibility.py derives the answer at every
read from two facts already recorded — impact_functions.intensity_unit and
hazard_datasets.intensity_unit — so the posture cannot drift from the data,
and seeding a curve that consumes the unit flips every affected dataset to
computable with no migration and no backfill.
What you do declare is the acknowledgement: an ingest whose unit no curve can
consume is refused with 422 E_HAZARD_INTENSITY_UNIT_MISMATCH unless the
request sets indicator: true. That keeps a mistyped or mis-sourced unit
failing fast instead of quietly entering the catalogue as a score-only layer.
Declaring indicator: true for a unit a curve can consume is refused too —
the derivation would contradict it.
A computable peril may still owe a caveat. Posture answers "does a number
come out?"; EaiGrade in the same module answers "how much weight can it
bear?" — full by default, screening for a peril whose method is a stated
simplification of the published model behind it. Unlike posture, grade is
declared: it is a property of the method, not of a recorded unit, so there is
nothing to derive it from. Add an entry to _SCREENING_GRADE_PERILS with the
note the API will show, and the eai_grade/eai_grade_note fields carry it
onto the score card, the rollups and GET /v1/hazards. Earthquake is the
worked example (ADR-060): ESHM20 PGA rasters read at a point
against four representative ESRM20 class curves, where ESRM20 itself convolves
taxonomy-keyed fragility with spectral acceleration in OpenQuake.
Eligibility is a (peril × exposure type) question, not a per-peril flag: the
curves that do exist in these families are exposure-specific (drought
crop-yield for agricultural exposure, heat-mortality for population). The
unit gate still unions across exposure types — a dataset is an indicator only
when nothing can consume the unit — and GET /v1/hazards reports the
per-exposure-type breakdown under eai_eligibility.by_exposure_type, now as
{eligible, reason, note} per type rather than a bare boolean.
Curve applicability is the second axis, and it is enforced (ADR-061,
issue #690). When a job names both an exposure dataset and a built-in
curve, validate_impact_refs compares the dataset's exposure_type against
the type that curve was calibrated for (impact_function_seeder.
builtin_exposure_type()) and refuses a mismatch with 422
E_EXPOSURE_TYPE_NOT_EAI_ELIGIBLE. Two exemptions: a user-authored curve is
never refused (its author asserted its applicability, and that is the
extension path), and an org-wide run — exposure_dataset_id = null — has
no single exposure type to compare, so it is unchecked. So seeding a curve for
a new exposure type means adding it to _BUILTIN_EXPOSURE_TYPES as well as to
_BUILTIN_FUNCTIONS; a curve absent from the former is treated as
user-authored and escapes the gate.
| Computable peril | Indicator peril | |
|---|---|---|
| Seeded impact function | required (step 3) | none |
| Score scheme | optional | required (step 4) — it is the only output |
POST /v1/hazards/ingest |
plain | indicator: true |
POST /v1/compute/impact |
works, unless the built-in curve's exposure type ≠ the dataset's (422 E_EXPOSURE_TYPE_NOT_EAI_ELIGIBLE) |
422 E_INDICATOR_DATASET_NO_EAI |
| Score card / rollups | indicator: false |
indicator: true |
1. Enum member¶
- Add the member to
HazardTypeinsrc/climate_lama/models/enums.py, and its two-letter engine code to_HAZ_TYPE_TO_ENGINE. - Add a matching
ALTER TYPE hazard_type_enum ADD VALUEAlembic revision. Postgres cannot use a value in the same transaction that adds it, so the enum-add and any seed rows referencing it must be separate revisions (precedent: migration0069). Use Alembic's typed operations; never hand-write raw SQL. - Check:
alembic upgrade headfrom scratch, and the member appears inGET /v1/hazardsfilters.
2. Ingestor¶
- Add
src/climate_lama/core/ingest/<peril>.pywith aBaseGeoTIFFIngestorsubclass declaringintensity_unit(andsupported_unitswhen more than one unit is acceptable) plusfrequency_type. - Register it in
INGESTOR_REGISTRYinsrc/climate_lama/core/ingest/unit_compatibility.pyand export it fromsrc/climate_lama/core/ingest/__init__.py. - Check:
assert_ingest_unit_compatible(HazardType.<PERIL>)no longer raises for the "no ingestor" reason. A peril with no registered ingestor is refused regardless ofindicator, because there is no declared unit for the flag to be about.
A peril whose ingest source is NetCDF, not GeoTIFF. Every registered
ingestor today subclasses BaseGeoTIFFIngestor, whose format_name is
"geotiff" — so resolve_ingest_adapter
(src/climate_lama/worker/ingest/adapters.py) resolves every hazard type
to GeoTIFFIngestAdapter by walking that one-hop HazardType -> ingestor ->
format_name chain. That breaks the moment one peril needs a NetCDF source
(e.g. wildfire FWI via CDS/EWDS) while its BaseGeoTIFFIngestor subclass
still declares "geotiff" for its GeoTIFF path — the chain can only ever
answer one format per peril. Flipping format_name to "netcdf" is
explicitly the wrong fix: it commits the whole hazard type to one format
and breaks any existing GeoTIFF ingest of it.
The fix (#706) is a per-job override, not a peril-level one:
resolve_ingest_adapter(haz_type, source_format=...) takes an optional
source_format that, when set, is looked up directly in
INGEST_ADAPTER_REGISTRY and wins over the peril chain; omitted, resolution
is unchanged. It is threaded end to end — ingest_hazard's params dict
("source_format": "netcdf") -> build_ingest_chord /
build_ingest_resume_chord -> the plan_chunks Celery task -> this
override. A caller staging a .nc file for a GeoTIFF-registered peril sets
it; every other caller leaves it None and gets today's behaviour
unchanged. Downstream, the read step already prefers a planned chunk's own
"format" marker (resolve_adapter_for_chunk) over the peril chain, so once
planning resolves the right adapter the rest of the chord — and any packs
that dispatch a specific NetCDF product — need no further wiring for this
gap specifically.
3. Curves — or a documented absence¶
For a computable peril:
- Add the curve(s) to
_BUILTIN_FUNCTIONSinsrc/climate_lama/core/impact_function_seeder.py, withintensity_unitmatching the ingestor's, pluslicense/citation/source_url. - Add each new curve's name to
_BUILTIN_EXPOSURE_TYPESin the same module — this is what makes the (peril × exposure type) matrix real.tests/test_core/test_eai_eligibility.pyfails if the two tables drift. - Check:
seeded_units_for(HazardType.<PERIL>)contains the ingestor's unit; aPOST /v1/compute/impactagainst a dataset of that peril is accepted.
For an indicator peril: seed nothing, and record why in the ingestor's
module docstring, citing the search that closed. core/ingest/wildfire.py is
the worked example.
- Check:
POST /v1/compute/impactagainst a dataset of that peril returns422 E_INDICATOR_DATASET_NO_EAI.
4. Score scheme¶
- Seed a versioned
score_schemesrow (10 bands, contiguous, RAG-tiered, citation required) in an Alembic revision — precedent: migration0057. - Classify the peril in
_BAND_SOURCE_BY_HAZARDinsrc/climate_lama/core/scoring.py. That table is exhaustive overHazardTypeby design: an unclassified peril raises rather than defaulting. Point-read for every peril wired so far (ADR-073, #924): the discontinuous fields (flood, wildfire, coastal, earthquake) because a hex aggregate is a different answer at sub-cell scale, and the smooth ones (windstorm, TC, heatwave) because they are indicator perils whose impact runs are refused, so the cellmetricsthey were classified to read are never written by any code path. Classify a new peril toCELL_METRIConly if a writer actually emits its aggregate. - Check:
GET /v1/risk/lookupreturns abandfor the peril withscore_bands_enabledon, and nono_scheme_for_hazardreason. Before any dataset is catalogued the card reportsno_catalogued_dataset(#933), notno_scheme_for_hazard— so this check is only meaningful once step 1 has landed a dataset.
Mandatory for an indicator peril: the band is its user-visible output.
5. LayerSpec¶
- Add an entry to
_HAZARD_LAYER_SPECSinsrc/climate_lama/models/layer_spec.py— layer type, colormap, opacity, tooltip fields. - Check:
layer_specs_for_hazard_datasetreturns a non-empty list for a dataset of that peril, and the map renders it.
6. Chunk budget¶
- If the peril's rasters are materially larger or smaller than the default, add
an entry to
hazard_ingest_chunk_size_bytes_per_haz_typeinsrc/climate_lama/config.py; otherwisehazard_ingest_chunk_size_bytes_defaultapplies. Watchhazard_ingest_max_grid_cellsfor global-extent products. - Check: a full-extent ingest completes without an
E_INGEST_PLAN_REJECTED.
7. Engine code¶
- Add the peril to
EngineAdapter.supported_hazards. This list, not the engine, is the gate. An entry here is a claim that the peril computes end to end; there is no fallback path to a heavier runtime (ADR-024), so a job for an unlisted peril fails fast. - Check the two-letter code from step 1 against
climate_lama_engine.HAZARD_CODESand reuse the documented spelling if the peril is in there. That table is descriptive, not enforcing — the engine carrieshaz_typeas a free-form tag and never branches on it, so an undocumented code computes exactly like a documented one. If the peril is absent, open an engine-repo issue to add the spelling, but do not treat that as a blocker: it never gates compute. (Getting this backwards cost earthquake a release cycle — see the Update on ADR-060 in DECISIONS.md and climate-lama-engine#30.) - What does block compute sits upstream of the engine: a seeded curve in
impact_function_seeder.pywhoseintensity_unitmatches the ingestor's. Without one,core/eai_eligibility.pyrefuses with422 E_INDICATOR_DATASET_NO_EAIand the job never reaches the adapter — ship the peril as an indicator until the curve lands, and leave it out ofsupported_hazardsso the omission stays truthful. - Check: an impact job for the peril reaches the engine adapter without an unknown-hazard error, and returns a finite EAI.
8. Tests: delete the peril's exemption¶
tests/hazard_wiring.pylists enum-only perils inUNWIRED_HAZARD_TYPES, each beside the issue that wires it. Removing the entry is part of wiring the peril —tests/test_hazard_wiring.pyfails if a listed peril turns out to be wired, so the list cannot rot in either direction.- Check: the full suite passes with the entry deleted.
9. UI note¶
- The UI reads
indicatorfromGET /v1/risk/lookup,GET /v1/risk/*rollups andGET /v1/hazards, and must label an indicator peril as a hazard-score layer with no monetary loss attached. Raise the UI-repo issue as part of the peril's wave; the backbone flag is additive, so an un-updated UI degrades to showing the score card without the label rather than to showing a wrong number. - Check: the peril appears on the score card with the indicator label, and no EAI figure is offered for it.
Where the mechanism lives¶
| Concern | File |
|---|---|
| Posture derivation | src/climate_lama/core/eai_eligibility.py |
| Grade declaration | src/climate_lama/core/eai_eligibility.py |
| Ingest gate | src/climate_lama/core/ingest/unit_compatibility.py |
| Seeded curves + exposure types | src/climate_lama/core/impact_function_seeder.py |
| Compute gate | src/climate_lama/core/compute_service.py |
| Score-card flag | src/climate_lama/core/lookup_service.py |
| Rollup flag | src/climate_lama/core/rollup_service.py |