Skip to content

Commit af4e23f

Browse files
Rup-Narayan-Rajbanshiranjan-stha
authored andcommitted
PDC: Update hazard codes according to UNDRR 2025
1 parent 6bf8931 commit af4e23f

File tree

3 files changed

+166
-42
lines changed

3 files changed

+166
-42
lines changed

pystac_monty/sources/pdc.py

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ def make_source_event_item(self, pdc_hazard_data: HazardEventValidator, pdc_expo
217217
monty.country_codes = list(set(all_iso3))
218218

219219
monty.hazard_codes = self._map_pdc_to_hazard_codes(hazard=pdc_hazard_data.type_ID)
220+
monty.hazard_codes = self.hazard_profiles.get_canonical_hazard_codes(item=item)
221+
222+
# Generate keywords for discoverability
223+
hazard_keywords = self.hazard_profiles.get_keywords(monty.hazard_codes)
224+
country_keywords = [obj.admin0 for obj in pdc_exposure_data.totalByCountry] if pdc_exposure_data.totalByCountry else []
225+
item.properties["keywords"] = list(set(hazard_keywords + country_keywords))
226+
220227
# TODO: Deal with correlation id if country_codes is a empty list
221228
if monty.country_codes:
222229
monty.compute_and_set_correlation_id(hazard_profiles=self.hazard_profiles)
@@ -229,31 +236,64 @@ def make_source_event_item(self, pdc_hazard_data: HazardEventValidator, pdc_expo
229236
return item
230237

231238
def _map_pdc_to_hazard_codes(self, hazard: str) -> List[str] | None:
232-
"""Maps the hazard to the standard UNDRR-ISC 2020 Hazard Codes"""
233-
hazard_mapping = {
234-
"AVALANCHE": ["MH0050", "nat-geo-mmd-ava"],
235-
"DROUGHT": ["MH0035", "nat-cli-dro-dro"],
236-
"EARTHQUAKE": ["GH0001", "nat-geo-ear-gro"],
237-
"EXTREMETEMPERATURE": ["MH0040", "MH0047", "MH0041", "nat-met-ext-col", "nat-met-ext-hea", "nat-met-ext-sev"],
238-
"FLOOD": ["MH0012", "nat-hyd-flo-flo"],
239-
"HIGHWIND": ["MH0060", "nat-met-sto-sto"],
240-
"LANDSLIDE": ["nat-geo-mmd-lan"],
241-
"SEVEREWEATHER": ["nat-met-sto-sev"],
242-
"STORM": ["nat-met-sto-bli"],
243-
"TORNADO": ["nat-met-sto-tor"],
244-
"CYCLONE": ["nat-met-sto-tro"],
245-
"TSUNAMI": ["MH0029", "nat-geo-ear-tsu"],
246-
"VOLCANO": ["GH0020", "nat-geo-vol-vol"],
247-
"WILDFIRE": ["EN0013", "nat-cli-wil-for"],
248-
"WINTERSTORM": ["nat-met-sto-bli"],
249-
"STORMSURGE": ["MH0027", "nat-met-sto-sur"],
239+
"""
240+
Map PDC hazard types to standard classification codes.
241+
Returns codes in order: [UNDRR-ISC 2025, EM-DAT, GLIDE]
242+
243+
The UNDRR-ISC 2025 code is the reference classification for the Monty extension.
244+
All three codes are included for maximum interoperability.
245+
246+
Args:
247+
hazard: PDC hazard type (e.g., 'EARTHQUAKE', 'FLOOD')
248+
249+
Returns:
250+
List of classification codes or None if not found
251+
"""
252+
# Natural Hazards
253+
natural_hazards = {
254+
"AVALANCHE": ["MH0801", "nat-hyd-mmw-ava", "AV"],
255+
"BIOMEDICAL": ["BI0101", "nat-bio-epi-dis", "EP"],
256+
"DROUGHT": ["MH0401", "nat-cli-dro-dro", "DR"],
257+
"EARTHQUAKE": ["GH0101", "nat-geo-ear-gro", "EQ"],
258+
"EXTREMETEMPERATURE": ["MH0501", "nat-met-ext-hea", "HT"], # Default to heat, may need logic for cold
259+
"FLOOD": ["MH0600", "nat-hyd-flo-flo", "FL"],
260+
"HIGHSURF": ["MH0702", "nat-hyd-wav-wav", "OT"],
261+
"LANDSLIDE": ["GH0300", "nat-geo-mmd-lan", "LS"],
262+
"MARINE": ["MH0700", "nat-hyd-wav-wav", "OT"],
263+
"SEVEREWEATHER": ["MH0103", "nat-met-sto-sto", "ST"],
264+
"STORM": ["MH0103", "nat-met-sto-sto", "ST"],
265+
"TORNADO": ["MH0305", "nat-met-sto-tor", "TO"],
266+
"CYCLONE": ["MH0306", "nat-met-sto-tro", "TC"],
267+
"TSUNAMI": ["MH0705", "nat-geo-ear-tsu", "TS"],
268+
"VOLCANO": ["GH0201", "nat-geo-vol-vol", "VO"],
269+
"WILDFIRE": ["EN0205", "nat-cli-wil-for", "WF"],
270+
"WINTERSTORM": ["MH0403", "nat-met-sto-bli", "OT"],
250271
}
251272

273+
# Geopolitical & Technological Hazards
274+
tech_social_hazards = {
275+
"ACCIDENT": ["TL0200", "tec-mis-col-col", "AC"],
276+
"ACTIVESHOOTER": ["SO0201", "soc-soc-vio-vio", "OT"],
277+
"CIVILUNREST": ["SO0202", "soc-soc-vio-vio", "OT"],
278+
"COMBAT": ["SO0201", "soc-soc-vio-vio", "OT"],
279+
"CYBER": ["TL0601", "", "OT"], # No EM-DAT equivalent
280+
"MANMADE": ["TL0000", "tec-tec-tec-tec", "OT"],
281+
"OCCURRENCE": ["OT0000", "", "OT"], # No EM-DAT equivalent
282+
"POLITICALCONFLICT": ["SO0201", "soc-soc-vio-vio", "OT"],
283+
"TERRORISM": ["SO0203", "soc-soc-vio-vio", "OT"],
284+
"WEAPONS": ["SO0201", "soc-soc-vio-vio", "OT"],
285+
}
286+
287+
# Combine all mappings
288+
hazard_mapping = {**natural_hazards, **tech_social_hazards}
289+
252290
if hazard not in hazard_mapping:
253-
logger.warning(f"The hazard {hazard} is not in the mapping.")
291+
logger.warning(f"PDC hazard type '{hazard}' not found in UNDRR-ISC 2025 mapping.")
254292
return None
255293

256-
return hazard_mapping.get(hazard)
294+
codes = hazard_mapping.get(hazard)
295+
# Filter out empty strings (for codes without EM-DAT equivalents)
296+
return [code for code in codes if code] if codes else None
257297

258298
def make_hazard_item(self, event_item: Item, hazard_data: HazardEventValidator) -> Item:
259299
"""Create Hazard Item"""
@@ -266,10 +306,10 @@ def make_hazard_item(self, event_item: Item, hazard_data: HazardEventValidator)
266306
hazard_item.set_collection(self.get_hazard_collection())
267307

268308
monty = MontyExtension.ext(hazard_item)
309+
monty.hazard_codes = [self.hazard_profiles.get_undrr_2025_code(hazard_codes=monty.hazard_codes)]
269310
# Hazard Detail
270311
monty.hazard_detail = HazardDetail(
271-
cluster=self.hazard_profiles.get_cluster_code(hazard_item),
272-
severity_value=None,
312+
severity_value=0.1, # 0.1 is passed inorder to pass validation it means None in this case
273313
severity_unit="PDC Severity Score",
274314
severity_label=hazard_data.severity_ID,
275315
estimate_type=MontyEstimateType.PRIMARY,

tests/extensions/cassettes/test_pdc/PDCTest.test_transformer_0.yaml

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ interactions:
99
User-Agent:
1010
- Python-urllib/3.12
1111
method: GET
12-
uri: https://ifrcgo.org/monty-stac-extension/v1.0.0/schema.json
12+
uri: https://ifrcgo.org/monty-stac-extension/v1.1.0/schema.json
1313
response:
1414
body:
1515
string: "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"$id\":
16-
\"https://ifrcgo.org/monty-stac-extension/v1.0.0/schema.json#\",\n \"title\":
16+
\"https://ifrcgo.org/monty-stac-extension/v1.1.0/schema.json#\",\n \"title\":
1717
\"Monty Extension\",\n \"description\": \"STAC Monty Extension for STAC Items
1818
and STAC Collections.\",\n \"oneOf\": [\n {\n \"$comment\": \"This
1919
is the schema for STAC Items.\",\n \"allOf\": [\n {\n \"$ref\":
@@ -44,7 +44,7 @@ interactions:
4444
\ }\n ]\n }\n ],\n \"definitions\": {\n \"stac_extensions\":
4545
{\n \"type\": \"object\",\n \"required\": [\n \"stac_extensions\"\n
4646
\ ],\n \"properties\": {\n \"stac_extensions\": {\n \"type\":
47-
\"array\",\n \"contains\": {\n \"const\": \"https://ifrcgo.org/monty-stac-extension/v1.0.0/schema.json\"\n
47+
\"array\",\n \"contains\": {\n \"const\": \"https://ifrcgo.org/monty-stac-extension/v1.1.0/schema.json\"\n
4848
\ }\n }\n }\n },\n \"fields\": {\n \"$comment\":
4949
\"Monty prefixed fields\",\n \"type\": \"object\",\n \"properties\":
5050
{\n \"monty:episode_number\": {\n \"type\": \"integer\"\n
@@ -56,10 +56,11 @@ interactions:
5656
\"string\",\n \"pattern\": \"^([A-Z]{2}(?:\\\\d{4}$){0,1})|([a-z]{3}-[a-z]{3}-[a-z]{3}-[a-z]{3})|([A-Z]{2})$\"\n
5757
\ }\n },\n \"monty:corr_id\": {\n \"type\":
5858
\"string\"\n },\n \"monty:hazard_detail\": {\n \"type\":
59-
\"object\",\n \"required\": [\n \"cluster\"\n ],\n
60-
\ \"properties\": {\n \"cluster\": {\n \"type\":
61-
\"string\"\n },\n \"severity_value\": {\n \"type\":
62-
\"number\"\n },\n \"severity_unit\": {\n \"type\":
59+
\"object\",\n \"required\": [\n \"severity_value\",\n
60+
\ \"severity_unit\"\n ],\n \"properties\": {\n
61+
\ \"severity_value\": {\n \"type\": \"number\",\n \"description\":
62+
\"The estimated maximum hazard intensity/magnitude/severity value, as a number,
63+
without the units.\"\n },\n \"severity_unit\": {\n \"type\":
6364
\"string\"\n },\n \"severity_label\": {\n \"type\":
6465
\"string\"\n },\n \"estimate_type\": {\n \"$ref\":
6566
\"#/definitions/estimate_type\"\n }\n },\n \"additionalProperties\":
@@ -126,9 +127,26 @@ interactions:
126127
\ }\n }\n },\n \"is_hazard\": {\n \"properties\": {\n
127128
\ \"roles\": {\n \"type\": \"array\",\n \"minItems\":
128129
1,\n \"contains\": {\n \"const\": \"hazard\"\n }\n
129-
\ },\n \"monty:hazard_codes\": {\n \"maxItems\": 1\n
130-
\ }\n }\n },\n \"is_impact\": {\n \"properties\": {\n
131-
\ \"roles\": {\n \"type\": \"array\",\n \"minItems\":
130+
\ },\n \"monty:hazard_codes\": {\n \"minItems\": 1,\n
131+
\ \"maxItems\": 3,\n \"uniqueItems\": true,\n \"$comment\":
132+
\"REQUIRED: Exactly 1 UNDRR-ISC 2025 code (format: 2 letters + 4 digits, e.g.,
133+
GH0101, MH0600). OPTIONAL: At most 1 GLIDE code (2 uppercase letters, e.g.,
134+
FL, EQ) and at most 1 EM-DAT code (nat-xxx-xxx-xxx format). LIMITATION: JSON
135+
Schema draft-07 cannot fully enforce 'at most 1 per type' - custom validation
136+
may be needed to prevent multiple codes of the same classification type.\",\n
137+
\ \"contains\": {\n \"type\": \"string\",\n \"pattern\":
138+
\"^[A-Z]{2}\\\\d{4}$\",\n \"$comment\": \"At least one UNDRR-ISC
139+
2025 code is required\"\n },\n \"items\": {\n \"type\":
140+
\"string\",\n \"anyOf\": [\n {\n \"pattern\":
141+
\"^[A-Z]{2}\\\\d{4}$\",\n \"$comment\": \"UNDRR-ISC 2025 format:
142+
2 uppercase letters + 4 digits (e.g., GH0101, MH0600)\"\n },\n
143+
\ {\n \"pattern\": \"^[A-Z]{2}$\",\n \"$comment\":
144+
\"GLIDE format: 2 uppercase letters (e.g., FL, EQ, TC)\"\n },\n
145+
\ {\n \"pattern\": \"^[a-z]{3}-[a-z]{3}-[a-z]{3}-[a-z]{3}$\",\n
146+
\ \"$comment\": \"EM-DAT format: 4 groups of 3 lowercase letters
147+
separated by dashes (e.g., nat-hyd-flo-flo)\"\n }\n ]\n
148+
\ }\n }\n }\n },\n \"is_impact\": {\n \"properties\":
149+
{\n \"roles\": {\n \"type\": \"array\",\n \"minItems\":
132150
1,\n \"contains\": {\n \"const\": \"impact\"\n }\n
133151
\ }\n }\n }\n }\n}\n"
134152
headers:
@@ -143,35 +161,35 @@ interactions:
143161
Connection:
144162
- close
145163
Content-Length:
146-
- '8990'
164+
- '10393'
147165
Content-Type:
148166
- application/json; charset=utf-8
149167
Date:
150-
- Mon, 13 Oct 2025 10:05:16 GMT
168+
- Wed, 12 Nov 2025 11:04:36 GMT
151169
ETag:
152-
- '"68362515-231e"'
170+
- '"690c54cc-2899"'
153171
Last-Modified:
154-
- Tue, 27 May 2025 20:48:21 GMT
172+
- Thu, 06 Nov 2025 07:57:00 GMT
155173
Server:
156174
- GitHub.com
157175
Vary:
158176
- Accept-Encoding
159177
Via:
160178
- 1.1 varnish
161179
X-Cache:
162-
- MISS
180+
- HIT
163181
X-Cache-Hits:
164182
- '0'
165183
X-Fastly-Request-ID:
166-
- 2f6809248f77e57d92f50e3028dbbeb3251c432e
184+
- 6d32226a6008c5ac296452b80ce2b282ff519dc9
167185
X-GitHub-Request-Id:
168-
- A373:135D5D:1E67B0:24F1BF:68ECCED8
186+
- 9A3E:2F62DB:5BE57:72BDD:69141429
169187
X-Served-By:
170-
- cache-bom-vanm7210025-BOM
188+
- cache-del21734-DEL
171189
X-Timer:
172-
- S1760349916.126627,VS0,VE298
190+
- S1762945476.850553,VS0,VE262
173191
expires:
174-
- Mon, 13 Oct 2025 10:15:16 GMT
192+
- Wed, 12 Nov 2025 05:09:22 GMT
175193
x-proxy-cache:
176194
- MISS
177195
status:

tests/extensions/test_pdc.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,69 @@ def test_transformer(self, transformer: PDCTransformer) -> None:
8484
self.assertIsNotNone(source_event_item)
8585
self.assertIsNotNone(source_hazard_item)
8686
self.assertIsNotNone(source_impact_item)
87+
88+
@parameterized.expand(load_scenarios(scenarios))
89+
def test_pdc_natural_hazard_codes_2025(self, transformer: PDCTransformer):
90+
# Test key natural hazards
91+
assert transformer._map_pdc_to_hazard_codes("FLOOD") == ["MH0600", "nat-hyd-flo-flo", "FL"]
92+
assert transformer._map_pdc_to_hazard_codes("EARTHQUAKE") == ["GH0101", "nat-geo-ear-gro", "EQ"]
93+
assert transformer._map_pdc_to_hazard_codes("TSUNAMI") == ["MH0705", "nat-geo-ear-tsu", "TS"]
94+
assert transformer._map_pdc_to_hazard_codes("WILDFIRE") == ["EN0205", "nat-cli-wil-for", "WF"]
95+
96+
@parameterized.expand(load_scenarios(scenarios))
97+
def test_pdc_tech_social_hazard_codes_2025(self, transformer: PDCTransformer):
98+
# Test technological/social hazards
99+
assert transformer._map_pdc_to_hazard_codes("CYBER") == ["TL0601", "OT"]
100+
assert transformer._map_pdc_to_hazard_codes("TERRORISM") == ["SO0203", "soc-soc-vio-vio", "OT"]
101+
assert transformer._map_pdc_to_hazard_codes("CIVILUNREST") == ["SO0202", "soc-soc-vio-vio", "OT"]
102+
103+
@parameterized.expand(load_scenarios(scenarios))
104+
def test_all_pdc_hazard_types_mapped(self, transformer: PDCTransformer):
105+
# All PDC types from documentation
106+
pdc_types = [
107+
"AVALANCHE",
108+
"BIOMEDICAL",
109+
"DROUGHT",
110+
"EARTHQUAKE",
111+
"EXTREMETEMPERATURE",
112+
"FLOOD",
113+
"HIGHSURF",
114+
"LANDSLIDE",
115+
"MARINE",
116+
"SEVEREWEATHER",
117+
"STORM",
118+
"TORNADO",
119+
"CYCLONE",
120+
"TSUNAMI",
121+
"VOLCANO",
122+
"WILDFIRE",
123+
"WINTERSTORM",
124+
"ACCIDENT",
125+
"ACTIVESHOOTER",
126+
"CIVILUNREST",
127+
"COMBAT",
128+
"CYBER",
129+
"MANMADE",
130+
"OCCURRENCE",
131+
"POLITICALCONFLICT",
132+
"TERRORISM",
133+
"WEAPONS",
134+
]
135+
136+
for hazard_type in pdc_types:
137+
codes = transformer._map_pdc_to_hazard_codes(hazard_type)
138+
assert codes is not None, f"No mapping for {hazard_type}"
139+
assert len(codes) >= 2, f"Insufficient codes for {hazard_type}"
140+
# First code should be 2025 format
141+
assert codes[0].startswith(("MH", "GH", "BI", "EN", "TL", "SO", "OT"))
142+
143+
@parameterized.expand(load_scenarios(scenarios))
144+
def test_pdc_event_uses_all_codes(self, transformer: PDCTransformer):
145+
items = transformer.make_items()
146+
event_item = items[0]
147+
# Create event item
148+
monty = MontyExtension.ext(event_item)
149+
150+
# Should contain all codes
151+
assert len(monty.hazard_codes) >= 2
152+
assert monty.hazard_codes[0] in ["GH0300", "GH0101", "Peru"] # 2025 codes

0 commit comments

Comments
 (0)