Skip to content

Dev to master -> v2.6.0 - #224

Merged
filippi merged 91 commits into
masterfrom
dev
Oct 28, 2026
Merged

filippi merged 91 commits into
masterfrom
dev

Conversation

@antonio-leblanc

Copy link
Copy Markdown
Collaborator

As discussed, suggestion to bring dev to master and then release 2.6.0

HugoFara and others added 30 commits August 12, 2026 17:13
The constructor left `properties` uninitialised. Models that register at
least one property overwrite it in registerProperty, but the six that
register none never do -- Iso, heatFluxBasic, factorChemFlux,
CraterVaporFluxModel, LavaSO2Flux and vaporFluxBasic. Destroying one of
those passed whatever the member happened to be built over to delete[].

The same destructor also freed fuelPropertiesTable with delete[], while
DataBroker::extractFuelProperties allocates it with a scalar new.

Neither has been observed in a run, because FireDomain keeps its models
in propModelsTable and fluxModelsTable and frees neither -- so nothing
destroys a model today. Both become reachable as soon as anything does,
including a test.
…l -Wextra

tests/runff runs one case through one propagation model (Rothermel, set
in params.ff). Nothing was watching the other sixteen propagation models
or any of the sixteen flux models.

These tests exercise one model at a time, without a simulation.
getSpeedForNode splits in two -- the DataBroker gathers properties into
a double*, then the model does arithmetic on that array -- and only the
second half is physics. ModelSandbox builds an empty FireDomain purely
to instantiate models; Inputs fills their property array by property
name rather than by index, so reordering a constructor's registerProperty
calls cannot silently change what a test measures.

Two kinds of assertion, and the README says which to trust. Invariants
-- no spread without fuel, more wind never means less spread, a downslope
is not an upslope, released energy is independent of how the time window
is cut -- hold whatever the implementation is. Pinned rates of spread
merely record what ForeFire produces today, at 1e-5 relative tolerance so
that -march=native and FP contraction differences between runners do not
trip them.

Parameters are process-global, so the sandbox snapshots the parameter map
and restores it on destruction; without that, three of five
--order-by=rand seeds fail.

Also compiles ForeFire's own sources with -Wall -Wextra. The warnings are
not clean yet, so they are informational: FOREFIRE_WARNINGS_AS_ERRORS
turns them fatal for whoever is clearing a file, and
FOREFIRE_ENABLE_WARNINGS=OFF turns them off. NetCDF's headers are marked
SYSTEM so their own warnings stay out of the output.

The framework is doctest 2.5.3, vendored as a single header under
third_party/ so a build needs no network. Nothing there is compiled into
libforefireL. FOREFIRE_BUILD_TESTS defaults on, except for wheel builds.
Both workflows only triggered on master, so nothing integrating into dev
was tested. main.yml already carried a commented-out dev push trigger.
The Linux workflow builds with `sudo bash ./install-forefire.sh -y`, so
build/ is root-owned and ctest cannot create build/Testing/Temporary as
the runner user. It exited 8 without running a single test.

macOS is unaffected: install-forefire-osx.sh runs unprivileged.
`properties` is allocated by each model's own constructor with
`new double[numProperties]` and freed by ~ForeFireModel. Sixteen flux
models and two propagation models freed it again in their own
destructor, so destroying any model that registers at least one property
was a double free. The flux ones also used scalar `delete` on an array
allocated with `new[]`.

Removing the derived deletes and leaving it to the base class is the
whole fix; no ownership changes.

This has never been hit in production because FireDomain keeps its
models in propModelsTable and fluxModelsTable and frees neither, so
nothing destroys a model today. It is reachable from anything that does,
including a test -- which is why the test that covers it comes with it:
`every model can be destroyed` now runs all 33 models rather than the
six that register no properties, and `destroying a model does not
disturb the next one` allocates across a destruction, since a double
free often surfaces as the next allocation coming back wrong rather than
as an immediate abort.

Verified by reintroducing a single delete: the suite aborts with
"double free or corruption (!prev)" and doctest reports the case as
CRASHED - SIGABRT.
Two rate-of-spread models misbehave once dead fuel moisture reaches or
exceeds the moisture of extinction. Neither is reachable today, because
Md is a static column of the fuel table that never gets that high, but
both become reachable the moment dead fuel moisture is driven by a
dynamic field.

RothermelAndrews2018 returned NaN. Its moisture damping cubic is only
defined below the moisture of extinction: it reaches zero at mf/me == 1
and turns negative past it, driving the reaction intensity negative,
then the wind limit `wv = 0.9 * RI` negative, then pow(wv, B) to NaN for
the non-integer B. The `R <= 0` exit test is false for NaN, so the value
escaped into FireNode::velocity. Return zero before the cubic is
evaluated, and negate the exit test so a NaN can no longer slip past it.

The Balbi family sped up instead of slowing down. xsi is the share of
combustion energy spent vaporising the moisture the live fuel carries in
excess of the dead fuel; when the dead fuel is the wetter of the two it
goes negative, so (1-xsi) exceeds 1 and amplifies both the radiant term
and the flame temperature, which R00 raises to the fourth power. At
Md=3.0 that put the flame temperature near 18000 K and made the fire
spread 33x faster than at Md=1.0. Floor xsi at zero: the dead fuel's own
penalty is already carried by the 1/(1 + a*Md) factor. Applied to all
five models that share the expression.

xsi is deliberately not capped above. With the current fuel tables it
reaches ~5.3, which makes the flame temperature negative, but that traces
back to Deltah equalling DeltaH in most rows of fuels.csv rather than to
the model, and capping would move every existing calibration.

Behaviour below the thresholds is unchanged to the last digit, and the
runff KML and NetCDF references still match.
The runff test compares ForeFire against frozen ForeFire output, so it
cannot tell a physics fix from a physics regression: any deliberate
change to a spread model breaks it, and any accidental one that leaves
real_case.ff untouched slips through. This suite holds no reference data.
Every assertion follows analytically from the published spread equations,
so it stays valid across recalibration and across the planned switch from
a static fuel.Md column to a dynamic dead-moisture field.

Five invariants, over Rothermel, RothermelAndrews2018 and BalbiNov2011:

  finite      ROS is a real number for every moisture in [0, 3]
  monotonic   ROS decreases as dead fuel moisture rises
  extinction  ROS reaches zero at Md >= me and stays there
  responsive  a dynamic dead-moisture layer reaches the model
  resolved    DataBroker finds an optimised getter for every property

`responsive` skips until dynamic dead moisture exists, then activates.
`resolved` exists because DataBroker does not raise on an unknown
property: it warns and silently falls back to an un-optimised path, so a
mistyped property name yields stale values rather than an error.

Two constraints the probe has to respect. Each probe runs in its own
interpreter, because the core keeps mutable global state and a second
ForeFire() in one process inherits the first one's parameters -- an
in-process sweep returns one identical displacement for every input.
And relax is forced to 1, because below that FireNode::update blends in
the ignition velocity, which decays geometrically but never reaches zero,
so an extinguished front still creeps and extinction is unobservable.

Each assertion was mutation-checked: flipping the sign of the moisture
damping term, applying the dead-moisture layer inverted, removing the
moisture-of-extinction cutoff and mistyping a property name each turn the
matching invariant red. Runs in about 16 seconds.
registerPropagationModel matched the prefix "moist" and routed anything
starting with it to getMoisturesProperties, which fills five slots from
the moistures.* scalar parameters. That getter exists for the NFDRS-style
group FarsitePropagationModel registers as moist.ones / moist.tens /
moist.hundreds / moist.liveh / moist.livew, where five reserved slots
match five written values.

The prefix also caught the singular "moisture", which reserves one slot.
Balbi2015, Balbi2020 and BalbiNov2011TMdMl each register it, so every
property they registered afterwards was written four slots too late and
the last four ran past the end of the properties array. Balbi2020 read
its temperature out of moistures.liveh, its Rhod out of moistures.tens
and its Md out of moistures.hundreds, and wrote 28 doubles into a
24-element allocation. Measured, it spread the same 42.43 m -- the
distance covered by the initial front alone -- at every fuel-table Md
from 0.02 to 0.40: the model could not see its own fuel table.

Match "moist." instead, so the group still reaches the five-slot getter
while "moisture" resolves through propPropertiesGetters to getMoisture,
which writes exactly one value.

That leaves getMoisture reachable for those three models for the first
time, and it dereferenced moistureLayer unconditionally. Guard both
overloads and fall back to the moistures.ones parameter, which is the
value these models were already receiving, so no configured simulation
changes its numbers; warn once, since a model asking for a moisture
field and getting a constant is almost certainly misconfigured.

The runff KML and NetCDF references are unaffected: Rothermel registers
no moisture property.
Balbi2020, Balbi2015 and BalbiNov2011TMdMl each register a "moisture"
property beside their fuel properties, which is what the property-shift
fix addresses. Supply a temperature and a moisture layer from the probe
so all three can run, and add them to the sweep. Reverting the "moist."
prefix narrowing turns Balbi2020's monotonicity red, so the fix is now
covered.

Run duration is calibrated per model rather than fixed. Spread rates
differ by more than two orders of magnitude across these six models, and
one duration either drives the fast ones into the domain edge, clipping
the dry end of the sweep, or leaves the slow ones so close to the initial
front that perimeterResolution quantises the displacement and adjacent
moistures collide. The calibration iterates, because a Balbi front
accelerates as it develops and a rate measured over a short trial
underestimates the eventual one. All six models now land between 350 and
715 m in a 2000 m domain, well clear of both failure modes, and the
sweep keeps resolving if any model is recalibrated later.

Runs in about 40 seconds.
Adds invariants.yml, which builds the pyforefire extension and runs
tests/python/test_moisture_invariants.py on pushes to master, pull
requests against it, and manual dispatch.

A separate workflow rather than a job in main.yml, because the suite
needs the Python extension and install-forefire.sh does not build it:
outside a wheel build CMakeLists defaults FOREFIRE_BUILD_PYTHON to OFF,
so main.yml produces only the native interpreter. The job installs the
package into a venv instead of system Python, which keeps it clear of
PEP 668, and pip's build isolation supplies scikit-build-core and
pybind11 from pyproject.toml.

Checkout skips LFS, unlike main.yml: the suite builds its fuel, wind,
temperature and moisture layers in memory and needs no fixtures. Build
and suite together take about a minute locally.

This gives the repository a check that can distinguish a physics fix
from a physics regression. The runff test compares ForeFire against
frozen ForeFire output, so a deliberate model change breaks it while an
accidental one that leaves real_case.ff untouched passes.

Also documents the suite in TESTING.md, including the requirement that
each probe run in its own interpreter, and corrects the claim there that
nothing under tests/python is validated in CI.
Matches the change made to main.yml and macos.yml: the workflow only
triggered on master, so nothing integrating into dev would run it.
Unit tests for every propagation and flux model, and -Wall -Wextra
Keep rate of spread finite and damped at high fuel moisture
Recover the double-delete and moisture fixes that missed dev
Nothing tested the HTTP server, and it is due for changes to request
parsing and path resolution. These seven cases record what it does today,
so a regression shows up here rather than as a blank map in a browser.

Two behaviours the web console depends on and that any hardening must
preserve:

  - POST / with a body of "ff:<command>" reaches the interpreter. That is
    the entire UI-to-engine channel (forefireGUI.js sendCommand).
  - Files resolve against the working directory. The console issues
    plot[filename=fuel.png], ForeFire writes fuel.png beside the case,
    and the browser fetches /fuel.png as a static file. A document root
    that does not cover both htdocs and the working directory would break
    every overlay.

Deliberately not covered: path traversal. Asserting the current
behaviour would document an unfixed weakness in a public repository. Its
test belongs with the fix, when the assertion can be that the request is
refused.

Two problems found while writing this, neither fixed here:

  - HttpCommandServer.hpp was not self-contained. It used std::ifstream
    without including <fstream> and an unqualified cout, so it compiled
    only when included after a header that supplied both. Fixed, because
    the test cannot include it otherwise; no behaviour change.
  - stop() deadlocks. It closes the listening socket and then joins the
    accept thread, but closing a socket does not reliably wake a thread
    blocked in accept() on Linux, so the join never returns. The suite
    therefore starts one server and never stops it. Fixing that needs
    shutdown() or a self-pipe and belongs with the hardening work.
…ation

Pin the HTTP command server's behaviour before hardening it
Eight simulations in threads, each required to reproduce the fire node
count it produces when run alone, plus a case hammering object
construction to exercise the shared id counter.

Skips when the GIL is on, since every call into the extension is
serialised there and no race can show. Under PYTHON_GIL=0 on a
free-threaded interpreter it segfaults or returns wrong counts, which is
the point: it gives the shared-state work in #175 a failing test to fix.

Deliberately not wired into CI. On an ordinary interpreter it would skip
and report green while proving nothing; it earns a job once it can pass
for the right reason.

Refs #175
…on safe

Two races that the GIL currently hides.

ForeFireAtom::instanceNRCount was a plain long incremented with ++ to give
every atom its id. Concurrent construction could hand the same id to two
objects. It is now std::atomic<long> with a relaxed fetch_add, which is all
the ordering distinct ids need.

SimulationParameters::GetInstance used the textbook unsafe lazy singleton,
so two threads could both see null and both construct, then diverge with
different parameter objects. It is now a function-local static, whose
initialisation C++11 guarantees runs once. The unused private member is
gone.
StringRepresentation kept the output buffer, the nesting level and the
GeoJSON ring aggregation as statics and file globals, so two
representations in one process built into the same state. All of it is
per-instance now.

The file-scope 'outPattern' and 'domain' globals were shadowed by
members of the same name in every member function, so they were dead;
removed with the rest.

writedOnce stays a file global on purpose. Making it per-instance is the
one part of this that is observable: output() skips a representation
whose domain has no front once anything has been written, so the second
representation a coupled session creates would gain a first output file
it does not produce today. That is a behaviour decision for the coupled
runs rather than a mechanical one, and it belongs with the state work.

outputstr stays public because Command::dumpString reads it directly.

rungeojson.ff produces byte-identical output to dev, both files
(md5 5598d4b548bd7826546815f822b01073 and 177683c7bb42b267be6baa1d98ef5acd).

Refs #175
Add the concurrency stress test that reproduces #175
The script that writes the NetCDF landscape file was deleted in ac0baba
while four documents kept describing it, so anyone following the docs to
build their first case hit a file that was not there.

Restored from ac0baba^ with three fixes, since it was not usable as it
stood:

- The 3-D and 4-D paths were broken. Dimensions were read off the array
  as (NY, NX, NZ, NT) while the variable was created as (NT, NZ, NY, NX)
  and assigned untransposed, so a 4-D field raised a broadcast error and
  a 3-D one never had its NT dimension created. Fields are now indexed
  outermost axis first, which is what prealCF2Case.py in the same
  directory settled on when it fixed its own copy.
- scipy.io.netcdf is deprecated and scipy is not a project dependency.
  Uses netCDF4, which the tests already require, writing the same
  NETCDF3_CLASSIC format prealCF2Case.py writes.
- parametersProperties has seven required keys and the docs called them
  optional. They are checked before the file is opened, and named in the
  error, rather than raising a bare KeyError over a half-written file.

tests/python/test_genforefirecase.py builds a landscape, loads it in
ForeFire, ignites and steps, and pins the 3-D and 4-D shapes. The four
documents now describe what exists.

prealCF2Case.py still carries its own copy of both functions. Merging
them is a change to a working script and is left for its own commit.

Closes #167
ForeFire reports zero ASan errors today on both test paths, so the job
can be blocking from the day it lands rather than after a cleanup. The
point of turning it on now is to keep that property while the ownership
work in #175 starts moving allocations around.

-DFOREFIRE_SANITIZE=<list> passes <list> to -fsanitize= on the compile
line, the executables and the shared library, and switches the
optimisation flags to -g -O1 -fno-omit-frame-pointer: the default
-O3 -flto -fomit-frame-pointer set makes the reports unreadable. Having
it as a build option rather than raw CMAKE_CXX_FLAGS in the workflow
means the same build is reproducible locally.

The job runs the unit suite and runff under ASAN_OPTIONS=detect_leaks=0.
Errors block; leaks do not, and that is deliberate. Nothing owns a
PropagationModel (#159), so ASan reports every one as leaked, and a job
that is permanently red is a job everyone ignores. The leak count still
runs as an informational step so the figure stays visible, and it can be
made blocking once #159 lands.

Verified in ubuntu:24.04, which is what ubuntu-latest runs:

  build with -fsanitize=address       clean
  unit suite, detect_leaks=0          4/4, 0 errors
  runff, detect_leaks=0               passes, KML and NetCDF match
  leak report, detect_leaks=1         exits 8, ~4.7 MB reported

Negative control: reintroducing the double free fixed in #157 into
ForeFireV1HeatFluxModel turns the job red with

  ERROR: AddressSanitizer: attempting double-free
  SUMMARY: AddressSanitizer: double-free in operator delete[](void*)

so the job catches the class of bug this project has actually had.

Closes #162
Make the atom id counter atomic and the parameters singleton safe
…on-buffer

Give each string representation its own buffer
antonio-leblanc and others added 27 commits October 21, 2026 19:57
# Conflicts:
#	CHANGELOG.md
#	src/Command.cpp
#	src/Command.h
#206)

Since #182 the domain deletes its propagative layer. loadData also
registers that layer with the broker, which deleted every registered
layer, so clear[] after loadData freed it twice. The broker now keeps it
listed for lookup and leaves deleting it to the domain, in its destructor
and when the layer's key is registered again.

reload_case.ff now ends with clear[], so the runff step of the
AddressSanitizer job covers this path.

Fixes #205
* docs(changelog): correct the v2.5.0 wheel platforms

PyPI has three wheels for 2.5.0 -- manylinux x86_64, manylinux aarch64 and
macosx_14_0_arm64. There is no Intel macOS wheel: the macos-13 runner was
retired and its job dropped in e43e0a0, which is an ancestor of the tag.

* docs(changelog): add the entries merged since v2.5.0 without one

#179, #181, #182, #189, #190, #192 and #197 had no entry, and the
listenHTTP entry did not credit #203. Version bumps, merges and the
documentation backports are left out.

---------

Co-authored-by: Jean-Baptiste Filippi <batti.filippi@gmail.com>
…209)

The "table" branch registered the FuelDataLayer and then fell through to
the trailing `return false`. Nothing in the engine reads the result, only
the bindings do, so the layer worked while reporting it had not been made.

Co-authored-by: HugoFara <github@hugofara.net>
* feat(build): FOREFIRE_ENABLE_NETCDF to build without NetCDF

`#include <netcdf>` in DataBroker.h pulled NetCDF into 48 of the 58 files
in src/. A new FF_NO_NETCDF guard compiles out every NetCDF-backed path
(loadFromNCFile, load/saveArrivalTimeNC, saveData, writeNetCDF, the PGD
form of createDomain, the NetCDF form of loadData). Each keeps a stub that
prints that the build has no NetCDF, so the command table is unchanged.

Default is ON: a normal build compiles exactly the same code as before
(tests/runff KML and NetCDF outputs match their references). Only adds
lines, no existing line changes. Needed for a WebAssembly build (#171),
and allows a slim pip wheel without NetCDF/HDF5.

* docs(changelog): FOREFIRE_ENABLE_NETCDF

---------

Co-authored-by: HugoFara <github@hugofara.net>
* feat(build): compile the engine to WebAssembly

Builds on FOREFIRE_ENABLE_NETCDF (#208) for #171. emcmake now configures
the WASM build with no arguments: EMSCRIPTEN turns off NetCDF, MPI, the
Python module, the unit tests and the termios-driven CLI shell, and turns
on an embind module that mirrors the pybind11 API with typed arrays in
place of NumPy arrays. The binding transposes C-order arrays into the
x-major order the data layers index with, as _pyforefire.cpp does.

The module links to a ~1 MB .wasm and runs: bindings/wasm/smoke.mjs
reproduces the NumPy documentation's array-only case and gets the same
front a native pyforefire does, 349 nodes.

* ci(wasm): build the module and run the smoke test under Node
* feat(wasm): run a fire in the browser on real terrain

A static page over the WebAssembly module (#171). landscape.mjs builds the
fuel and elevation layers from satellite and Terrarium tiles for any square
of ground, worker.mjs runs the whole simulation off the main thread and
reports why it ended, and terrain3d.mjs draws the result on a raw WebGL
heightfield that tilts up from the 2D map, with click-to-ignite by ray cast.

* ci(wasm): publish the browser demo to GitHub Pages on demand

* fix(wasm): return addLayer's result, and fail the smoke test on a missing layer

addLayer returned void, so JavaScript saw undefined and the demo's worker
treated the propagation layer as unregistered: every run failed with "a
layer failed to register". The engine side was fixed in #209; this is the
binding half, which #210 shipped without.

The smoke test printed each layer's result without checking it, which is
how this got through CI. It now exits non-zero if any layer is missing.

* feat(wasm): split the demo into Explore and Research modes

Explore is the default: click the map to light a fire, set the wind on a
dial or from a scenario, and watch it play back over a full-height map.
Research adds the propagation model, the two tuning factors, the numerics,
fuel moisture and bed depths, and the fuel class cut-offs, each labelled
with the ForeFire name it sets.

Each run now gets its own worker. The engine's state is process-wide
(#175), so a reused module kept the first run's propagation model and a
model switch did nothing.

* fix(wasm): seams, terrain spikes, and zooming out in 3D

The tile mosaic was drawn at fractional pixel offsets, so every tile edge
was antialiased: a dark seam in the imagery, and in the terrain tiles
half-blended pixels that decode to spikes hundreds of metres high. The
offsets are now rounded to whole pixels.

The wheel in 3D only moved the camera over the ground already loaded. It
now zooms the ground as in 2D, and the rebuilt mesh keeps the orbit pose.

setView dropped tilt.value, so the second terrain rebuild passed undefined
as the camera blend and drew nothing until the view was toggled.

* fix(wasm): keep the fire playing through a pan or zoom

Any pan or zoom cleared the run, because the fronts were drawn in the
current view's metres and would have landed on the wrong ground. Each run
now remembers the square it was computed on, its fronts are drawn wherever
that square sits in the view, and the ignition is measured against that
square rather than a view still waiting on its tiles.

* fix(wasm): leave unburnt islands unfilled, and keep the wind arrow off the terrain

Each front was filled on its own, so the inner front ForeFire keeps round
ground the fire has encircled was filled orange like the burn around it.
The fronts are now one shape filled even-odd, which leaves that ground
clear.

The wind arrow was painted on the 2D canvas, which is also the texture of
the 3D mesh, so it was draped over the terrain. It is now an overlay on
the map, hidden in 3D where map north turns with the camera.

* ci(wasm): publish the demo to GitHub Pages on every push to dev
…spread (#213)

The faded perimeter trail is replaced by two layers built from what the
engine already reports. Arrival paints the burnt ground by when it caught
fire, the arrival-time map ForeFire writes natively, with isochrones at a
round interval. Spread colours each segment of the front by its nodes'
rate of spread, which the worker now keeps from the `vel` in print[] instead
of discarding it.

Each frame's fronts become one Path2D in domain metres once per run, so the
arrival layer redraws through a transform as the view pans and zooms.
ci: pull only the runff LFS data, cached between runs
docs: link the WASM demo from the README
* ci(docker): skip unit tests in the builder stage, tests/ is not copied

* ci(docker): copy third_party/ into the builder stage
…copyable (#221)

* perf: define FireNode::getNext() and getPrev() in the header

Both are one-line accessors called through the PLT from the front-tracking
loops; defined in the header they are inlined.

* perf: define the one-line accessors of FFPoint, FFEvent, FireNode and FireDomain in the headers

Same change as the previous commit for the other accessors that show up in a
profile of the propagation loops.

* refactor: let the compiler generate FFPoint and FFVector copies (rule of zero)

f319083 (#161) spelled out operator= so that the copy constructor and the
assignment cannot drift apart when a member is added. Removing both instead
gives that guarantee by construction: the compiler generates them from the
member list. The empty virtual destructors from the 2014 import go too:
nothing derives from either class.

Both become trivially copyable, 24 bytes instead of 32, and are copied and
passed by value without constructor and destructor calls.

* changelog: link #221
Every corrected example was run against a build of dev.

- Comments: a '#' after a command is read as a schedule operator, the
  command does not run ("unknown post operator"). Moved trailing
  comments to their own line and documented the rule.
- FireDomain: only sw, ne and t are read. BBoxWSEN and date= were
  ignored, leaving the domain georeferenced at DefaultSWLngLat
  (Corsica). Document setParameter[BBoxWSEN=...] before FireDomain.
- startFire: lonlat needs three values (lon,lat,z); (lon,lat) starts
  no fire.
- trigger: the form is trigger[wind;vel=(...)]@t=..., not
  fuelType=wind; t= inside the brackets is not read.
- plot: opt: is notation, not syntax; there is no area= key (BBoxWSEN,
  BBoxLBRT, BBoxLBRT=active); size takes no parentheses; the arrival
  time parameter is arrival_time.

The in-app help (app/forefire/commands.md) gets the same fixes.
* fix: use 3.59e-4 in the Rothermel wind exponent

10E-4 is 1e-3 in C++. Regenerates the runff references and the unit test pin.

* test: compare runff fronts by shape, not node by node

The front now has a different node count on each CPU (630 on the Linux runner, 636 on macOS, 637 elsewhere). Both comparisons run on failure, and the outputs are uploaded.

* test: set the runff front tolerance from the CI runners

* test: reword the tolerance comment

* build: -ffp-contract=off so every platform gives the same front; runff back to exact comparison

* changelog: the old Rothermel coefficient was 3.59e-3, not 1e-3; one list under Changed
…#220)

* fix: stretch a fuel map that misses the domain, like the other layers

When a NetCDF layer does not overlap the domain, DataBroker warns
"Failsafe Reprojection to whole domain extent" and builds the layer on the
domain extent. The fuel branch, added in c3a30da, computes that extent but
passes the file's own one to FuelDataLayer, so the map stays outside the
domain and every point reads its first cell.

* changelog: link #220

---------

Co-authored-by: Jean-Baptiste Filippi <batti.filippi@gmail.com>
@filippi
filippi merged commit 4019a4c into master Oct 28, 2026
17 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 9f4247b2 Deployed Oct 27, 2026 by filippi via deploy #34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants