geeViz.fireLib¶
Wildland fire modeling on Earth Engine.
Earth Engine is a lazy, tile-parallel, stateless engine. Fire spread is inherently sequential — a front advances over many timesteps, each depending on the last. Most Earth Engine fire projects founder on that seam, so this package draws the line explicitly rather than discovering it late:
Tier 1 — everything pixel-wise, in Earth Engine. Fuels assembly, terrain derivatives, fire-danger climatology, per-pixel fire behavior, risk algebra. All embarrassingly parallel; this is where the value is.
Tier 2 — cost-distance propagation, in Earth Engine. Arrival-time
surfaces via ee.Image.cumulativeCost, which is the Eikonal/minimum-
travel-time solution and needs no timestep loop at all. Documented
ceiling: it is isotropic.
Tier 3 — the real simulators, outside. FSim, FlamMap, FARSITE, ELMFIRE. This package writes their inputs and reads their outputs. It does not reimplement them.
The propagation insight worth stating up front, because it is what makes Tier 2 practical:
cost = ee.Image(1).divide(ros) # seconds per meter
arrival = cost.cumulativeCost(ignition, 50000) # seconds to each pixel
frames = [arrival.lte(t * dt) for t in range(1, 101)]
Least-accumulated-cost from a source is minimum travel time. A hundred animation frames are a hundred thresholds of one image, not a hundred iterations. Doing it as nested neighborhood operations instead would require a 100-pixel halo on every tile, because each one expands the required footprint by a pixel.
What this package will not do, stated so it can be designed around:
No true elliptical head-fire spread.
cumulativeCostassigns cost per pixel, not per edge, so it cannot express “cheap downwind, expensive upwind”. Chaining short wind-blocks approximates directional behavior; it does not reproduce Huygens wavelets.No coupled fire-atmosphere behavior. Plume dynamics and downdraft-driven runs are WRF-Fire and QUIC-Fire territory.
No spotting. Ember transport is stochastic and cost-distance cannot express it.
No replacement for a project-level FSim run.
- geeViz.fireLib.landfire_fuels(bands=('FBFM40', 'CBH', 'CBD', 'CC', 'CH'), *, region: Any = None)[source]¶
Assemble a multi-band LANDFIRE fuels image.
- Parameters:
bands – Keys from
FUEL_ASSETS. The default is exactly the set a Rothermel surface run plus a Van Wagner crown-fire check needs: a surface fuel model, canopy base height, canopy bulk density, canopy cover, and canopy height.region – Optional geometry to clip to. Clipping early is usually the right call for an AOI-scale analysis — it keeps later reductions from touching tiles they will discard anyway.
- Returns:
ee.Imagewith one band per requested key, named by that key.
Note
Bands are not unit-converted here. LANDFIRE ships canopy base height and canopy height in metres x 10, and canopy bulk density in kg/m3 x 100, precisely so they can be stored as integers.
behavior()applies the scaling where the physics needs real units — doing it here would mean two places could disagree about whether a value was already scaled, which is the sort of error that produces plausible numbers rather than obvious ones.
- geeViz.fireLib.terrain_layers(dem_asset: str = 'USGS/3DEP/10m', *, region: Any = None)[source]¶
Slope, aspect and terrain derivatives for fire behavior.
- Parameters:
dem_asset – DEM to derive from. 3DEP 10 m for CONUS; pass
"USGS/SRTMGL1_003"for near-global 30 m coverage.region – Optional clip geometry.
- Returns:
ee.Imagewithelevation,slope(degrees),aspect(degrees),northness,eastness, andslope_tan(tangent of slope, which is the form Rothermel’s slope factor actually consumes).
Note
northness/eastnessexist because raw aspect must never be fed to a model or a statistic. 359 degrees and 1 degree are adjacent on the ground and maximally distant numerically; averaging them yields 180, which points the wrong way. Decomposing to cos/sin makes the circular quantity behave like two linear ones.
- geeViz.fireLib.fuel_model_params(fbfm: int) Dict[str, Any][source]¶
Rothermel bed parameters for a Scott & Burgan fuel model number.
- Parameters:
fbfm – FBFM40 code, e.g. 102 for GR2 or 165 for TU5.
- Returns:
Dict of bed properties plus
nameandburnable.- Raises:
KeyError – The model is not in the table. Deliberate — silently substituting a default would return a plausible spread rate for a fuel bed nobody described, which is worse than an error because nothing downstream would question it.
- geeViz.fireLib.fuel_param_image(fbfm_band, param: str)[source]¶
Turn a fuel-model raster into a raster of one bed parameter.
Uses
remap, which is the right primitive here: the mapping is a lookup with no ordering meaning, so arithmetic on the model number itself would be nonsense (model 165 is not “more” than 102).- Parameters:
fbfm_band – Single-band
ee.Imageof FBFM40 codes.param – Key from
fuel_model_params(), e.g."w_1h".
- Returns:
ee.Imageof that parameter, masked where the fuel model is not in the table — masked rather than zero-filled, because zero is a meaningful fuel load and an unmapped pixel is not the same as a pixel with no fuel.
- geeViz.fireLib.fuel_coverage(fuels, region, *, scale: int = 90, fbfm_band: str = 'FBFM40') Dict[str, Any][source]¶
What fraction of an area the parameter table can actually model.
Run this before trusting any spread result over a new area.
_FBFM40_PARAMSis a verified subset, not the full Scott & Burgan 40, and an unlisted model contributes a masked pixel — so a landscape can come back looking calm simply because a third of it was unmodellable. The gap is invisible in the output image and obvious here.Measured on a real Oregon test area: 91.4% covered, with the 8.6% shortfall dominated by GS3 (123).
- Returns:
Dict with
covered_fraction,total_pixels, andmissing— a list of(fuel_model, pixels, fraction)sorted by how much of the area each accounts for, which is the priority order for extending the table.
- geeViz.fireLib.rate_of_spread(fuels, terrain, *, wind_speed_20ft, moisture_1h=0.06, moisture_live=1.5, fbfm_band: str = 'FBFM40', wind_adjustment: float = 0.4)[source]¶
Rothermel surface rate of spread, ft/min, as an
ee.Image.- Parameters:
fuels – Image carrying the fuel-model band (see
landfire_fuels()).terrain – Image carrying
slope_tan(seeterrain_layers()).wind_speed_20ft – 20-ft wind speed in mi/h, as a number or an
ee.Image. GRIDMET’svsis 10 m in m/s — convert before passing it, or the result is wrong by roughly a factor of two and still looks reasonable.moisture_1h – Dead 1-hour fuel moisture, fraction (0.06 = 6%). GRIDMET ships
fm100/fm1000as percentages.moisture_live – Live fuel moisture, fraction. 1.50 is a typical growing-season value; 0.60 or below is critically dry.
fbfm_band – Name of the fuel-model band in
fuels.wind_adjustment – Factor converting 20-ft wind to midflame wind. 0.4 is a common sheltered-surface default; unsheltered grass is nearer 0.6 and dense timber nearer 0.1-0.2. This single number moves rate of spread more than almost any other input, which is why it is an explicit argument rather than a constant.
- Returns:
ee.Imagebandros_ft_min, masked where the fuel model is not in the parameter table and forced to zero on non-burnable models.
- geeViz.fireLib.ros_metric(ros_ft_min)[source]¶
Convert rate of spread from ft/min to m/s.
Kept as an explicit edge conversion. Rothermel’s published coefficients are unit-bearing, so the calculation stays in English units throughout and converts exactly once, here.
- geeViz.fireLib.flame_length(ros_ft_min, i_r=None, *, residence_time_min: float = 0.5)[source]¶
Byram flame length in feet from rate of spread.
Byram:
FL = 0.45 * I^0.46with fireline intensityIin BTU/ft/s. Intensity is reaction intensity times residence time times spread rate.Flame length is what most risk products key on – the 4 ft and 8 ft thresholds in Wildfire Risk to Communities are direct suppression interpretations: under 4 ft is generally attackable by hand crews, over 8 ft implies crowning or spotting and effectively rules out direct attack.
- geeViz.fireLib.crown_fire_initiation(fuels, flame_length_ft, *, cbh_band: str = 'CBH', cbh_scale: float = 0.1)[source]¶
Van Wagner crown-fire initiation: does the surface fire torch?
A surface fire transitions to crown fire when its intensity is sufficient to ignite the canopy base. The controlling geometry is canopy base height — the vertical gap between surface flames and the lowest live crown fuel.
- Parameters:
cbh_scale – LANDFIRE stores canopy base height as metres x 10 so it can be an integer raster, so 0.1 converts to metres. Getting this wrong by a factor of ten produces a canopy 10x too high and a landscape that never torches.
- Returns:
ee.Imagebandcrown_initiation— 1 where the surface fire is expected to reach the canopy base, 0 otherwise.
- geeViz.fireLib.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.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_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.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.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.
Modules