Add near-real-time burn severity from CIMSS BRISK - #1
Merged
Conversation
New stormscape.burn module and burn subcommand: screen the BRISK archive for fires intersecting an AOI, cache and mosaic the scenes, classify severity, and drape it over the hillshade. BRISK maps every large US fire daily from a nine-satellite dNBR composite, which is what makes it usable while a fire is still burning -- BAER soil burn severity lands days to weeks after containment, MTBS a year later. The portal's WMTS serves rendered PNG capped at zoom 7, so this reads the per-fire GeoTIFFs from the open archive behind it. Finding which fires touch an AOI costs no pixels: GDAL range-reads the headers (~60 fires in 1.4 s) and the footprints are memoised. Maps draw in the BAER class palette, banded into severity classes with the class names on the colour bar, so a stormscape map can sit beside a BAER product. That palette is read from the products themselves -- all 77 of the 2025 BAER soil-burn-severity rasters embed the same four class colours, and they are the four BRISK publishes. Validated against the BAER teams' own dNBR on the 39 fires carrying both (examples/brisk_vs_baer.py): BRISK computes the same quantity -- median slope 1.013, bias +0.003, r = 0.938 over 1.24 M cells. The disagreements are compositing latency, not a different algorithm: every poor performer read low on the BAER date and recovered to r = 0.80-0.96 given a scene 5-21 days later. So scenes now carry an age, immature composites are flagged, and --min-age turns that into a filter. dNBR is a vegetation index, not soil burn severity, and the USGS debris-flow models are calibrated on the latter -- flagged in the README, the CLI help and the module docstring. Also: drape_i15 gains norm/cbar_ticks/cbar_ticklabels for classed maps; export learns that severity is categorical and scales burn fields on their own range; brisk_cache/ joins the reserved event-root directories. 66 offline tests (398 total). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
stormscape.burnmodule and aburnsubcommand: screen the CIMSS BRISK archive for fires intersecting an AOI, cache and mosaic the scenes, classify severity, and drape it over the hillshade.Why BRISK
BRISK maps every large US fire daily from a nine-satellite dNBR composite, which is what makes it usable while a fire is still burning. The authoritative products are not: BAER soil burn severity lands days-to-weeks after containment and only for assessed fires, MTBS a year or more later.
The portal's WMTS serves rendered PNG capped at zoom 7, so this reads the per-fire GeoTIFFs from the open archive behind it. Finding which fires touch an AOI costs no pixels — GDAL range-reads the headers (~60 fires in 1.4 s) and footprints are memoised, so repeat screens are instant.
Colours match BAER
Maps draw in the BAER class palette, banded into severity classes with class names on the colour bar, so a stormscape map can sit beside a BAER product. The palette is read from the products themselves: all 77 of the 2025 BAER soil-burn-severity rasters embed the same four class colours — and they are the four BRISK publishes.
Validation
examples/brisk_vs_baer.pycompares BRISK against the BAER teams' own dNBR on the 39 fires carrying both. BRISK computes the same quantity: median slope 1.013, bias +0.003, and r = 0.938 over 1.24 M cells.The disagreements are compositing latency, not a different algorithm — every poor performer read low on the BAER date and recovered to r = 0.80–0.96 given a scene 5–21 days later. A fixed +14 d rule takes fires at r ≥ 0.90 from 14/39 to 24/39. So scenes now carry an
age_days, immature composites are flagged on every run, and--min-age DAYSturns that into a hard filter.BAER products from Sentinel-2 (20 m) agree better than Landsat-derived ones (30 m) — median r 0.923 vs 0.814, p = 0.0005.
Caveat, stated everywhere it matters
dNBR is a vegetation index, not soil burn severity, and the USGS post-fire debris-flow models are calibrated on the latter. BRISK is interim: act early, supersede with
--product sbs. Flagged in the README, the CLI help and the module docstring.Incidental
drape_i15gainsnorm/cbar_ticks/cbar_ticklabelsfor classed maps (general, reusable)exportlearnsseverityis categorical, and scales burn fields on their own range instead of the rainfall autoscale floorbrisk_cache/joins the reserved event-root directoriesTests
66 new offline tests (398 total, ~3 s). They pin the documented invariants: dNBR breaks match the published ×1000 thresholds, unburned stays distinct from unobserved, a scene's NaN surround never erases a neighbouring fire, class mosaics are never interpolated, the palette stays identical to BAER's, and a past-year archive index caches forever while today's expires.
🤖 Generated with Claude Code