pyfofem-fire-effects 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. pyfofem_fire_effects-0.1.1/CITATION.cff +22 -0
  2. pyfofem_fire_effects-0.1.1/LICENSE +22 -0
  3. pyfofem_fire_effects-0.1.1/MANIFEST.in +9 -0
  4. pyfofem_fire_effects-0.1.1/PKG-INFO +437 -0
  5. pyfofem_fire_effects-0.1.1/README.md +405 -0
  6. pyfofem_fire_effects-0.1.1/THIRD_PARTY_NOTICES.md +41 -0
  7. pyfofem_fire_effects-0.1.1/pyproject.toml +76 -0
  8. pyfofem_fire_effects-0.1.1/setup.cfg +4 -0
  9. pyfofem_fire_effects-0.1.1/src/pyfofem/__init__.py +83 -0
  10. pyfofem_fire_effects-0.1.1/src/pyfofem/components/__init__.py +124 -0
  11. pyfofem_fire_effects-0.1.1/src/pyfofem/components/_component_helpers.py +106 -0
  12. pyfofem_fire_effects-0.1.1/src/pyfofem/components/burnup.py +1545 -0
  13. pyfofem_fire_effects-0.1.1/src/pyfofem/components/burnup_calcs.py +588 -0
  14. pyfofem_fire_effects-0.1.1/src/pyfofem/components/consumption_calcs.py +1426 -0
  15. pyfofem_fire_effects-0.1.1/src/pyfofem/components/emission_calcs.py +347 -0
  16. pyfofem_fire_effects-0.1.1/src/pyfofem/components/emission_pipeline.py +670 -0
  17. pyfofem_fire_effects-0.1.1/src/pyfofem/components/mortality_calcs.py +836 -0
  18. pyfofem_fire_effects-0.1.1/src/pyfofem/components/soil_heating.py +2019 -0
  19. pyfofem_fire_effects-0.1.1/src/pyfofem/components/tree_flame_calcs.py +386 -0
  20. pyfofem_fire_effects-0.1.1/src/pyfofem/pyfofem.py +1057 -0
  21. pyfofem_fire_effects-0.1.1/src/pyfofem/supporting_data/FOFEM6.7/FOF_SPP.CSV +629 -0
  22. pyfofem_fire_effects-0.1.1/src/pyfofem/supporting_data/emissions_factors.csv +20 -0
  23. pyfofem_fire_effects-0.1.1/src/pyfofem/supporting_data/fofem_bark_thickness.csv +526 -0
  24. pyfofem_fire_effects-0.1.1/src/pyfofem/supporting_data/fofem_crnsch_eq1_bark.csv +444 -0
  25. pyfofem_fire_effects-0.1.1/src/pyfofem/supporting_data/species_codes_lut.csv +122 -0
  26. pyfofem_fire_effects-0.1.1/src/pyfofem_fire_effects.egg-info/PKG-INFO +437 -0
  27. pyfofem_fire_effects-0.1.1/src/pyfofem_fire_effects.egg-info/SOURCES.txt +28 -0
  28. pyfofem_fire_effects-0.1.1/src/pyfofem_fire_effects.egg-info/dependency_links.txt +1 -0
  29. pyfofem_fire_effects-0.1.1/src/pyfofem_fire_effects.egg-info/requires.txt +8 -0
  30. pyfofem_fire_effects-0.1.1/src/pyfofem_fire_effects.egg-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use PyFOFEM, please cite this software and the underlying FOFEM model."
3
+ title: "PyFOFEM"
4
+ type: software
5
+ version: 0.1.0
6
+ authors:
7
+ - family-names: Greene
8
+ given-names: Gregory A.
9
+ repository-code: "https://github.com/gagreene/pyfofem"
10
+ url: "https://github.com/gagreene/pyfofem"
11
+ keywords:
12
+ - fire effects
13
+ - FOFEM
14
+ - fuel consumption
15
+ - emissions
16
+ - soil heating
17
+ references:
18
+ - type: software
19
+ title: "First Order Fire Effects Model (FOFEM)"
20
+ publisher:
21
+ name: "USDA Forest Service"
22
+ url: "https://research.fs.usda.gov/firelab/products/dataandtools/fofem/spatialfofem-fire-effects-model"
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gregory A. Greene
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include THIRD_PARTY_NOTICES.md
3
+ include CITATION.cff
4
+
5
+ prune docs
6
+ prune development
7
+ prune examples
8
+ prune reference
9
+ prune tests
@@ -0,0 +1,437 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfofem-fire-effects
3
+ Version: 0.1.1
4
+ Summary: A Python implementation of the FOFEM fire-effects model
5
+ Author-email: "Gregory A. Greene" <map.n.trowel@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gagreene/pyfofem
8
+ Project-URL: Repository, https://github.com/gagreene/pyfofem
9
+ Keywords: fofem,fire,emissions,burnup,soil-heating,mortality
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ License-File: THIRD_PARTY_NOTICES.md
24
+ Requires-Dist: numpy>=1.23
25
+ Requires-Dist: pandas>=1.5
26
+ Requires-Dist: scipy>=1.10
27
+ Requires-Dist: tqdm>=4.64
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=7.0; extra == "test"
30
+ Requires-Dist: psutil>=5.9; extra == "test"
31
+ Dynamic: license-file
32
+
33
+ # pyfofem
34
+
35
+ `pyfofem` is a Python library for modelling first-order fire effects. Its calculations are validated against the pinned FOFEM C++ reference implementation. It provides vectorized APIs for tree mortality, fuel consumption, smoke emissions, burnup, and soil heating.
36
+
37
+ Start with the quick start guide: [docs/QUICK_START.md](docs/QUICK_START.md)
38
+
39
+ ## Directory Structure
40
+
41
+ ```text
42
+ pyfofem/
43
+ |-- src/pyfofem/ # Python library
44
+ | |-- __init__.py # Public API re-exports
45
+ | |-- pyfofem.py # Core orchestrators
46
+ | `-- components/ # Specialized computation modules
47
+ |-- tests/ # Unit, golden, and parity tests (pytest package)
48
+ | |-- __init__.py # Makes tests/ a package for qualified imports
49
+ | |-- _support.py # Shared path constants (no src/ sys.path insert)
50
+ | |-- conftest.py # Fixtures, markers, installed-only session check
51
+ | |-- run_unified_tests.py # `--suite core|full`, `--installed-only` runner
52
+ | |-- prepare_cpp_reference.py # Regenerates reference fixtures
53
+ | |-- compare_cpp_python_soil_heating.py # Lay* driver; run as a tests package module
54
+ | |-- unit/ # Golden-CSV + reference-independent unit tests
55
+ | |-- integration/ # Full-pipeline (`run_fofem_emissions`) tests
56
+ | |-- regression/ # Behavior regression tests
57
+ | |-- cpp_parity_live/ # Tests requiring the compiled reference
58
+ | `-- test_data/ # Input CSVs and expected outputs
59
+ |-- examples/ # Batch/array usage driver + example data
60
+ |-- development/burnup_array/ # Experimental prototype, outside the test gate
61
+ |-- reference/fofem_cpp/ # Pinned FOFEM reference source
62
+ |-- docs/reference/ # Literature and reference docs
63
+ |-- docs/CODEBASE.md # Architecture and model mapping
64
+ `-- README.md
65
+ ```
66
+
67
+ ## Features
68
+
69
+ - Tree mortality models (`crnsch`, `bolchar`, `crcabe`)
70
+ - Fuel consumption for litter, duff, herb, shrub, canopy, mineral soil
71
+ - Burnup post-frontal combustion engine based on Albini & Reinhardt
72
+ - Smoke emissions (`legacy`, `default`, `expanded` modes)
73
+ - Campbell soil-heating model (Massman HMV is in development and unavailable)
74
+ - Integrated soil-heating outputs in `run_fofem_emissions` (`Lay0`, `Lay2`, `Lay4`, `Lay6`, `Lay60d`, `Lay275d`)
75
+ - Reference-validation scripts/tests for burnup, consumption, and soil-heating outputs
76
+
77
+ ## Python support
78
+
79
+ PyFOFEM supports Python 3.11, 3.12, 3.13, and 3.14. GitHub Actions runs
80
+ smoke tests on Ubuntu for every supported version and on macOS and Windows for
81
+ Python 3.12 on pull requests. It runs the core suite on Ubuntu for every
82
+ supported version on updates to `master`.
83
+ ## Installation
84
+
85
+ Install the published distribution with pip or uv:
86
+
87
+ ```bash
88
+ python -m pip install pyfofem-fire-effects
89
+ # or
90
+ uv pip install pyfofem-fire-effects
91
+ `$([Environment]::NewLine)
92
+ The distribution name differs from the Python import package. Use
93
+ `import pyfofem` in Python code.
94
+
95
+ Install [uv](https://docs.astral.sh/uv/) and create the project environment:
96
+
97
+ ```bash
98
+ git clone https://github.com/gagreene/pyfofem.git
99
+ cd pyfofem
100
+ uv sync
101
+ ```
102
+
103
+ For development and the supported test suite, install the test extra and the
104
+ repository's `dev` dependency group, then use the locked environment:
105
+
106
+ ```bash
107
+ uv sync --all-extras --group dev
108
+ uv run python tests/run_unified_tests.py --suite core
109
+ ```
110
+
111
+ `uv.lock` records the reproducible development and CI resolution. Users who
112
+ prefer pip can install the package with `python -m pip install .`; contributors
113
+ using pip should add the test extra with `python -m pip install -e .[test]`.
114
+ ## Usage
115
+
116
+ ```python
117
+ from pyfofem import run_fofem_emissions
118
+
119
+ results = run_fofem_emissions(
120
+ litter=2.5, duff=10.0, duff_depth=3.0, herb=0.5, shrub=0.2,
121
+ crown_foliage=0.1, crown_branch=0.2, pct_crown_burned=50,
122
+ region="InteriorWest", cvr_grp="Ponderosa pine", season="Summer",
123
+ fuel_category="Natural", duff_moist=40, l_moist=10,
124
+ dw10_moist=12, dw1000_moist=20,
125
+ dw1=0.1, dw10=0.2, dw100=0.3, dw1000s=0.4, dw1000r=0.1,
126
+ soil_family="Fine-Silt", # required when soil_heating is enabled
127
+ soil_moisture=15.0, # optional mineral-soil moisture (%)
128
+ soil_heating=True, # bool or dict of advanced overrides
129
+ )
130
+
131
+ print(results["PM10F"])
132
+ print(results["DufCon"])
133
+ print(results["Lay2"])
134
+ ```
135
+
136
+ To match original FOFEM legacy emissions behavior, pass `em_mode="legacy"`.
137
+ In this mode, smoldering NOx (`NOXS`) is expected to be `0` by design.
138
+ In `expanded` mode, default smolder group 7 (`CWDRSC`) also has `NOx as NO = 0`,
139
+ so `NOXS` mainly comes from the duff group unless you change factor groups.
140
+
141
+ ## Output variables
142
+
143
+ Scalar calls return scalar values. When any modelled input is an array,
144
+ corresponding outputs are NumPy arrays with one value per input case. Fuel-load
145
+ and consumption outputs use the selected unit system: T/ac for `Imperial` and
146
+ kg/m² for `SI`. Emissions use lb/acre for `Imperial` and g/m² for `SI`.
147
+
148
+ ### Mortality outputs
149
+
150
+ `run_fofem_mortality()` returns the mortality probability directly; it does
151
+ not return a dictionary. The result is dimensionless and ranges from 0 (tree
152
+ survives) to 1 (tree dies). Unsupported species/model combinations can return
153
+ `NaN`. Scalar inputs produce a `float`, while array inputs produce a NumPy
154
+ array.
155
+
156
+ | Model | Output | Description |
157
+ |---|---|---|
158
+ | `bolchar` | Mortality probability | Probability of post-fire mortality from the bole-char model. |
159
+ | `crnsch` | Mortality probability | Probability of post-fire mortality from the crown-scorch model. |
160
+ | `crcabe` | Mortality probability | Probability of post-fire mortality from the cambium-kill model. |
161
+
162
+ ### Consumption, emissions, and soil-heating outputs
163
+
164
+ `run_fofem_emissions()` returns a dictionary. The following table includes
165
+ every key that can appear. The seven `*_Duff` keys depend on `em_mode`, and the
166
+ six `Lay*` keys are present only when soil heating is enabled.
167
+
168
+ | Variable | Units<br>(SI, Imperial) | Description |
169
+ |---|---|---|
170
+ | `LitPre`, `LitCon`, `LitPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire litter load. |
171
+ | `DW1Pre`, `DW1Con`, `DW1Pos` | kg/m², T/ac | Pre-fire, consumed, and post-fire 1-hour down woody fuel. |
172
+ | `DW10Pre`, `DW10Con`, `DW10Pos` | kg/m², T/ac | Pre-fire, consumed, and post-fire 10-hour down woody fuel. |
173
+ | `DW100Pre`, `DW100Con`, `DW100Pos` | kg/m², T/ac | Pre-fire, consumed, and post-fire 100-hour down woody fuel. |
174
+ | `DW1kSndPre`, `DW1kSndCon`, `DW1kSndPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire sound 1000-hour down woody fuel, summed across diameter classes. |
175
+ | `DW1kRotPre`, `DW1kRotCon`, `DW1kRotPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire rotten 1000-hour down woody fuel, summed across diameter classes. |
176
+ | `DufPre`, `DufCon`, `DufPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire duff load. |
177
+ | `HerPre`, `HerCon`, `HerPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire herbaceous fuel. |
178
+ | `ShrPre`, `ShrCon`, `ShrPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire shrub fuel. |
179
+ | `FolPre`, `FolCon`, `FolPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire crown foliage. |
180
+ | `BraPre`, `BraCon`, `BraPos` | kg/m², T/ac | Pre-fire, consumed, and post-fire crown branch fuel. |
181
+ | `MSE` | % | Mineral soil exposure. |
182
+ | `DufDepPre`, `DufDepCon`, `DufDepPos` | cm, in | Pre-fire, consumed, and post-fire duff depth. |
183
+ | `FlaDur` | s | Duration through the last timestep with flaming consumption above the Burnup reporting threshold. |
184
+ | `SmoDur` | s | Duration through the last timestep with smoldering consumption above the Burnup reporting threshold. |
185
+ | `FlaCon` | kg/m², T/ac | Total fuel consumed in the flaming phase. |
186
+ | `SmoCon` | kg/m², T/ac | Total fuel consumed in the smoldering phase. |
187
+ | `Lit-Equ` | Equation ID | Litter-consumption equation selected for the case. |
188
+ | `DufCon-Equ` | Equation ID | Duff-consumption equation selected for the case. |
189
+ | `DufRed-Equ` | Equation ID | Duff-depth-reduction equation selected for the case. |
190
+ | `MSE-Equ` | Equation ID | Mineral-soil-exposure equation selected for the case. |
191
+ | `Herb-Equ` | Equation ID | Herbaceous-consumption equation selected for the case. |
192
+ | `Shrub-Equ` | Equation ID | Shrub-consumption equation selected for the case. |
193
+ | `BurnupLimitAdj` | Code | Recoverable Burnup input-adjustment code. `0` means no adjustment; concatenated digits identify multiple adjustments. See the code list below. |
194
+ | `BurnupError` | Code | Burnup outcome code. `0` means success; nonzero values identify a failure. See the code list below. |
195
+ | `PM10F`, `PM10S` | g/m², lb/acre | PM10 emissions from flaming and total smoldering combustion. |
196
+ | `PM25F`, `PM25S` | g/m², lb/acre | PM2.5 emissions from flaming and total smoldering combustion. |
197
+ | `CH4F`, `CH4S` | g/m², lb/acre | Methane emissions from flaming and total smoldering combustion. |
198
+ | `COF`, `COS` | g/m², lb/acre | Carbon monoxide emissions from flaming and total smoldering combustion. |
199
+ | `CO2F`, `CO2S` | g/m², lb/acre | Carbon dioxide emissions from flaming and total smoldering combustion. |
200
+ | `NOXF`, `NOXS` | g/m², lb/acre | Nitrogen oxides, reported as NO, from flaming and total smoldering combustion. |
201
+ | `SO2F`, `SO2S` | g/m², lb/acre | Sulfur dioxide emissions from flaming and total smoldering combustion. |
202
+ | `PM10S_Duff` | g/m², lb/acre | Duff-only smoldering PM10. Present for `legacy` and `expanded` modes. |
203
+ | `PM25S_Duff` | g/m², lb/acre | Duff-only smoldering PM2.5. Present for `legacy` and `expanded` modes. |
204
+ | `CH4S_Duff` | g/m², lb/acre | Duff-only smoldering methane. Present for `legacy` and `expanded` modes. |
205
+ | `COS_Duff` | g/m², lb/acre | Duff-only smoldering carbon monoxide. Present for `legacy` and `expanded` modes. |
206
+ | `CO2S_Duff` | g/m², lb/acre | Duff-only smoldering carbon dioxide. Present for `legacy` and `expanded` modes. |
207
+ | `NOXS_Duff` | g/m², lb/acre | Duff-only smoldering nitrogen oxides, reported as NO. Present for `legacy` and `expanded` modes. |
208
+ | `SO2S_Duff` | g/m², lb/acre | Duff-only smoldering sulfur dioxide. Present for `legacy` and `expanded` modes. |
209
+ | `Lay0` | °C | Maximum modelled mineral-soil surface temperature. Present only when soil heating is enabled. |
210
+ | `Lay2` | °C | Maximum modelled mineral-soil temperature at 2 cm depth. Present only when soil heating is enabled. |
211
+ | `Lay4` | °C | Maximum modelled mineral-soil temperature at 4 cm depth. Present only when soil heating is enabled. |
212
+ | `Lay6` | °C | Maximum modelled mineral-soil temperature at 6 cm depth. Present only when soil heating is enabled. |
213
+ | `Lay60d` | Layer index | Deepest requested soil layer whose modelled temperature exceeds 60 °C; `-1` means no layer exceeded the threshold. With the default 1-cm depth grid, the index is also the depth in cm. |
214
+ | `Lay275d` | Layer index | Deepest requested soil layer whose modelled temperature exceeds 275 °C; `-1` means no layer exceeded the threshold. With the default 1-cm depth grid, the index is also the depth in cm. |
215
+
216
+ Units in paired entries are listed in the table header's **SI, Imperial**
217
+ order. The `PM10S`, `PM25S`, `CH4S`, `COS`, `CO2S`, `NOXS`, and `SO2S` values include
218
+ duff smoldering. Their corresponding `*_Duff` values report the duff-only
219
+ portion rather than an additional quantity to add to the total.
220
+
221
+ ## Examples
222
+
223
+ Example scripts live in `examples/`:
224
+
225
+ - `examples/emissions_batch.py`
226
+
227
+ Example input datasets are in `examples/example_data/`:
228
+
229
+ - `fofem_emissions_batch_test.csv`
230
+
231
+ ### Soil-heating options in `run_fofem_emissions`
232
+
233
+ - `soil_heating`: `False` (default), `True`, or `dict`
234
+ - `soil_family`: required when soil heating is enabled; accepts GUI-style names (for example `Fine-Silt`) or internal family names
235
+ - `soil_moisture`: optional top-level mineral-soil moisture override (%)
236
+ - `soil_heating` dict overrides:
237
+ - `soil_moisture`
238
+ - `start_temp`
239
+ - `efficiency_wl`
240
+ - `efficiency_hs`
241
+ - `efficiency_duff`
242
+ - `depth_layers_cm` (13 depths expected)
243
+ - `timestep_s`
244
+
245
+ Soil moisture precedence during soil-heating runs:
246
+ 1. Top-level `soil_moisture`
247
+ 2. `soil_heating["soil_moisture"]`
248
+ 3. `moisture_regime` soil value
249
+ 4. Clipped `duff_moist` fallback (`0..25%`)
250
+
251
+ ### Burnup status codes in `run_fofem_emissions`
252
+
253
+ `run_fofem_emissions()` returns two burnup-status fields:
254
+
255
+ - `BurnupError`: hard burnup failure code. `0` means burnup ran successfully.
256
+ - `BurnupLimitAdj`: clipping/adjustment code for recoverable inputs. `0` means no clipping was applied.
257
+
258
+ When `BurnupError != 0`, the burnup model does not run for that case and
259
+ `pyfofem` falls back to simplified consumption-duration defaults for the
260
+ emissions pipeline.
261
+
262
+ `BurnupError` codes:
263
+
264
+ - `0`: success
265
+ - `10`: `fistart` starting fire intensity, below **40 kW/m²**. The accepted range is
266
+ **40 to 100,000 kW/m²**, inclusive; values above the maximum are clipped
267
+ under adjustment code `1`.
268
+ - `11`: `ti` surface fire residence time, below **10 s**. The accepted range
269
+ is **10 to 200 s**, inclusive; values above the maximum are clipped under
270
+ adjustment code `2`.
271
+ - `12`: `u` windspeed at the top of the fuel bed, below **0 m/s**. The
272
+ accepted range is **0 to 5 m/s**, inclusive; values above the maximum are
273
+ clipped under adjustment code `3`.
274
+ - `13`: `tamb_c` ambient temperature in degrees Celsius, below **-40 °C**.
275
+ The accepted range is **-40 to 40 °C**, inclusive; values above the maximum
276
+ are clipped under adjustment code `5`.
277
+ - `14`: `dfm` duff moisture content as a fraction of dry weight, above
278
+ **1.972** (**197.2%** in the public `duff_moist` input). When duff is
279
+ present, the accepted range is **0.1 to 1.972** (**10% to 197.2%**),
280
+ inclusive; values below the minimum are clipped under adjustment code `6`.
281
+ Duff moisture is not range-checked when the duff load is zero.
282
+ - `15`: fire cannot dry the fuel. This is a calculated physical failure rather
283
+ than a separate input-range threshold.
284
+ - `16`: no fuel ignited within the residence time. This is a calculated
285
+ physical failure rather than a separate input-range threshold.
286
+ - `20`: `wdry` oven-dry fuel loading, outside **(1e-8, 1e6) kg/m²**.
287
+ - `21`: `ash` mineral ash content as a dry-mass fraction, outside
288
+ **(0.0001, 0.1)**.
289
+ - `22`: `htval` low heat of combustion, outside **(1e7, 3e7) J/kg**.
290
+ - `23`: `fmois` fuel moisture content as a fraction of dry weight, outside
291
+ **(0.01, 3.0)** (**1% to 300%**).
292
+ - `24`: `dendry` oven-dry fuel mass density, outside **(200, 1000) kg/m³**.
293
+ - `25`: `sigma` fuel-particle surface-area-to-volume ratio, outside
294
+ **(4, 10,000) m⁻¹**.
295
+ - `26`: `cheat` fuel specific heat capacity, outside
296
+ **(1000, 3000) J/(kg·K)**.
297
+ - `27`: `condry` oven-dry fuel thermal conductivity, outside
298
+ **(0.025, 0.25) W/(m·K)**.
299
+ - `28`: `tpig` piloted-ignition temperature, outside **(200, 400) °C**.
300
+ - `29`: `tchar` end-of-pyrolysis char temperature, outside
301
+ **(250, 500) °C**.
302
+ - `90`: no fuel particles; every fuel loading is less than or equal to zero.
303
+ - `91`: `ntimes` maximum number of simulation timesteps, is less than or
304
+ equal to zero; this is controlled by `burnup_kwargs["max_times"]`.
305
+ - `99`: unexpected burnup exception
306
+
307
+ Square brackets or the word "inclusive" above indicate valid endpoints. The
308
+ parenthesized fuel-particle ranges for codes `20`-`29` are strict: values equal
309
+ to either endpoint are rejected.
310
+
311
+ `BurnupLimitAdj` codes are concatenated digits when more than one adjustment is
312
+ applied. For example, `13` means codes `1` and `3` both occurred, and `246`
313
+ means codes `2`, `4`, and `6` occurred.
314
+
315
+ - `0`: no clipping applied
316
+ - `1`: `fistart` starting fire intensity, above **100,000 kW/m²**; clipped
317
+ to **100,000 kW/m²**.
318
+ - `2`: `ti` surface fire residence time, above **200 s**; clipped to
319
+ **200 s**.
320
+ - `3`: `u` windspeed at the top of the fuel bed, above **5 m/s**; clipped to
321
+ **5 m/s**.
322
+ - `4`: `d` fuel bed depth, below **0.1 m** or above **5 m**; clipped to the
323
+ nearest endpoint of the inclusive **0.1 to 5 m** range.
324
+ - `5`: `tamb_c` ambient temperature in degrees Celsius, above **40 °C**;
325
+ clipped to **40 °C**.
326
+ - `6`: `dfm` duff moisture content as a fraction of dry weight, below
327
+ **0.1** when duff is present; clipped to **0.1** (**10%** in the public
328
+ `duff_moist` input).
329
+
330
+ ## Testing
331
+
332
+ Run the full supported suite. `pyproject.toml` sets `testpaths = ["tests"]`,
333
+ so plain `pytest` (or `python -m pytest`) collects only the supported
334
+ package suite under `tests/` and does not touch the experimental prototype
335
+ below:
336
+
337
+ ```bash
338
+ python -m pytest
339
+ ```
340
+
341
+ Run the unified publish-oriented suite (recommended for CI/package checks):
342
+
343
+ ```bash
344
+ # Fastest, representative pull-request checks
345
+ python tests/run_unified_tests.py --suite ci-smoke
346
+
347
+ # Fast publish-safe suite
348
+ python tests/run_unified_tests.py --suite core
349
+
350
+ # Extended suite with parity/comparison tests
351
+ python tests/run_unified_tests.py --suite full
352
+ ```
353
+
354
+ Run the standalone Lay* soil-heating reference comparison from the repository
355
+ root so its ``tests`` package import resolves correctly:
356
+
357
+ ```bash
358
+ python -m tests.compare_cpp_python_soil_heating
359
+ ```
360
+
361
+ This diagnostic reads the pinned `reference/fofem_cpp/soil.tmp` fixture, exits
362
+ nonzero when a comparison exceeds its embedded tolerance, and is not part of
363
+ the unified test suites.
364
+
365
+ `.github/workflows/ci.yml` runs `ci-smoke` on pull requests and `core` on
366
+ pushes to `master`. `core` validates Python behavior against committed golden
367
+ data and does not build or run the C++ reference. The `full` suite invokes the
368
+ live C++ harness and golden generators; it is intentionally excluded from
369
+ ordinary CI and release checks. Core reads the pinned C++ Git revision and every
370
+ golden manifest, so a changed reference commit fails before it can be treated as
371
+ a current golden baseline. Run `full` only when that failure identifies a pinned
372
+ upstream C++ change or when a deliberate Python/parity investigation needs new C++
373
+ evidence.
374
+
375
+ ### Experimental prototype: `development/burnup_array`
376
+
377
+ `development/burnup_array` is a non-production, array-based burnup
378
+ prototype. It is **outside the default/release test gate** — `testpaths`
379
+ does not include it, `run_unified_tests.py` does not run it, and it is not
380
+ part of `core` or `full`.
381
+
382
+ Its explicit, separate diagnostic invocation:
383
+
384
+ ```bash
385
+ python -m pytest development/burnup_array/tests -q
386
+ ```
387
+
388
+ As of this writing that command **fails during collection**, not just an
389
+ individual test: `test_consumption_calcs_array.py` imports
390
+ `burnup_array_calcs` as a top-level module, but `burnup_array_calcs.py`
391
+ itself uses a relative import (`from .burnup_array_kernel import ...`),
392
+ raising `ImportError: attempted relative import with no known parent
393
+ package`. This is a known, tracked failure — not a skip, and not silently
394
+ part of the supported suite. Maintain it independently of the library's
395
+ supported test suites.
396
+
397
+ For package-validation workflows where you want to ensure tests are running
398
+ against the installed package (not local `src/`), use:
399
+
400
+ ```bash
401
+ python tests/run_unified_tests.py --suite core --installed-only
402
+ ```
403
+
404
+ ### Packaging pipeline usage
405
+
406
+ PyPI wheel/sdist check:
407
+
408
+ ```bash
409
+ python -m pip install build twine
410
+ python -m build
411
+ python -m twine check dist/*
412
+ python -m pip install .
413
+ python tests/run_unified_tests.py --suite core --installed-only
414
+ ```
415
+
416
+ Conda recipe `test:commands` example:
417
+
418
+ ```yaml
419
+ test:
420
+ commands:
421
+ - python tests/run_unified_tests.py --suite core --installed-only
422
+ ```
423
+
424
+ The Conda recipe lives in `conda-recipe/`. See `conda-recipe/README.md` for
425
+ build and test commands.
426
+
427
+ Reference-validation tooling and deterministic golden-data verification live
428
+ under `tests/cpp_parity_live/`. See [CODEBASE.md](docs/CODEBASE.md) for the
429
+ current test tiers and maintenance guidance. Release provenance and attribution
430
+ for bundled FOFEM-derived data are in
431
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
432
+
433
+ ## License
434
+
435
+ PyFOFEM source is licensed under the [MIT License](LICENSE).
436
+ Bundled FOFEM-derived runtime tables and their provenance are described in
437
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).