Repository navigation
Conversation
A structure model could read the flow but not the fire. StructureStep now carries an optional StructureFire: the fire grid's geometry, the level it refines, the time of the fire's fields, and the engine's heat_release() and level_set(). It is nullptr without a fire. FireCoupler records when its last step ended, so the view states which fire step the fields belong to. The example fire engine returns its whole flux as heat_release(); the example structure model prints what the view holds each step. Three CTests check it against the engine's growing disk: both on level 0; the fire on a level-1 patch with the model on level 0; the fire on level 0 with the model on a level-1 patch. Structures_ExampleModel also checks that a run without a fire gets no view. No change to runs without a structure model; FireCoupler's two new members are written in advance() only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- report_fire is a free function (nvcc rejects a device lambda in a private member) - FireCoupler keeps the fire step's end in double, as ERF's time - the example engine checkpoints its last step and restores its fluxes, so heat_release() after a restart matches the straight run; FireView gains a restart leg that checks the view's time and heat right after the restart - docs and comments: restart wording, lifetime rule, erf.fire.engine, the 11 % / 1.4 % numbers, the checker's limits; heat_flux pinned in the decks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
asalmgren
left a comment
There was a problem hiding this comment.
Review comments below. No crash-level bugs found. The main point is that the fire view's time doesn't exactly match the model's step time in a single-precision build, or when the fire is on a finer level (first two comments).
| z_phys_cc, T_atm_k0, RH_atm_k0, precip_accum_k0.get()}; | ||
| m_engine->advance(forcing); | ||
| m_stepped = true; | ||
| m_step_end_time = time_d + static_cast<double>(dt); |
There was a problem hiding this comment.
In single precision dt here is the Real (float) dt, while ERF advances time as t_new = time + dt[lev] with dt[lev] in double. With a dt that float can't represent exactly (e.g. 0.1), step_end_time comes out as t + 0.100000001490116 while the model's step.time is t + 0.1, so fire->time == step.time is false even though the PR description says they match exactly. The CTests don't catch this because dt = 0.5 and 0.25 are exact in float. Suggest passing ERF's double dt_lev into advance() (or making the dt parameter double) and computing the end time from that.
| post_dycore ? yvel_new : yvel_old, | ||
| z_phys_cc, T_atm_k0, RH_atm_k0, precip_accum_k0.get()}; | ||
| m_engine->advance(forcing); | ||
| m_stepped = true; |
There was a problem hiding this comment.
When the fire sits on a finer level than the structure model, this end time is built by adding up the fine substeps, while the coarse model's time is time + dt[lev] in one addition (and ERF then resets t_new[lev+1] to that). Even in double they can differ by round-off: with dt_ref_ratio=3 and dt0 = 0.1, three adds of 0.0333... give t + 0.30000000000000004 vs t + 0.3. Storing the time ERF assigns to the level (t_new[m_lev]) instead of summing would avoid the drift.
| #ifdef ERF_USE_FIRE | ||
| if (m_fire_coupler) { | ||
| const FireEngine& engine = m_fire_coupler->engine(); | ||
| const double fire_time = m_fire_coupler->stepped() ? m_fire_coupler->step_end_time() : time; |
There was a problem hiding this comment.
Before the fire's first advance(), this falls back to the model's current step time time, not the time the fire's fields belong to. That's only right if the fire's level has been advanced every step since start/restart. If it hasn't stepped yet (e.g. right after a regrid or restart, or a level-0 model several coarse steps in before the fire level advances), fire->time reports t while heat_release/level_set still hold the state from t0, so the model treats stale fields as current.
| FireGrid m_fg; | ||
| int m_lev = 0; | ||
| amrex::Geometry m_geom_atm; ///< geometry of the fire grid's level | ||
| bool m_stepped = false; ///< advance() was called in this run |
There was a problem hiding this comment.
Possible simplification: instead of m_stepped plus the stepped() ? step_end_time() : time fallback in each consumer, set m_step_end_time in FireCoupler::initialize (and on restart) to the start/restart time (t_new[fire_lev]). Then it always means "time of the engine's fields", m_stepped/stepped() go away, and the fallback in advance_structure_model (and the issue noted there) goes with them.
| std::istringstream is(std::string(buf.dataPtr())); | ||
| is >> m_time; | ||
| is >> m_time >> m_step_start >> m_step_dt; | ||
| if (!is) { Abort("[FIRE] fire.example: cannot read " + restart_chkfile + "/FireEngineExample"); } |
There was a problem hiding this comment.
This now requires m_step_start and m_step_dt and aborts if they're missing, so checkpoints written by the example engine before this PR (which hold only m_time) can no longer be restarted from. Reading the two new fields as optional (defaulting m_step_dt to 0 if absent) would keep old checkpoints working.
| Real heat = Real(-1.0), burned = Real(-1.0); | ||
| if (f.heat_release) { heat = f.heat_release->sum(0) * dA; } | ||
| if (f.level_set) { | ||
| burned = dA * amrex::ReduceSum(*f.level_set, 0, |
There was a problem hiding this comment.
This hand-rolls a ReduceSum plus ParallelDescriptor::ReduceRealSum to count burned cells, but FireEngine::burned_area() already computes the burned area across ranks. Since this file is the reference example, out-of-tree models will copy this pattern (including the device-lambda workaround). Exposing burned_area in StructureFire, or calling the engine's version, would avoid the duplication.
| } | ||
|
|
||
| private: | ||
|
|
There was a problem hiding this comment.
Nit: stray blank line added after private:, unrelated to the change.
Related to #5009
Summary
A structure model (
erf.structures.model, added in #5009) can read the flow but not the fire. To heat power-line conductors and towers from a fire, a model needs the fire's heat release and burned area at its own points.This PR adds an optional, read-only view of the fire to the structure seam:
StructureStep::fire. It holds the fire grid's geometry, the level the fire grid refines, the time of the fire's fields, and the engine's existing gettersheat_release()andlevel_set(). It isnullptrwhen the run has no fire. ERF itself still contains no structure model, and no fire physics is added here. The models that use the view live outside ERF, as the fire engines do.The fire and the model may sit on different levels. The view states which fire step its fields belong to, and three new CTests check this for the three layouts.
Runs without a structure model are bit-identical. No existing gold file or test result changes.
Structures_ExampleModelgains one check (no fire view without a fire).Theory
Which fire step a model sees. Within one step of a level, ERF advances the structure model first, then the dycore, then the fire ($t$ the start of the model's step, $t_f$ the time the fire's fields belong to and $\Delta t_c$ the step of the fire's level when it is coarser:
ERF_Advance.cpp). A finer level takes its substeps after the coarse step. WithWhat the tests compare. The example fire engine burns a disk of radius$r(t) = r_0 + v,t$ that releases $q_0$ inside it. With one substep per fire step of length $\Delta t_f$ , the heat released $Q$ and the burned area $A$ at $t_f$ are
with$r_0$ = 150 m, $v$ = 50 m/s and $q_0$ = 5e4 W/m². $Q = 0$ before the fire's first step.
Why a 3 % tolerance. Fire cells of 10 m resolve these disks to 1.4 % or better wherever the disk sits on the grid. Fields one fire step late or early miss by 11 % or more.
Changes
1. The fire view (
Source/MovingBodies/Coupling/ERF_StructureModel.H:98,:128)A new struct
StructureFireand a new last memberStructureStep::fire. The struct uses AMReX types only, so the seam header still includes no fire header.geomleverf.fire.anchor_level)timeheat_releaseFireEngine::heat_release()[W/m²], ornullptrlevel_setFireEngine::level_set(), negative where burned, ornullptr2. Building the view (
Source/MovingBodies/Coupling/ERF_StructureCoupling.cpp:87)ERF::advance_structure_modelfills the view underERF_USE_FIREwhen a fire coupler exists, and passesnullptrotherwise.3. The fire step's time (
Source/FireCoupling/ERF_FireCoupler.H:206,ERF_FireCoupler.cpp:189)FireCoupler::stepped()andstep_end_time()record when the engine's lastadvance()ended.advance()now takes ERF'sdoubletime and passes it to the engine asReal, as before. In single precision the view's time then matches the model's step time exactly.4. Example engine (
Tests/FireEngineExample/ERF_FireEngineExample.cpp:126,:139)heat_release()returns the whole flux of the last step, before the sensible/latent split. The engine now writes its last step's start and length to its checkpoint and rebuilds that step's fluxes on a restart, soheat_release()after a restart equals the straight run's.5. Example model (
Tests/StructureModelExample/ERF_StructureModelExample.cpp:42)When
step.fireis set, the model prints each step the fire's level and time, the heat released (MultiFab::sum) and the burned area (ReduceSumover the level set). It is a free function, because nvcc does not accept a device lambda inside a private member function.6. Tests (
Tests/CTestList.cmake:5049) and docs (Docs/sphinx_doc/theory/StructureCoupling.rst:79)Three CTests, built only when both example directories are compiled in (the
particles = ONleg ofci.yml). A new section "Reading the fire" describes the view, its timing, its restart behaviour and its lifetime.No new input keys.
Results
check_fire_view.awkcompares each printed line with the disk. 2 ranks, fire cells 10 m (5 m on level 1).Structures_ExampleModel_FireViewchk00002(t = 1 s)Structures_ExampleModel_FireViewFinerFireStructures_ExampleModel_FireViewFinerModelMutants, each one line, each run against the four structure tests above plus
Fire_ExampleEngine_Restart:step_end_time= start of the stepnullptrpassed instead of the viewFireViewFinerModelheat_release()returns the sensible part onlyFireView(restart leg)FireView(restart leg)Test(s)
ERF_ENABLE_FIRE=ON,ERF_ENABLE_MOVING_BODIES=ON, both example directories compiled in:ctest -R 'Structures|Fire_|FireCoupl|StructureRegistry'38/38 passed.amrex.the_arena_init_size=0 amrex.init_snan=1 fab.do_initval=1and FPE traps (invalid, zero, overflow), with one box per rank (fabarray.mfiter_tile_size = 1024 1024 1024) and with tiling.ERF_ENABLE_FIRE=OFFandERF_ENABLE_MOVING_BODIES=ONcompiles with no warnings.git diff --checkare clean.ReduceSumin the example model.Not in this PR
🤖 Generated with Claude Code