Scheduling callbacks and output writers
Callbacks and output writers in Simulation actuate on objects that subtype Oceananigans.Utils.AbstractSchedule. Schedules are small callable objects that return true when an action should fire and false otherwise. This page collects the built-in schedules, when to use them, and how to combine them for more complex behavior.
Simulation automatically shortens the next time step so that callback and output events scheduled by model time happen exactly when requested. Set align_time_step = false in the Simulation constructor to disable this.
For the following examples, we will use the following simple simulation and progress:
using Oceananigans
grid = RectilinearGrid(size=(1, 1, 1), extent=(1, 1, 1))
model = NonhydrostaticModel(; grid)
simulation = Simulation(model, Δt=0.1, stop_time=2.5, verbose=false)
dummy(sim) = @info string("Iter: ", iteration(sim), " -- I was called at t = ", time(sim),
" and wall time = ", prettytime(sim.run_wall_time))dummy (generic function with 1 method)IterationInterval
IterationInterval(interval) actuates every interval iterations:
schedule = IterationInterval(11)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 11 -- I was called at t = 1.0999999999999999 and wall time = 72.163 ms
[ Info: Iter: 22 -- I was called at t = 2.2000000000000006 and wall time = 76.791 msUse the offset kwarg to shift the trigger so that, for example,
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
schedule = IterationInterval(7; offset=-2)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 5 -- I was called at t = 0.5 and wall time = 1.991 ms
[ Info: Iter: 12 -- I was called at t = 1.2 and wall time = 4.779 ms
[ Info: Iter: 19 -- I was called at t = 1.9000000000000006 and wall time = 7.551 msnotice that the callback is actuated on iterations 5, 12, 19, …
Above, we have overwritten the original callback called "dummy" with the new one with the offset schedule. An alternative way is to construct the Callback manually and add it in the simulation:
simulation.callbacks[:dummy] = Callback(dummy, schedule)Callback of dummy on IterationInterval(7) with offset -2TimeInterval
TimeInterval(interval) actuates every interval of model time, in units corresponding to model.clock.time. For example,
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
schedule = TimeInterval(1.11)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 12 -- I was called at t = 1.11 and wall time = 29.175 ms
[ Info: Iter: 24 -- I was called at t = 2.22 and wall time = 39.037 msWhen model.clock.time isa AbstractTime such as DateTime, then interval can be Dates.Period:
using Dates
start_time = DateTime(2025, 1, 1)
clock = Clock(time = start_time)
datetime_model = NonhydrostaticModel(; grid, clock)
stop_time = start_time + Dates.Minute(3)
datetime_simulation = Simulation(datetime_model; Δt=Dates.Second(25), stop_time, verbose=false)
schedule = TimeInterval(Dates.Minute(1))
add_callback!(datetime_simulation, dummy, schedule)
run!(datetime_simulation)[ Info: Iter: 0 -- I was called at t = 2025-01-01T00:00:00 and wall time = 0 seconds
[ Info: Iter: 3 -- I was called at t = 2025-01-01T00:01:00 and wall time = 3.863 seconds
[ Info: Iter: 6 -- I was called at t = 2025-01-01T00:02:00 and wall time = 3.865 seconds
[ Info: Iter: 9 -- I was called at t = 2025-01-01T00:03:00 and wall time = 3.867 secondsWallTimeInterval
WallTimeInterval(interval; start_time=time_ns()*1e-9) uses wall-clock seconds instead of model time. This is mostly useful for writing checkpoints to disk after consuming a fixed amount of computational resources. For example, using the previous simulation without the
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
schedule = WallTimeInterval(0.005)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 1 -- I was called at t = 0.1 and wall time = 0 seconds
[ Info: Iter: 8 -- I was called at t = 0.7999999999999999 and wall time = 22.364 ms
[ Info: Iter: 15 -- I was called at t = 1.5000000000000002 and wall time = 27.057 ms
[ Info: Iter: 23 -- I was called at t = 2.3000000000000007 and wall time = 32.349 msSpecifiedTimes
SpecifiedTimes(times...) actuates when model.clock.time reaches the given values. The constructor accepts numeric times or Dates.DateTime values and sorts them automatically. This schedule is helpful for pre-planned save points or events tied to specific model times.
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
schedule = SpecifiedTimes(0.2, 1.5, 2.1)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 2 -- I was called at t = 0.2 and wall time = 21.785 ms
[ Info: Iter: 15 -- I was called at t = 1.5 and wall time = 78.984 ms
[ Info: Iter: 21 -- I was called at t = 2.1 and wall time = 81.665 msArbitrary functions of model
Any function of model that returns a Bool can be used as a schedule:
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
after_two(model) = model.clock.time > 2
add_callback!(simulation, dummy, after_two, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 20 -- I was called at t = 2.0000000000000004 and wall time = 19.167 ms
[ Info: Iter: 21 -- I was called at t = 2.1000000000000005 and wall time = 19.683 ms
[ Info: Iter: 22 -- I was called at t = 2.2000000000000006 and wall time = 20.149 ms
[ Info: Iter: 23 -- I was called at t = 2.3000000000000007 and wall time = 20.595 ms
[ Info: Iter: 24 -- I was called at t = 2.400000000000001 and wall time = 21.037 ms
[ Info: Iter: 25 -- I was called at t = 2.5 and wall time = 21.476 msCombining schedules
Some applications benefit from running extra steps immediately after an event or from combining multiple criteria.
ConsecutiveIterations
ConsecutiveIterations(parent_schedule, N=1) actuates when the parent schedule does and for the next N iterations. For example, averaging callbacks often need data at the scheduled time and immediately afterwards.
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
times = SpecifiedTimes(0.55, 1.5, 2.12)
schedule = ConsecutiveIterations(times)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 1 -- I was called at t = 0.1 and wall time = 0 seconds
[ Info: Iter: 6 -- I was called at t = 0.55 and wall time = 22.152 ms
[ Info: Iter: 7 -- I was called at t = 0.65 and wall time = 27.018 ms
[ Info: Iter: 16 -- I was called at t = 1.5 and wall time = 34.282 ms
[ Info: Iter: 17 -- I was called at t = 1.6 and wall time = 35.150 ms
[ Info: Iter: 23 -- I was called at t = 2.12 and wall time = 40.105 ms
[ Info: Iter: 24 -- I was called at t = 2.22 and wall time = 40.963 msAndSchedule and OrSchedule
Use AndSchedule(s₁, s₂, ...) when an action should fire only if every child schedule actuates in the same iteration. Use OrSchedule(s₁, s₂, ...) when any one of the child schedules should trigger the action. Both accept any mix of AbstractSchedules, so you can require, for example, output every hour and every 1000 iterations:
Oceananigans.Simulations.reset!(simulation)
simulation.stop_time = 2.5
after_one_point_seven(model) = model.clock.time > 1.7
schedule = AndSchedule(IterationInterval(2), after_one_point_seven)
add_callback!(simulation, dummy, schedule, name=:dummy)
run!(simulation)[ Info: Iter: 0 -- I was called at t = 0.0 and wall time = 0 seconds
[ Info: Iter: 18 -- I was called at t = 1.8000000000000005 and wall time = 46.941 ms
[ Info: Iter: 20 -- I was called at t = 2.0000000000000004 and wall time = 48.744 ms
[ Info: Iter: 22 -- I was called at t = 2.2000000000000006 and wall time = 50.457 ms
[ Info: Iter: 24 -- I was called at t = 2.400000000000001 and wall time = 52.143 msStateful schedules such as TimeInterval, SpecifiedTimes, and ConsecutiveIterations store their own counters, so create a fresh instance (or call copy) for each callback or output writer that needs an identical pattern.
Output-specific schedules
Some schedules only apply to output writers because they keep extra state or require file access.
AveragedTimeInterval
AveragedTimeInterval(interval; window=interval, stride=1) asks an output writer to accumulate data over a sliding time window before writing. The window ends at each actuation time, runs for window seconds, and samples every stride iterations inside the window.
AveragedSpecifiedTimes
AveragedSpecifiedTimes(times; window, stride=1) behaves like SpecifiedTimes but with a trailing averaging window. Pass either a SpecifiedTimes instance or raw times.
FileSizeLimit
FileSizeLimit(size_limit) actuates when the target file grows beyond size_limit bytes. Output writers update the internal path automatically so you usually only pass the size limit. Combine it with OrSchedule to rotate files when either the clock reaches a value or the file becomes too large.