geeViz.fireLib.spread¶
Fire spread without a timestep loop.
The instinct when modeling spread on a raster is to iterate: dilate the burned area, advance a timestep, repeat a few hundred times. On Earth Engine that is the expensive path, and not by a little. Each neighborhood operation expands the footprint a tile must fetch by one pixel, so a hundred nested ones need a hundred-pixel halo on every tile. The engine will either crawl or refuse.
The native primitive does it in one call. ee.Image.cumulativeCost
computes, for each pixel, the minimum accumulated cost to reach it from a
source. Set cost to time per unit distance and least-accumulated-cost
is minimum travel time — the same quantity FlamMap’s MTT computes,
and the solution to the Eikonal equation |grad T| = 1 / ROS:
cost = ee.Image(1).divide(ros)
arrival = cost.cumulativeCost(ignition, maxDistance=50000)
The hundred animation frames people wanted from the loop are a hundred thresholds of that single image. No halo growth, no graph depth, trivially parallel.
The limitation, stated plainly: this is isotropic. cumulativeCost
assigns cost per pixel, not per edge, so it cannot express “cheap
downwind, expensive upwind”. A single call gives fuel- and
terrain-modulated spread that is directionally neutral — no elliptical
head-fire elongation.
spread_with_wind_blocks() works around that by iterating
coarsely instead of finely: chain one call per wind period, each using
that period’s wind. Twenty chained calls for a five-day fire is very
manageable, and it captures wind shifts, which is what drives the
large runs operationally. Within-block anisotropy is still lost. That
trade is fine for planning and risk; it is not fine for operational
head-fire prediction, and the difference should be stated in any product
built on this.
Module Attributes
Guards a runaway cumulativeCost. |
|
Whether |
Functions
|
Check whether arrival times come back in the units you expect. |
|
Turn one arrival-time surface into an animation-ready collection. |
|
Chained cost-distance spread across changing wind. |
|
Which unit's ignitions burn which unit's land. |
|
Fire arrival time, in seconds, from an ignition source. |
- geeViz.fireLib.spread.DEFAULT_MAX_DISTANCE_M = 50000¶
Guards a runaway cumulativeCost. It bounds the search, so an unreachable target does not walk the whole landscape.
- geeViz.fireLib.spread.GEODETIC_DISTANCE = True¶
Whether
cumulativeCostmeasures distance on the curved Earth rather than in the map projection’s plane.This must stay True for anything in geographic coordinates, and the reason is measurable. With
geodeticDistance=Falseon an EPSG:4326 image, a degree of longitude is treated as the same ground distance as a degree of latitude. It is not — it is shorter bycos(latitude)— so east-west travel is inflated by1 / cos(latitude).Measured at 44.2 deg N with a uniform 1 m/s spread rate, comparing modeled arrival time against the analytic answer:
1/cos(44.2 deg) = 1.394, which is the 1.386 almost exactly. A fire would have spread 39% too slowly east-west and correctly north-south — a systematic directional error that looks entirely plausible on a map and gets worse toward the poles (2x at 60 deg N).
The residual ~5% on the diagonal is the grid-path overhead: cost accumulates between pixel centres, so the discrete shortest path is slightly longer than the straight line.
- geeViz.fireLib.spread.travel_time(ros_m_s, ignition, *, max_distance_m: int = 50000, min_ros_m_s: float = 0.0001, geodetic: bool = True)[source]¶
Fire arrival time, in seconds, from an ignition source.
- Parameters:
ros_m_s – Rate of spread in metres per second as an
ee.Image. Useros_metric()to convert from Rothermel’s ft/min.ignition –
ee.Imagewhose non-zero pixels are sources, or anee.Geometry/ee.FeatureCollectionto rasterize.max_distance_m – Search radius. Also the cost ceiling — pixels beyond it are masked rather than assigned a huge time.
min_ros_m_s – Floor applied to the spread rate before inverting. Non-burnable fuel has ROS exactly zero, and
1/0is infinite cost, which is correct but propagates as a masked pixel that can sever otherwise-connected paths. The floor makes non-fuel effectively impassable (a very large but finite cost) while keeping the cost surface defined.
- Returns:
ee.Imagebandarrival_s— seconds for the fire to reach each pixel.
Note
Cost-unit question, now settled empirically:
cumulativeCostaccumulates cost per metre, so a cost band in seconds-per- metre yields arrival times in seconds. Verified by doubling a uniform spread rate and confirming arrival times halved exactly — the feared factor-of-30 per-pixel interpretation is ruled out.Accuracy against the analytic answer, uniform 1 m/s at 44.2 deg N: 0.997 due east, 0.993 due north, 1.053 on the diagonal. The diagonal residual is grid-path overhead and is not worth correcting for; the directional bias that was worth fixing is described on
GEODETIC_DISTANCE.
- geeViz.fireLib.spread.isochrones(arrival_s, *, n_frames: int = 100, step_seconds: float | None = None, total_seconds: float | None = None)[source]¶
Turn one arrival-time surface into an animation-ready collection.
This is the payoff of not iterating. Each frame is a threshold of the same image, so a hundred frames cost a hundred cheap comparisons rather than a hundred rounds of neighborhood growth.
- Parameters:
arrival_s – Output of
travel_time().n_frames – Number of frames.
step_seconds – Seconds per frame. Defaults to
total_seconds / n_frames.total_seconds – Burn period. Defaults to 24 hours.
- Returns:
ee.ImageCollectionof masked burned-extent frames, each witht_secondsandt_hoursproperties — ready for geeViz’s existing timelapse machinery.
- geeViz.fireLib.spread.spread_with_wind_blocks(fuels, terrain, blocks, ignition, *, ros_fn=None, max_distance_m: int = 50000)[source]¶
Chained cost-distance spread across changing wind.
Iterates coarsely – one
cumulativeCostper wind period rather than one per timestep. A five-day fire at six-hourly wind is about twenty calls, which Earth Engine handles comfortably, and it captures the wind shifts that drive large runs. Within a block the spread is still isotropic.- Parameters:
blocks – Sequence of dicts, each with
wind_speed(mi/h), optionallymoisture_1h, andduration_s.ros_fn – Callable
(fuels, terrain, **block) -> ee.Imagein m/s. Defaults to Rothermel viabehavior.
- Returns:
ee.Imagebandburned— final perimeter after all blocks.
- geeViz.fireLib.spread.transmission_matrix(fuels, terrain, ignitions, units, *, wind_speed_20ft=8.0, burn_period_s: float = 86400, scale: int = 30, max_distance_m: int = 50000)[source]¶
Which unit’s ignitions burn which unit’s land.
Cross-boundary fire transmission, and it falls out of the same primitive nearly free. Every ignition is independent, so this maps onto Earth Engine cleanly where a timestep loop does not.
- Parameters:
ignitions –
ee.FeatureCollectionof ignition points, each carrying the property named bysource_prop.units –
ee.FeatureCollectionof ownership or community polygons with aunitproperty.
- Returns:
ee.FeatureCollection, one feature per (ignition, unit) pair carrying burned area in hectares.
- geeViz.fireLib.spread.calibrate_cost_units(ros_m_s, ignition, known_distance_m: float, known_time_s: float, *, scale: int = 30)[source]¶
Check whether arrival times come back in the units you expect.
Runs
travel_time()over a landscape of uniform spread rate, where the answer is known analytically: reaching a pointdmetres away atrm/s must taked / rseconds.Returns a dict with the expected and observed times and their ratio. A ratio near the pixel size means cost is accumulating per pixel rather than per metre — a factor-of-30 error at 30 m that leaves every number plausible and every number wrong. Run this once against a fire with known progression before publishing anything derived from arrival times.