Disclosure: This came up during a wildfire fuel modeling exploration being run by an AI agent. I'm a human submitting it, but I did not discover this on my own and typically work in JS/TS rather than C++ so I'm not personally familiar with the workings of this library. I have confirmed the source myself by adding a unit test locally and am preparing a PR with the test and fix.
In src/behave/fuelModels.cpp, FuelModels::populateFuelModels() passes isDynamic = true to setFuelModelRecord(...) for every standard Scott & Burgan (2005) fuel model (model numbers 101–204), regardless of whether the model is actually dynamic. As a result, getIsDynamic() returns true for 23 models that are static per S&B 2005 — the ones with no transferable live herbaceous load.
This surfaced downstream in the pyrothermel Python bindings (j-tenny/pyrothermel#2), where FuelModel.from_existing(code).is_dynamic returns True for e.g. SH4, TU5, TL1, SB1.
The static vs. dynamic distinction
A fuel model is "dynamic" when its live herbaceous load transfers to dead based on curing (S&B 2005) — i.e. exactly the models with non-zero fuelLoadLiveHerbaceous. Per S&B 2005 (and the load data in this very file), the dynamic standard models are GR1–GR9, GS1–GS4, SH1, SH9, TU1, TU3 — and only those.
What's wrong
The entire 101–204 block passes isDynamic = true (the 2nd-to-last argument of setFuelModelRecord). For the static models that's incorrect. The 23 mislabeled records:
| Code |
Model # |
Name (note the trailing (S)) |
live-herb load |
isDynamic now |
should be |
| SH2 |
142 |
Moderate load, dry climate shrub (S) |
0 |
true |
false |
| SH3 |
143 |
Moderate load, humid climate shrub (S) |
0 |
true |
false |
| SH4 |
144 |
Low load, humid climate timber-shrub (S) |
0 |
true |
false |
| SH5 |
145 |
High load, dry climate shrub (S) |
0 |
true |
false |
| SH6 |
146 |
Low load, humid climate shrub (S) |
0 |
true |
false |
| SH7 |
147 |
Very high load, dry climate shrub (S) |
0 |
true |
false |
| SH8 |
148 |
High load, humid climate shrub (S) |
0 |
true |
false |
| TU2 |
162 |
Moderate load, humid climate timber-shrub (S) |
0 |
true |
false |
| TU4 |
164 |
Dwarf conifer understory (S) |
0 |
true |
false |
| TU5 |
165 |
Very high load, dry climate timber-shrub (S) |
0 |
true |
false |
| TL1–TL9 |
181–189 |
all (S) |
0 |
true |
false |
| SB1–SB4 |
201–204 |
all (S) |
0 |
true |
false |
(GR1–9, GS1–4, SH1, SH9, TU1, TU3 are already correctly true.)
Impact: metadata only (which is why it likely went unnoticed)
isDynamic only does work when there's live herbaceous load to transfer to dead. Every mislabeled record has fuelLoadLiveHerbaceous = 0, so the dynamic load-transfer step is a no-op for them — fire-behavior outputs (rate of spread, flame length, etc.) are unaffected. The defect is purely in the reported getIsDynamic() attribute. It only matters to consumers that read the flag as metadata — e.g. pyrothermel exposing is_dynamic.
Minimal repro (data-level)
For any of the 23 records above, getIsDynamic(n) returns true while getFuelLoadLiveHerbaceous(n, ...) returns 0. For example, TU5 (#165):
getIsDynamic(165) -> true // expected false
getFuelLoadLiveHerbaceous(165, ...) -> 0 // static: nothing to transfer
A sweep of the standard burnable set (101–204) shows isDynamic == true for all of them, including every (S) model.
References
Disclosure: This came up during a wildfire fuel modeling exploration being run by an AI agent. I'm a human submitting it, but I did not discover this on my own and typically work in JS/TS rather than C++ so I'm not personally familiar with the workings of this library. I have confirmed the source myself by adding a unit test locally and am preparing a PR with the test and fix.
In
src/behave/fuelModels.cpp,FuelModels::populateFuelModels()passesisDynamic = truetosetFuelModelRecord(...)for every standard Scott & Burgan (2005) fuel model (model numbers 101–204), regardless of whether the model is actually dynamic. As a result,getIsDynamic()returnstruefor 23 models that are static per S&B 2005 — the ones with no transferable live herbaceous load.This surfaced downstream in the
pyrothermelPython bindings (j-tenny/pyrothermel#2), whereFuelModel.from_existing(code).is_dynamicreturnsTruefor e.g. SH4, TU5, TL1, SB1.The static vs. dynamic distinction
A fuel model is "dynamic" when its live herbaceous load transfers to dead based on curing (S&B 2005) — i.e. exactly the models with non-zero
fuelLoadLiveHerbaceous. Per S&B 2005 (and the load data in this very file), the dynamic standard models are GR1–GR9, GS1–GS4, SH1, SH9, TU1, TU3 — and only those.What's wrong
The entire 101–204 block passes
isDynamic = true(the 2nd-to-last argument ofsetFuelModelRecord). For the static models that's incorrect. The 23 mislabeled records:(S))isDynamicnowtruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalsetruefalse(GR1–9, GS1–4, SH1, SH9, TU1, TU3 are already correctly
true.)Impact: metadata only (which is why it likely went unnoticed)
isDynamiconly does work when there's live herbaceous load to transfer to dead. Every mislabeled record hasfuelLoadLiveHerbaceous = 0, so the dynamic load-transfer step is a no-op for them — fire-behavior outputs (rate of spread, flame length, etc.) are unaffected. The defect is purely in the reportedgetIsDynamic()attribute. It only matters to consumers that read the flag as metadata — e.g. pyrothermel exposingis_dynamic.Minimal repro (data-level)
For any of the 23 records above,
getIsDynamic(n)returnstruewhilegetFuelLoadLiveHerbaceous(n, ...)returns0. For example, TU5 (#165):A sweep of the standard burnable set (101–204) shows
isDynamic == truefor all of them, including every(S)model.References
FuelModel.from_existing(...).is_dynamicreturnsTruefor all 40 standard burnable models, including static SH/TU/TL/SB models j-tenny/pyrothermel#2