Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d0f673f
Add restart capability to the fire behavior model
masih-e Aug 26, 2026
cf7696c
Refactor restart code into state submodule
masih-e Sep 24, 2026
b8d3934
Move global 2D distribution helper to mpi_mod
masih-e Sep 25, 2026
6c023a9
Refactor restart initialization and scheduling
masih-e Sep 25, 2026
cb37820
Clean up standalone driver restart flow
masih-e Sep 25, 2026
5225310
Clean up restart helper dependencies
masih-e Sep 29, 2026
922aa41
Move FMC restart writes into FMC model
masih-e Sep 29, 2026
3d75991
Make restart submodule signatures explicit
masih-e Oct 2, 2026
8aba8fc
Merge develop into restart-pr
masih-e Oct 2, 2026
e73f23c
Clean up restart initialization interface
masih-e Oct 2, 2026
e5b75c5
Merge remote-tracking branch 'origin/develop' into restart-pr
masih-e Oct 20, 2026
3291f13
Preserve perimeter activation state across restart
masih-e Oct 20, 2026
59b9081
Preserve double precision in fuel restart fields
masih-e Oct 20, 2026
bc205bc
Merge restart PR #39 into develop locally
mefrediani Oct 21, 2026
1f61101
Add standalone restart-chain regression coverage
mefrediani Oct 21, 2026
a863938
Respect checkpoint field applicability and preserve run provenance
mefrediani Oct 21, 2026
ed18f91
Document restart validation and the P02 initialization failure
mefrediani Oct 21, 2026
5c3f117
Use aligned integer fire schedules and preserve restart phase
mefrediani Oct 21, 2026
0e5709b
Cover fractional forcing schedules and restart chains through P00 har…
mefrediani Oct 21, 2026
376a650
Document clock normalization and validate exchange-window phase
mefrediani Oct 21, 2026
67b3567
Exercise schedule rejection and legacy clock compatibility
mefrediani Oct 21, 2026
3ed4a17
Expose the scheduling validation suite in run documentation
mefrediani Oct 21, 2026
a559126
Explain fractional schedules and restart event phase with examples
mefrediani Oct 22, 2026
ce551ee
Merge develop after restart and moisture initialization corrections
mefrediani Oct 23, 2026
c8aeeaa
Record P00 validation on merged P02 develop
mefrediani Oct 23, 2026
5de15ed
Run restart regressions in CI and document suite selection
mefrediani Oct 23, 2026
2c6b5b9
Explain regression test and checkpoint counts
mefrediani Oct 23, 2026
b2da936
Describe regression behavior without development labels
mefrediani Oct 23, 2026
56b8320
Consolidate test guidance and require unit-test explanations
mefrediani Oct 23, 2026
ecc5fcd
Merge restart CI foundation while preserving scheduling coverage
mefrediani Oct 23, 2026
cdd147d
Run fractional scheduling cases in existing CI suites
mefrediani Oct 23, 2026
15b675e
Retain legacy moisture interval tolerance after clock integration
mefrediani Oct 23, 2026
91d8da5
Correct the core Python test inventory
mefrediani Oct 23, 2026
82139bf
Merge develop after restart and extinction PRs
mefrediani Oct 23, 2026
e877286
Describe restart compatibility without development-history labels
mefrediani Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ list(APPEND _state_files state/state_mod.F90

list(APPEND _share_files share/proj_lc_mod.F90
share/datetime_mod.F90
share/fire_clock_mod.F90
share/emis_mod.F90
share/fuel_mod.F90
share/ros_mod.F90
Expand Down
143 changes: 125 additions & 18 deletions doc/Configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,11 @@ output fields retain their existing single-precision NetCDF representation.
last/next update times, and the moisture activation and cadence settings.
A resumed run restores this state without reinitialization and requires the
same activation and cadence. New checkpoints cannot be written before
moisture initialization. PR #39 time-based checkpoints are accepted when their
positive next-update time establishes initialization and their saved interval
matches ``fmoist_dt``. Legacy step-based files are rejected because their
metadata cannot distinguish initialized state from the old delayed-initialization
moisture initialization. Older checkpoints without explicit initialization
status or time are accepted for time-based cadence when their positive next-update
time establishes initialization and their saved interval matches ``fmoist_dt``.
Legacy step-based files are rejected because their metadata cannot distinguish
initialized state from the old delayed-initialization
defect. Prescribed-moisture legacy files remain readable. This compatibility
rule does not resolve the separate humidity-convention audit.

Expand Down Expand Up @@ -315,10 +316,11 @@ output fields retain their existing single-precision NetCDF representation.

Restart files retain physical moisture. The reader reconstructs spread
parameters from that state using the current damping calculation, so no
new restart field or namelist option is required. Existing PR #39 files
remain readable when their inputs satisfy the same validation. A legacy
checkpoint can contain flame diagnostics calculated with the old damping;
these are replaced at the next fire advance. Readability does not imply
new restart field or namelist option is required. Checkpoints from earlier
versions remain readable when their state and metadata satisfy the restart
compatibility checks. A legacy checkpoint can contain flame diagnostics
calculated with the old damping; these are replaced at the next fire advance.
Readability does not imply
numerical equivalence across the extinction correction. Continuity must be
assessed with continuous and restarted runs of the same model revision.

Expand Down Expand Up @@ -436,16 +438,121 @@ atmospheric data.
Timing and perimeter validation
===============================

``interval_atm`` must be a positive integer multiple of ``dt`` when using the
standalone forcing reader or NUOPC exchange schedule. For example, a 4 s fire
step and 60 s atmospheric interval are aligned. An 8 s fire step with a 4 s
atmospheric interval is not: the equality-based update trigger would miss the
first update and retain its expired schedule. The interval is an exchange
cadence, not necessarily the atmospheric solver timestep. Direct WRF stepping
and unforced ideal runs may leave ``interval_atm`` unset. Supplied positive
intervals are checked; file-driven and NUOPC entry points also require that the
interval is supplied. Programmatically constructed configurations are checked
before state allocation.
Output, forcing/exchange, restart writes, and the final time must align with
fire steps. Positive ``interval_output``, ``interval_atm``, and
``restart_interval`` values are whole seconds; ``-1`` disables an optional
schedule. At least one positive interval is needed to establish the fire clock.
Output and restart periods need not divide one another. For example,
``dt=0.3``, output every 12 s, forcing every 18 s, and restart writes every 9 s
are supported. An output interval of 13 s with that timestep is rejected.

The common interval is the greatest common divisor of enabled periods. Its
ratio to ``dt`` must differ from the nearest integer by at most
``min(0.01, 5e-6 * ratio)`` fire steps. The 5 ppm allowance covers timestep
inputs rounded to six significant digits, including ``0.222222`` for 2/9 s;
the 0.01-step bound prevents large intervals from admitting ambiguous alignment.
The resulting reduced rational seconds/steps defines the scheduling clock.
All other event and final times must be exactly reachable on this clock.
No tolerance grows with run duration, and no event fires after a missed step.

For example, consider these two accepted schedules:

.. list-table:: Converting event periods to fire-step counts
:header-rows: 1
:widths: 45 25 30

* - Quantity
- ``dt=0.3`` s
- ``dt=0.222222`` s
* - Output / atmosphere / restart periods
- 12 / 18 / 9 s
- 12 / 18 / 8 s
* - Common interval (greatest common divisor)
- 3 s
- 2 s
* - Common interval divided by input ``dt``
- Approximately 10 steps
- Approximately 9.000009 steps
* - Accepted integer step count
- 10
- 9
* - Scheduling timestep (common interval / step count)
- 3/10 s
- 2/9 s
* - Output / atmosphere / restart step intervals
- 40 / 60 / 30 steps
- 54 / 81 / 36 steps

In the ``dt=0.3`` example, 3 s is the largest whole-second interval that divides
12, 18, and 9 s. It is used to construct the clock; it does not request an
event every 3 s. In the rounded-timestep column, the discrepancy of about
0.000009 step is smaller than the allowed ``5e-6 * 9 = 0.000045`` step.
The scheduler therefore represents each step as exactly 2/9 s, while physics
continues to use the configured ``dt=0.222222`` s.

For a rejected example, change the ``dt=0.3`` output period to 13 s while
keeping the 18 s atmospheric and 9 s restart periods. Their common interval
becomes 1 s, and ``1 / 0.3`` is about 3.333333 steps. Its distance from the
nearest integer is about 0.333333 step, far above the allowed 0.0000167 step.
The model rejects this schedule because 13 s falls between fire steps.

Event detection uses integer step counts from the original origin. Calendar
timestamps use integer whole seconds and a subsecond remainder, so elapsed
single-precision multiplication cannot stall output or drift across calendar
boundaries. Physics retains the configured ``dt``; normalization corrects the
schedule within the documented input-rounding allowance. The shared
``grid%clock%Elapsed(itimestep)`` interface returns REAL64 scheduling seconds
from the original origin. The moisture component computes its initialization
and elapsed update time from the configured physics timestep.

With the 0.3 s clock above, output every 12 s means output at steps 40, 80,
120, and so on. The event check compares the current step with those integers;
it does not need a floating-point elapsed time to equal 12 s exactly. At step
41, the timestamp is constructed from ``41 * 3 / 10``: 12 whole seconds plus
a 3/10 s remainder after the original start time. With the rounded 2/9 s
clock, the first 12 s output occurs at step 54 because ``54 * 2 / 9 = 12``.
If the caller reaches step 55 without processing that event, the scheduler
reports a missed event instead of writing late or skipping a forcing record.

``interval_atm`` is the exchange cadence, independently of the atmospheric
solver timestep. File-driven and NUOPC entry points require it. Direct WRF
stepping and unforced ideal runs may leave it unset. Supplied positive intervals
are validated. NUOPC uses the exact rational fire clock for ESMF subcycling,
accumulates ``1/atm_step_interval`` of each instantaneous flux, and resets before
the first advance of the next exchange window. The file-driven component
requires consecutive records spaced by ``interval_atm`` and an exact record at
every requested update; missing records and implicit skipping are rejected.

Standalone stops at the validated final step without overshooting or taking a
partial step. Its endpoint need not be an output, forcing, or restart event.
NUOPC/ESMX run endpoints must additionally be reachable by the host exchange
clock. Direct WRF retains responsibility for its host integration endpoint.

New checkpoints store the reduced clock ratio and atmospheric interval.
Restart restores the original origin and absolute timestep, then reconstructs
the next events strictly after that step. A checkpoint between events therefore
preserves their original phase. Output/restart intervals may change if they
remain compatible with the saved clock; atmospheric cadence must match in
file-driven runs. Restart writes require whole-second boundaries, consistent
with the existing filenames and namelist date interface. The REAL64 fuel fields
and saved perimeter-activation state are unchanged.

For example, start a run at 00:00:00 with ``dt=0.3``, output every 12 s,
and checkpoints every 9 s. A checkpoint at 00:00:09 stores the original
00:00:00 origin and absolute step 30. To resume, the new namelist's ``start_*``
values select 00:00:09, but reading the checkpoint restores that original
origin and step count. The next advance is step 31. The next output is step
40 at 00:00:12, followed by step 80 at 00:00:24. Restarting does not shift
those outputs to 00:00:21 and 00:00:33. Checkpoints continue on their own
9 s schedule, next at step 60 (00:00:18). Both schedules remain measured
from 00:00:00, although their events do not always coincide.

Idealized checkpoints without ``clock_seconds``, ``clock_steps``, and
``interval_atm`` are accepted when their timestamp agrees with the reconstructed
original clock and their other configuration metadata match. File-driven
checkpoints without ``interval_atm`` are rejected with a specific compatibility
diagnostic. This does not infer unknown forcing or humidity semantics. This
checkpoint interface does not provide NUOPC/WRF-host restart support.

For a supplied perimeter, ``fire_ignition_start_time1`` must be nonnegative
and align with a fire timestep. Roundoff-sized offsets are normalized to that
Expand Down
3 changes: 1 addition & 2 deletions driver/advance_mod.F90
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ subroutine Advance_state (grid, config_flags)

call Advance_fire_components (grid, config_flags)

grid%datetime_now = grid%datetime_start
call grid%datetime_now%Add_seconds (grid%itimestep * grid%dt)
call grid%Update_model_clock ()
call grid%datetime_now%Print_datetime ()

if (DEBUG_LOCAL) call Print_message (' Leaving Advance_state... ')
Expand Down
2 changes: 1 addition & 1 deletion driver/fire_behavior.F90
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ program fire_behavior
call grid%Handle_restart (config_flags, initialize = .true.)

if (DEBUG_LOCAL) write (OUTPUT_UNIT, *) ' Starting temporal loop...'
do while (grid%datetime_now < grid%datetime_end)
do while (grid%itimestep < grid%final_step)
call Advance_state (grid, config_flags)

call grid%Check_model_clock ()
Expand Down
46 changes: 21 additions & 25 deletions io/namelist_mod.F90
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module namelist_mod
use ros_mod, only : ROS_WRFFIRE
use fmc_mod, only : FMC_WRFFIRE
use emis_mod, only : EMIS_WRFFIRE
use fire_clock_mod, only : fire_clock_t
use stderrout_mod, only : Stop_simulation, Print_message
use mpi_mod, only : Convert_mpi_comm_to_f08

Expand Down Expand Up @@ -159,7 +160,7 @@ module namelist_mod
real :: true_lat_2 = 40.363 ! true latitude 2

! Atm block
integer :: interval_atm = -1 ! Time step [s] of the atm (or frequency to read atm data if offline)
integer :: interval_atm = -1 ! Atmospheric exchange/forcing cadence [s], independent of host solver dt
integer :: kde = 1 ! Number of atm vertical levels

! Devel block
Expand All @@ -169,6 +170,7 @@ module namelist_mod
procedure, public :: Broadcast_nml => Broadcast_nml
procedure, public :: Check_nml => Check_nml
procedure, public :: Check_time_intervals => Check_time_intervals
procedure, public :: Get_fire_clock
procedure, public :: Initialization => Init_namelist
procedure, public :: Init_devel_block => Init_devel_block
procedure, public :: Init_fire_block => Init_fire_block
Expand Down Expand Up @@ -426,41 +428,35 @@ end subroutine Check_nml

subroutine Check_time_intervals (this, require_atm_interval)

use, intrinsic :: ieee_arithmetic, only : ieee_is_finite
use, intrinsic :: iso_fortran_env, only : real64

implicit none

class (namelist_t), intent (in) :: this
logical, intent (in), optional :: require_atm_interval
real (real64) :: ratio, tolerance
type (fire_clock_t) :: clock
logical :: check_atm


! The WRF-data component reads only time/atm; do not require fire settings.
if (.not. ieee_is_finite (this%dt)) call Stop_simulation ('dt must be finite and positive')
if (this%dt <= 0.0) call Stop_simulation ('dt must be finite and positive')

! File-driven and NUOPC schedules need interval_atm; direct WRF stepping
! and unforced ideal runs can leave it unset. Validate a supplied interval.
! The atmospheric component reads only time/atm, without ignition settings.
check_atm = this%interval_atm > 0
if (present (require_atm_interval)) check_atm = check_atm .or. require_atm_interval
if (check_atm) then
if (this%interval_atm <= 0) call Stop_simulation ('interval_atm must be positive for atmospheric updates')
ratio = real (this%interval_atm, real64) / real (this%dt, real64)
! Four single-precision roundoff units in the dimensionless step count.
! Cap the tolerance at 1e-4 step so long schedules cannot admit partial steps.
tolerance = min (1.0e-4_real64, 4.0_real64 * epsilon (this%dt) * max (1.0_real64, abs (ratio)))
if (anint (ratio) < 1.0_real64 .or. abs (ratio - anint (ratio)) > tolerance) &
call Stop_simulation ('interval_atm must be an integer multiple of dt')
end if
if (check_atm .and. this%interval_atm <= 0) &
call Stop_simulation ('interval_atm must be positive for atmospheric updates')
if (this%interval_atm /= -1 .and. this%interval_atm <= 0) &
call Stop_simulation ('interval_atm must be positive or -1')
if (this%interval_output /= -1 .and. this%interval_output <= 0) &
call Stop_simulation ('interval_output must be positive or -1')
if (this%restart_interval /= -1 .and. this%restart_interval <= 0) &
call Stop_simulation ('restart_interval must be positive or -1')
clock = this%Get_fire_clock ()

! The equality-based scheduler can stall after a missed update.
! interval_atm is the exchange interval, not necessarily the host model dt.
! Intentional record skipping and output/restart alignment need a separate
! time-scheduling review; bypassing this check would not implement skipping.
end subroutine Check_time_intervals

function Get_fire_clock (this) result (clock)
class (namelist_t), intent (in) :: this
type (fire_clock_t) :: clock

call clock%Init (this%dt, [this%interval_output, this%interval_atm, this%restart_interval])
end function Get_fire_clock

subroutine Init_atm_block (this, file_name)

implicit none
Expand Down
24 changes: 23 additions & 1 deletion io/wrfdata_mod.F90
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ function Get_datetime_index (this, datetime) result (return_value)

call Get_netcdf_var (trim (this%file_name), 'Times', times)

if (datetime%Get_frac_ss () /= 0.0) call Stop_simulation ('Atmospheric record time must be a whole second')

do n = 1, size (times)
if (times(n) == datetime%datetime) then
return_value = n
Expand All @@ -191,7 +193,7 @@ function Get_datetime_index (this, datetime) result (return_value)

write (ERROR_UNIT, *) 'Datetime not found in WRF file:'
call datetime%Print_datetime ()
stop
call Stop_simulation ('Required atmospheric record is missing; record skipping is not supported')

end function Get_datetime_index

Expand Down Expand Up @@ -680,6 +682,25 @@ subroutine Update_atm_state (this, datetime_now, config_flags)

end subroutine Update_atm_state

subroutine Check_forcing_schedule (file_name, interval)
use, intrinsic :: iso_fortran_env, only : INT64
character (len = *), intent (in) :: file_name
integer, intent (in) :: interval
character (len = :), allocatable :: times(:)
type (datetime_t) :: expected
integer :: n

if (interval <= 0) call Stop_simulation ('interval_atm must be positive for atmospheric updates')
call Get_netcdf_var (file_name, 'Times', times)
do n = 2, size (times)
expected%datetime = times(n-1)
expected%fractional_seconds = 0.0
call expected%Add_seconds (int (interval, INT64))
if (expected%datetime /= times(n)) &
call Stop_simulation ('Atmospheric record spacing differs from interval_atm; record skipping is not supported')
end do
end subroutine Check_forcing_schedule

function Wrfdata_t_const (file_name, config_flags) result (return_value)

use, intrinsic :: iso_fortran_env, only : REAL32, INT32
Expand All @@ -702,6 +723,7 @@ function Wrfdata_t_const (file_name, config_flags) result (return_value)

return_value%file_name = trim (file_name)
call Is_netcdf_file_present (trim (file_name))
call Check_forcing_schedule (trim (file_name), config_flags%interval_atm)

! Init projection
call Get_netcdf_att (trim (return_value%file_name), 'global', 'CEN_LAT', att_real32)
Expand Down
13 changes: 8 additions & 5 deletions nuopc/fire_behavior_nuopc.F90
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ subroutine Advertise(model, rc)
! interval_atm for exchange and flux accumulation.
call config_flags%Check_time_intervals (require_atm_interval = .true.)
call Init_fire_state (grid, config_flags)
if (mod (grid%final_step, grid%atm_step_interval) /= 0) &
call Stop_simulation ('NUOPC final time must align with the atmospheric exchange interval')

! Import/ Export Variables -----------------------------------------------------

Expand Down Expand Up @@ -837,7 +839,9 @@ subroutine SetClock(model, rc)

rc = ESMF_SUCCESS

call ESMF_TimeIntervalSet (timeStep, s_r8 = real (config_flags%dt, kind = ESMF_KIND_R8), rc = rc)
! ESMF and CFBM must use the same exact subcycling period, including rounded dt input.
call ESMF_TimeIntervalSet (timeStep, s = grid%clock%seconds / grid%clock%steps, &
sN = mod (grid%clock%seconds, grid%clock%steps), sD = grid%clock%steps, rc = rc)
if (ESMF_LogFoundError (rcToCheck = rc, msg = ESMF_LOGERR_PASSTHRU, line = __LINE__, file = __FILE__)) &
return

Expand Down Expand Up @@ -890,8 +894,8 @@ subroutine Advance(model, rc)

rc = ESMF_SUCCESS

! ratio of fire to atmosphere time step
dtratio = config_flags%dt / config_flags%interval_atm
! Each exchange window averages exactly atm_step_interval fire advances.
dtratio = 1.0 / real (grid%atm_step_interval)

! query for clock, importState and exportState
call NUOPC_ModelGet(model, modelClock=clock, importState=importState, &
Expand Down Expand Up @@ -994,10 +998,9 @@ subroutine Advance(model, rc)

if (grid%datetime_now == grid%datetime_start) call grid%Save_state ()

If_reset_fluxes: if (grid%datetime_now == grid%datetime_next_atm_update) then
If_reset_fluxes: if (grid%Atm_event_due ()) then

call grid%datetime_now%Print_datetime ()
call grid%datetime_next_atm_update%Add_seconds (config_flags%interval_atm)

ptr_hflx_fire = 0.
ptr_evap_fire = 0.
Expand Down
3 changes: 1 addition & 2 deletions physics/level_set_mod.F90
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,7 @@ subroutine Check_isolated_negative_lfn (grid, threshold, min_size, max_size)
#endif

if (detection_sum > 0.0) then
grid%datetime_now = grid%datetime_start
call grid%datetime_now%Add_seconds (grid%itimestep * grid%dt)
call grid%Update_model_clock ()
call grid%Save_state ()

write (msg, '(a, i6, a, i6, a, i6, a)') 'Isolated negative LFN cluster (size', int (global_size_value), ') near (i=', &
Expand Down
Loading
Loading