Custom stepper lifecycle¶
Use this when the numerical step is yours and you still want the OpenPFC clock, field modifiers, and save hooks.
SpectralETDSession remains the JSON session for spectral ETD physics.
It is not a requirement for a custom step.
What you call¶
Need |
API |
|---|---|
Fixed step, clock already built |
|
Accept or reject a step |
|
Fields, modifiers, save and accepted-step hooks, profiling |
|
A standard backend stack owned by OpenPFC |
|
SimulationSession<Stack> and SimulationLifecycle are peers. The
session owns Time and a computational stack. The lifecycle owns Time
and the modifier/hook loop, and does not own a stack. A frontend may use
either one, or build a lifecycle around a session’s time() and the
fields that live on the stack. There is no third session type for custom
steppers.
Header: openpfc/kernel/simulation/simulation_lifecycle.hpp.
Who owns what¶
OpenPFC owns the clock, the accept/reject transition, named-field lookup,
and application of FieldModifier initial and boundary conditions.
The application owns the stepper, material laws, derived observables, and the decision of which columns or files those observables use.
SnapshotSeries and DiagnosticsSeries are not members of the lifecycle.
Call them from the save observer. They still work with no lifecycle at all.
Fixed and adaptive loops¶
run commits Time::next() before the step. A callback
step(double t, double interval) receives the clock interval. That
interval is min(dt, t1 - t_before) on the last step. A one-argument
step(double t) does not see it.
run_attempts opens begin_attempt with Time::get_dt(), which clips
to t1 and to the next save. The stepper returns StepDecision. The
lifecycle’s runner owns increment_step_success and
increment_step_rejection. A rejection does not call the save observer
or the accepted-step hook. An acceptance calls the accepted-step hook
once, then the save observer only when do_save() is true. An exception
from the step closes the attempt and propagates.
SimulationLifecycle::schedule(t0, t1, dt, saveat) builds that clock.
A non-positive saveat still saves the initial state and the final
state.
Initial conditions and boundary conditions¶
Bind each real field by name, then add modifiers. Names are not assumed:
nothing in the lifecycle is called psi, h, or c.
life.bind_field("density", density);
life.bind_field("solute", solute);
life.add_initial_condition("density", std::make_unique<pfc::Constant>(0.1));
Initial modifiers run once, before the first save. Boundary modifiers
run from the whole-step apply hook, including on an attempt that is
later rejected.
prepare_stage(stage_time) is the stepper’s own request for boundary
data at an internal stage. It uses the same modifiers and does not save,
checkpoint, or advance Time.
Save, checkpoint, profiling¶
life.set_save_observer([&](const pfc::Time &now) {
const int step = pfc::time::increment(now);
const double t = pfc::time::current(now);
snapshots.write(step, t);
diagnostics.write(step, t, columns);
});
life.set_accepted_step_hook([&](const pfc::Time &now) {
checkpoint.maybe_save(state, now);
});
life.set_profiling(&profiling_session); // optional; nullptr leaves it off
The save observer follows output cadence, including t0 when
do_save() is true. The accepted-step hook follows the physical step:
once per acceptance, not at t0, and not on a rejection.
CheckpointService::maybe_save decides its own every from that hook.
The lifecycle does not own the service and does not invent a payload.
Profiling frames each step callback when a session is set, including a
rejected attempt, because that work ran. prepare_stage is not a frame.
The accepted-step hook is outside the frame. Output does not move on a
rejection.