What you will learn:
Locate a primary earthquake event by bounding box and hazard code.
Retrieve aftershocks within a time window using USGS + GDACS hazard collections.
Find triggered landslides via secondary hazard codes (
LS,GH0301).Query impact records (displacement, damages) filtered by country codes (
TUR,SYR).Build a multi-layer Folium map with simulated intensity contours and colour-coded legend.
Environment Setup¶
All dependencies are pre-installed. Click Launch Binder at the top of the page — no setup needed.
Run this in the first code cell before anything else:
!pip install -q pystac-client pandas folium geopandas
import os; from getpass import getpass
if 'MONTANDON_API_TOKEN' not in os.environ:
os.environ['MONTANDON_API_TOKEN'] = getpass('API token: ')pip install -e . # from repo root
export MONTANDON_API_TOKEN='your_token_here'
jupyter labSee Getting Started for token instructions.
# Import required libraries
import os
from getpass import getpass
import pystac_client
import pystac
import pandas as pd
import geopandas as gpd
import folium
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
# ============================================================================
# AUTHENTICATION & CONNECTION TO MONTANDON STAC API
# ============================================================================
# Montandon STAC API URL (CORRECT URL with /stac suffix)
STAC_API_URL = "https://montandon-eoapi-stage.ifrc.org/stac"
# First try to get token from environment variable
api_token = os.getenv('MONTANDON_API_TOKEN')
# If not set, prompt user to enter token
if api_token is None:
print("=" * 70)
print("AUTHENTICATION REQUIRED")
print("=" * 70)
print("\nThe Montandon STAC API requires a Bearer Token for authentication.")
print("\nHow to get your token:")
print(" 1. Visit: https://goadmin-stage.ifrc.org/")
print(" 2. Log in with your IFRC credentials")
print(" 3. Generate an API token from your account settings")
print("\nAlternatively, set the MONTANDON_API_TOKEN environment variable:")
print(" PowerShell: $env:MONTANDON_API_TOKEN = 'your_token_here'")
print(" Bash: export MONTANDON_API_TOKEN='your_token_here'")
print("\n" + "=" * 70)
api_token = getpass("Enter your Montandon API Token: ")
# Create authentication headers for pystac_client
AUTH_HEADERS = {
"Authorization": f"Bearer {api_token}"
}
# Connect to the STAC API using pystac_client
try:
catalog = pystac_client.Client.open(STAC_API_URL, headers=AUTH_HEADERS)
print(f"Connected to: {catalog.title}")
collections = list(catalog.get_collections())
print(f"Available collections: {len(collections)}")
except Exception as e:
print(f"Connection failed: {e}")
catalog = None======================================================================
AUTHENTICATION REQUIRED
======================================================================
The Montandon STAC API requires a Bearer Token for authentication.
How to get your token:
1. Visit: https://goadmin-stage.ifrc.org/
2. Log in with your IFRC credentials
3. Generate an API token from your account settings
Alternatively, set the MONTANDON_API_TOKEN environment variable:
PowerShell: $env:MONTANDON_API_TOKEN = 'your_token_here'
Bash: export MONTANDON_API_TOKEN='your_token_here'
======================================================================
Connected to: stac-fastapi
Connected to: stac-fastapi
Available collections: 29
Available collections: 29
Step 1 — Find the Primary Event (Earthquake)¶
We search for the M 7.8 earthquake of 6 February 2023 Goldberg et al. (2023)
using a union of hazard codes from UNDRR-ISC 2025 (GH0101), GLIDE (EQ),
and EM-DAT (nat-geo-ear-gro).
# Define earthquake hazard codes
EARTHQUAKE_HAZARD_CODES = [
"GH0101", # UNDRR-ISC 2025: Earthquake (Seismic cluster)
"EQ", # GLIDE
"nat-geo-ear-gro", # EM-DAT: Earthquake > Ground movement
"GH0001", # UNDRR-ISC 2020: Earthquake
"GH0002", # UNDRR-ISC 2020: Ground Shaking
]
# Define search parameters
target_date = "2023-02-06"
start_datetime = f"{target_date}T00:00:00Z"
end_datetime = f"{target_date}T23:59:59Z"
# Bounding box for Turkey-Syria earthquake region - EXPANDED
# [west, south, east, north] - larger area to capture more aftershocks
TURKEY_SYRIA_BBOX = [34.0, 35.5, 40.0, 39.0] # Expanded from [35.0, 36.0, 39.0, 38.5]
# Aftershock search duration (days after main event)
AFTERSHOCK_DAYS = 14 # Extended from 5 days to capture more aftershocks
print(f"Search Configuration:")
print(f" Date: {target_date}")
print(f" Bounding Box: {TURKEY_SYRIA_BBOX}")
print(f" Aftershock window: {AFTERSHOCK_DAYS} days")
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def get_magnitude(item: pystac.Item) -> Optional[float]:
"""
Extract magnitude from eq:magnitude or monty:hazard_detail.severity_value
"""
mag = item.properties.get("eq:magnitude")
if mag is not None:
return mag
hazard_detail = item.properties.get("monty:hazard_detail")
if hazard_detail and isinstance(hazard_detail, dict):
mag = hazard_detail.get("severity_value")
if mag is not None:
return mag
return None
def get_monty_correlation_id(item: pystac.Item) -> Optional[str]:
"""Extract the correlation ID from a STAC item."""
return item.properties.get("monty:correlation_id")
def get_monty_hazard_detail(item: pystac.Item) -> Dict[str, Any]:
"""Extract hazard detail from a STAC Item."""
return item.properties.get("monty:hazard_detail", {})
def get_monty_impact_detail(item: pystac.Item) -> Dict[str, Any]:
"""Extract impact detail from a STAC Item."""
return item.properties.get("monty:impact_detail", {})Search Configuration:
Date: 2023-02-06
Bounding Box: [34.0, 35.5, 40.0, 39.0]
Aftershock window: 14 days
# Search for primary earthquake events using BBOX (more reliable than country codes)
# Uses authenticated catalog connection
if not catalog:
print("No catalog connection. Please check authentication.")
usgs_hazards = []
gdacs_emdat_events = []
primary_events = []
events_with_mag = []
else:
# Search USGS hazards with BBOX
usgs_search = catalog.search(
collections=["usgs-hazards"],
bbox=TURKEY_SYRIA_BBOX,
datetime=f"{start_datetime}/{end_datetime}",
max_items=100
)
usgs_hazards = list(usgs_search.items())
# Search GDACS and EMDAT with BBOX
gdacs_emdat_search = catalog.search(
collections=["gdacs-events", "emdat-events", "gdacs-hazards", "emdat-hazards"],
bbox=TURKEY_SYRIA_BBOX,
datetime=f"{start_datetime}/{end_datetime}",
max_items=50
)
gdacs_emdat_events = list(gdacs_emdat_search.items())
# Combine all events
primary_events = usgs_hazards + gdacs_emdat_events
print(f"Found {len(usgs_hazards)} USGS + {len(gdacs_emdat_events)} GDACS/EMDAT = {len(primary_events)} total events")
# Filter events with magnitude
events_with_mag = []
for item in primary_events:
mag = get_magnitude(item)
if mag is not None and mag > 0:
events_with_mag.append(item)
if not events_with_mag:
events_with_mag = primary_eventsFound 12 USGS + 16 GDACS/EMDAT = 28 total events
# Select the event with the highest magnitude
if events_with_mag:
main_event = max(events_with_mag, key=lambda x: get_magnitude(x) or 0)
main_magnitude = get_magnitude(main_event) or 0
# Extract earthquake details
eq_depth = main_event.properties.get("eq:depth")
eq_alert = main_event.properties.get("eq:alert")
eq_tsunami = main_event.properties.get("eq:tsunami")
print(f"Primary Event: {main_event.properties.get('title')}")
print(f" Magnitude: {main_magnitude} | Depth: {eq_depth or 'N/A'} km | Alert: {eq_alert or 'N/A'}")
# Set up aftershock search window - uses AFTERSHOCK_DAYS variable
event_time = pd.to_datetime(main_event.datetime)
aftershock_start = event_time + timedelta(minutes=10)
aftershock_end = event_time + timedelta(days=AFTERSHOCK_DAYS)
print(f" Aftershock window: {aftershock_start.date()} to {aftershock_end.date()}")
else:
print("No events found.")
main_event = None
main_magnitude = 0
eq_depth = None
eq_alert = None
eq_tsunami = NonePrimary Event: M 7.8 - Pazarcik earthquake, Kahramanmaras earthquake sequence
Magnitude: 7.8 | Depth: 10.0 km | Alert: N/A
Aftershock window: 2023-02-06 to 2023-02-20
# Known epicenters (matching the helper function)
known_epicenters = {
"us6000jllz": [37.014, 37.226], # 7.8M Pazarcik
"us6000jlqa": [37.203, 38.024], # 7.5M Elbistan
}
Step 2 — Find Secondary Hazards (Aftershocks & Landslides)¶
Subsequent earthquakes (GH0101) within a 30-day window after the
main event. We search both usgs-hazards and gdacs-hazards
to maximise coverage.
Mass movements triggered by the shaking, identified via codes
LS (GLIDE), GH0301 (UNDRR-ISC), and
nat-geo-mmd-lan / nat-hyd-mmw-lan (EM-DAT).
# --- 2a. Aftershocks ---
# Increase max_items to capture more aftershocks
usgs_aftershock_search = catalog.search(
collections=["usgs-hazards"],
bbox=TURKEY_SYRIA_BBOX,
datetime=f"{aftershock_start.isoformat()}/{aftershock_end.isoformat()}",
max_items=2000 # Increased from 500 to capture more aftershocks
)
usgs_aftershocks = list(usgs_aftershock_search.items())
print(f"Found {len(usgs_aftershocks)} USGS aftershocks")Found 14 USGS aftershocks
# Search GDACS hazards for additional aftershocks
gdacs_aftershock_search = catalog.search(
collections=["gdacs-hazards"],
bbox=TURKEY_SYRIA_BBOX,
datetime=f"{aftershock_start.isoformat()}/{aftershock_end.isoformat()}",
max_items=2000 # Increased from 500
)
gdacs_aftershocks = list(gdacs_aftershock_search.items())
# Combine all aftershocks
aftershocks = usgs_aftershocks + gdacs_aftershocks
print(f"Aftershocks: {len(usgs_aftershocks)} USGS + {len(gdacs_aftershocks)} GDACS = {len(aftershocks)} total")
# --- 2b. Landslides ---
LANDSLIDE_HAZARD_CODES = [
"GH0007", # UNDRR-ISC 2020: Landslide (Earthquake Trigger)
"GH0301", # UNDRR-ISC 2025: Landslide
"LS", # GLIDE
"nat-hyd-mmw-lan", # EM-DAT: Hydrological > Mass movement (wet) > Landslide
"nat-geo-mmd-lan" # EM-DAT: Geophysical > Mass movement (dry) > Landslide
]
landslide_code_filters = [{"op": "a_contains", "args": [{"property": "monty:hazard_codes"}, code]} for code in LANDSLIDE_HAZARD_CODES]
landslide_filter = {
"op": "and",
"args": [
{
"op": "or",
"args": landslide_code_filters
},
{
"op": "t_intersects",
"args": [{"property": "datetime"}, {"interval": [aftershock_start.isoformat(), aftershock_end.isoformat()]}]
}
]
}
search_landslides = catalog.search(
collections=["usgs-hazards", "gdacs-hazards", "emdat-hazards"],
bbox=TURKEY_SYRIA_BBOX,
filter=landslide_filter,
filter_lang="cql2-json",
max_items=100
)
landslides = list(search_landslides.items())
print(f"Found {len(landslides)} landslides in the region")Aftershocks: 14 USGS + 7 GDACS = 21 total
Found 0 landslides in the region
Step 3 — Find Cascading Impacts (Displacement & Damages)¶
Impact records for Türkiye (TUR) and Syria (SYR)
are queried from IDMC, EM-DAT, and GDACS impact collections using
a_contains on monty:country_codes.
# Impact search window
impact_end = event_time + timedelta(days=30)
impact_filter = {
"op": "and",
"args": [
# Filter by Country Codes
{
"op": "or",
"args": [
{"op": "a_contains", "args": [{"property": "monty:country_codes"}, "TUR"]},
{"op": "a_contains", "args": [{"property": "monty:country_codes"}, "SYR"]}
]
},
# Filter by Time
{
"op": "t_intersects",
"args": [{"property": "datetime"}, {"interval": [start_datetime, impact_end.isoformat()]}]
}
]
}
search_impacts = catalog.search(
collections=["idmc-idu-impacts", "emdat-impacts", "gdacs-impacts"],
bbox=TURKEY_SYRIA_BBOX,
filter=impact_filter,
filter_lang="cql2-json",
max_items=100
)
impacts = list(search_impacts.items())
print(f"Found {len(impacts)} impact records")Found 100 impact records
Step 4 — Multi-Layer Map Visualisation¶
The map overlays four data layers with a colour-coded legend:
| Layer | Colour | Symbol |
|---|---|---|
| Main / Major Event (M ≥ 7) | Red | ★ Star |
| Aftershocks | Orange | ● Circle (scaled by magnitude) |
| Landslides | Green | 🍃 Leaf |
| Impacts | Blue | ℹ Info |
# Helper to get coordinates from geometry (Point or Polygon)
def get_event_coordinates(item):
"""
Extract coordinates from STAC item.
For known major earthquakes, uses verified epicenter coordinates instead of polygon centroids.
"""
if not item.geometry:
return None
# MANUAL OVERRIDE: Known epicenters for Turkey-Syria 2023 earthquakes
# These are scientifically verified coordinates, more accurate than ShakeMap polygon centroids
known_epicenters = {
"us6000jllz": [37.014, 37.226], # 7.8M Pazarcik/Kahramanmaras - [lon, lat]
"us6000jlqa": [37.203, 38.024], # 7.5M Elbistan - [lon, lat]
}
# Check if this is one of the known events
for event_id, coords in known_epicenters.items():
if event_id in item.id.lower():
return coords
# Fall back to geometry-based extraction
geom_type = item.geometry.get("type")
coords = item.geometry.get("coordinates")
if not coords:
return None
if geom_type == "Point":
return coords
elif geom_type == "Polygon":
# For polygons, calculate centroid (simple average of outer ring)
# Note: This may not be accurate for large ShakeMap coverage areas
try:
outer_ring = coords[0]
lons = [p[0] for p in outer_ring]
lats = [p[1] for p in outer_ring]
centroid_lon = sum(lons) / len(lons)
centroid_lat = sum(lats) / len(lats)
return [centroid_lon, centroid_lat]
except (IndexError, TypeError):
return None
return None# Helper function to add simulated intensity contours
def add_intensity_contours(map_obj, coords, magnitude):
# Define zones (Color, Radius Multiplier in km)
zones = [
("red", 40), # Severe Shaking
("orange", 80), # Strong Shaking
("yellow", 160) # Moderate Shaking
]
# Adjust radii based on magnitude relative to M7.0
# Ensure a minimum scale to avoid tiny circles for smaller major events
scale_factor = max(magnitude / 7.0, 0.5)
for color, base_radius in zones:
radius_km = base_radius * scale_factor
folium.Circle(
location=[coords[1], coords[0]],
radius=radius_km * 1000, # Meters
color=color,
weight=2,
fill=True,
fill_color=color,
fill_opacity=0.15, # Slightly more transparent to handle overlap
popup=f"Simulated Intensity Zone (~{int(radius_km)} km) for M{magnitude}"
).add_to(map_obj)
# Initialize Map centered on the main event
main_coords = get_event_coordinates(main_event)
if main_coords:
# Folium uses [lat, lon]
m = folium.Map(location=[main_coords[1], main_coords[0]], zoom_start=6)
# Add contours for Main Event
add_intensity_contours(m, main_coords, main_magnitude)
else:
print("Warning: Could not determine main event coordinates. Defaulting to Turkey.")
m = folium.Map(location=[37.0, 37.0], zoom_start=6)
# Create a detailed popup for the main event
main_popup_html = f"""<div style='width: 200px'>
<b>Main Earthquake Event</b><br>
<b>Magnitude:</b> {main_magnitude}<br>
<b>Date:</b> {main_event.datetime}<br>
<b>Depth:</b> {eq_depth if eq_depth else 'N/A'} km<br>
<b>Alert:</b> {eq_alert if eq_alert else 'N/A'}<br>
<b>Tsunami:</b> {'Yes' if eq_tsunami == 1 else 'No' if eq_tsunami == 0 else 'N/A'}<br>
<b>ID:</b> {main_event.id}
</div>"""
# Add Main Event Marker
if main_coords:
folium.Marker(
[main_coords[1], main_coords[0]],
popup=folium.Popup(main_popup_html, max_width=250),
tooltip=f"Main Event - Magnitude {main_magnitude}",
icon=folium.Icon(color="red", icon="star")
).add_to(m)
# Initialize counters
major_event_count = 0
aftershock_count = 0
# Add Aftershocks and Major Events
print("Processing aftershocks...")
for item in aftershocks:
coords = get_event_coordinates(item)
if coords:
mag = get_magnitude(item)
if mag is None:
continue
# Check for Major Event (e.g., M >= 7.0)
if mag >= 7.0:
major_event_count += 1
# Add Marker
popup_html = f"""<div style='width: 150px'>
<b>Major Event</b><br>
<b>Magnitude:</b> {mag}<br>
<b>Date:</b> {item.datetime}
</div>"""
folium.Marker(
[coords[1], coords[0]],
popup=folium.Popup(popup_html, max_width=200),
tooltip=f"Major Event - Mag {mag}",
icon=folium.Icon(color="red", icon="star")
).add_to(m)
# Add Contours for Major Event
add_intensity_contours(m, coords, mag)
else:
aftershock_count += 1
popup_html = f"""<div style='width: 150px'>
<b>Aftershock</b><br>
<b>Magnitude:</b> {mag}<br>
<b>Date:</b> {item.datetime}
</div>"""
folium.CircleMarker(
[coords[1], coords[0]],
radius=max(mag * 1.5, 3), # Size by magnitude, min 3
color="orange",
fill=True,
fill_opacity=0.6,
popup=folium.Popup(popup_html, max_width=200),
tooltip=f"Aftershock - Mag {mag}"
).add_to(m)
print(f"Map shows {major_event_count} major events (M>=7.0) and {aftershock_count} aftershocks")
# Add Landslides
for item in landslides:
coords = get_event_coordinates(item)
if coords:
popup_html = f"""<div style='width: 150px'>
<b>Landslide</b><br>
<b>Date:</b> {item.datetime}<br>
<b>Location:</b> {item.properties.get('title', 'N/A')}
</div>"""
folium.Marker(
[coords[1], coords[0]],
popup=folium.Popup(popup_html, max_width=200),
tooltip="Landslide",
icon=folium.Icon(color="green", icon="leaf")
).add_to(m)
# Add Impacts
for item in impacts:
coords = get_event_coordinates(item)
if coords:
detail = item.properties.get("monty:impact_detail", {})
impact_type = detail.get('type', 'Unknown')
impact_value = detail.get('value', 'N/A')
impact_unit = detail.get('unit', '')
popup_html = f"""<div style='width: 150px'>
<b>Impact</b><br>
<b>Type:</b> {impact_type}<br>
<b>Value:</b> {impact_value} {impact_unit}
</div>"""
folium.Marker(
[coords[1], coords[0]],
popup=folium.Popup(popup_html, max_width=200),
tooltip=f"{impact_type}: {impact_value} {impact_unit}",
icon=folium.Icon(color="blue", icon="info-sign")
).add_to(m)
# Add a legend
legend_html = '''<div style="position: fixed;
bottom: 50px; right: 50px; width: 220px; height: auto;
background-color: white; z-index:9999; font-size:14px;
border:2px solid grey; border-radius: 5px; padding: 10px">
<p style="margin-bottom: 5px;"><b>Legend</b></p>
<p style="margin: 3px;"><span style="color: red;">★</span> Main/Major Event (M7+)</p>
<p style="margin: 3px;"><span style="color: red; opacity: 0.5;">●</span> Severe Shaking Zone</p>
<p style="margin: 3px;"><span style="color: orange; opacity: 0.5;">●</span> Strong Shaking Zone</p>
<p style="margin: 3px;"><span style="color: yellow; opacity: 0.5;">●</span> Moderate Shaking Zone</p>
<p style="margin: 3px;"><span style="color: orange;">●</span> Aftershocks</p>
<p style="margin: 3px;"><span style="color: green;">🍃</span> Landslides</p>
<p style="margin: 3px;"><span style="color: blue;">ⓘ</span> Impacts</p>
</div>'''
Processing aftershocks...
Map shows 1 major events (M>=7.0) and 20 aftershocks
m.get_root().html.add_child(folium.Element(legend_html))
# Display Map
m- Goldberg, D. E., Taymaz, T., Reitman, N. G., & others. (2023). Rapid Characterization of the February 2023 Kahramanmaraş, Türkiye, Earthquake Sequence. The Seismological Research Letters, 94(4), 1474–1490. 10.1785/0220230113
- U.S. Geological Survey. (2024). USGS Earthquake Hazards Program. https://earthquake.usgs.gov
- European Commission Joint Research Centre. (2024). Global Disaster Alert and Coordination System (GDACS). https://www.gdacs.org
- Guha-Sapir, D. (2024). EM-DAT: The Emergency Events Database. Centre for Research on the Epidemiology of Disasters (CRED). https://www.emdat.be
- Internal Displacement Monitoring Centre. (2024). IDMC Global Internal Displacement Database. https://www.internal-displacement.org