Integrator Examples
On this page
The snippets below are written to match current oakcli usage. Where a block creates its own ctx, you can run the same pattern under oakcli run --script your_script.lua.
Fixed-Step RK4
The workhorse for smooth, well-conditioned fields. Four drift evaluations per step give 4th-order accuracy without any adaptive overhead:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.1,
wavenumber = 0.35,
omega = 0.2
})
local integrator = ooc.create_context_integrator(ctx, "rk4", {
initial_dt = 0.01
})
ooc.set_integrator(ctx, integrator)
for i = 1, 1000 do
ooc.step(ctx)
end
Adaptive RKF45
The six-stage Runge–Kutta–Fehlberg embedded pair writes a fifth-order state and estimates error from its fourth-order companion. It automatically shrinks or grows the timestep to meet tolerance, using a scaled infinity norm for real fields and a scaled RMS magnitude for complex fields. This is useful for problems with intermittent sharp features:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.02)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.15,
wavenumber = 0.6,
omega = 0.3
})
local integrator = ooc.create_context_integrator(ctx, "rkf45", {
initial_dt = 0.02,
min_dt = 1.0e-5,
max_dt = 0.1,
tolerance = 1.0e-4,
safety = 0.9,
adaptive = true
})
ooc.set_integrator(ctx, integrator)
for i = 1, 500 do
ooc.step(ctx)
local m = ooc.step_metrics_latest(ctx)
ooc.log("step=%d dt=%.5f err=%.2e", m.step_index, m.accepted_dt, m.rms_error)
end
ETDRK4 for Supported Semilinear Complex Flows
Use ETDRK4 when the target field is complex and the plan decomposes into a supported exact-linear part plus same-field nonlinear or dt-scaled source terms. This is a strong fit for dispersive or damped spectral flows with additive forcing:
local ctx = ooc.create()
local dt = 0.0125
ooc.set_timestep(ctx, dt)
local field = ooc.add_field(ctx, {1024}, {
type = "complex_double",
fill = {0.0, 0.0}
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_chirp",
amplitude = 0.16,
wavenumber = 0.018,
omega = -0.18,
phase = math.pi / 5,
kdot = -0.055,
wdot = 0.014,
rotation = 0.45,
scale_by_dt = true
})
ooc.add_dispersion_operator(ctx, field, {
coefficient = 0.78,
order = 1.08,
spacing = 0.02
})
ooc.add_linear_dissipative_operator(ctx, field, {
coefficient = 0.018,
order = 2.0,
spacing = 0.02
})
local integrator = ooc.create_context_integrator(ctx, "etdrk4", {
initial_dt = dt,
target_field = field
})
ooc.set_integrator(ctx, integrator)
for i = 1, 200 do
ooc.step(ctx)
end
Keep the plan on that single target field. For ETDRK4-supported stimulus operators, set scale_by_dt = true; time-dependent stimulus families also need their normal evolving clock behavior rather than fixed_clock = true.
Implicit Crank–Nicolson
A-stable trapezoidal rule — safe on stiff diffusion-dominated equations without sacrificing 2nd-order accuracy:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.05)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 1.0
})
ooc.add_linear_dissipative_operator(ctx, field, {
coefficient = 0.04,
order = 2.0,
spacing = 0.02
})
local integrator = ooc.create_context_integrator(ctx, "crank_nicolson", {
initial_dt = 0.05
})
ooc.set_integrator(ctx, integrator)
for i = 1, 200 do
ooc.step(ctx)
end
Backward Euler — Maximum Stiffness Tolerance
L-stable implicit method; first-order but unconditionally stable on arbitrarily stiff linear operators. Use when Crank–Nicolson introduces oscillations near sharp transients:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.1)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 1.0
})
ooc.add_linear_dissipative_operator(ctx, field, {
coefficient = 0.08,
order = 2.0,
spacing = 0.02
})
local integrator = ooc.create_context_integrator(ctx, "backward_euler", {
initial_dt = 0.1
})
ooc.set_integrator(ctx, integrator)
for i = 1, 200 do
ooc.step(ctx)
end
Subordination with Stochastic Noise
Two-stage subordinated integrator with Gaussian noise injection — suitable for fractional diffusion or stochastic PDEs:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.08,
wavenumber = 0.4,
omega = 0.15
})
local integrator = ooc.create_context_integrator(ctx, "subordination", {
initial_dt = 0.01,
enable_stochastic = true,
stochastic_strength = 0.2,
noise = "gaussian",
random_seed = 1337
})
ooc.set_integrator(ctx, integrator)
for i = 1, 100 do
ooc.step(ctx)
end
Switch to Laplace noise for heavier-tailed fluctuations:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.08,
wavenumber = 0.4,
omega = 0.15
})
local integrator = ooc.create_context_integrator(ctx, "subordination", {
initial_dt = 0.01,
enable_stochastic = true,
stochastic_strength = 0.1,
noise = "laplace",
})
ooc.set_integrator(ctx, integrator)
for i = 1, 100 do
ooc.step(ctx)
end
Multi-Integrator Sequence
set_integrator_sequence lets you assign different integration methods to different fields and step them together each tick. Each integrator advances at the same requested dt; the simulation clock advances by the minimum accepted step and reports the maximum error across all integrators.
A typical use: explicit RK4 for a reaction field (fast but smooth) and Crank–Nicolson for a diffusion field (stiff, needs implicit treatment):
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
-- Field 0: reaction term (explicit OK)
local reaction_field = ooc.add_field(ctx, {256}, { fill = 0.0 })
-- Field 1: diffusion concentration (stiff)
local diffusion_field = ooc.add_field(ctx, {256}, { fill = 0.5 })
ooc.add_stimulus_operator(ctx, reaction_field, {
type = "stimulus_sine",
amplitude = 0.05,
wavenumber = 0.08,
omega = 0.1
})
ooc.add_linear_dissipative_operator(ctx, diffusion_field, {
coefficient = 0.03,
order = 2.0,
spacing = 0.02
})
local rk4 = ooc.create_context_integrator(ctx, "rk4", {
initial_dt = 0.01,
target_field = reaction_field
})
local cn = ooc.create_context_integrator(ctx, "crank_nicolson", {
initial_dt = 0.01,
target_field = diffusion_field
})
ooc.set_integrator_sequence(ctx, { rk4, cn })
for i = 1, 500 do
ooc.step(ctx)
end
Clear the sequence (detach all integrators):
ooc.set_integrator_sequence(ctx, {}) -- empty table clears the sequence
-- or equivalently:
ooc.detach_integrator(ctx)
Manual Step Loop with Integrator Step
Drive an integrator manually for custom multi-rate schemes or when you want explicit control over the requested dt:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.1,
wavenumber = 0.5,
omega = 0.2
})
local integrator = ooc.create_context_integrator(ctx, "rkf45", {
initial_dt = 0.01,
adaptive = true,
min_dt = 1e-5,
max_dt = 0.1,
tolerance = 1e-4,
})
local dt = 0.01
for i = 1, 200 do
local ok, err = ooc.integrator_step(ctx, integrator, dt)
if not ok then
ooc.log("step %d failed: error code %d", i, err)
break
end
local m = ooc.step_metrics_latest(ctx)
dt = m.next_dt -- follow the adaptive suggestion for the next iteration
end
Use auto_advance = false only for probe steps where you intentionally do not want public time or step metrics advanced:
local ok, err = ooc.integrator_step(ctx, integrator, 0.01, false)
if not ok then
ooc.log("probe step failed: code %d", err)
end
There is no later zero-dt “commit” call for that probe result. If you want the simulation clock and metrics history updated, call integrator_step with auto_advance = true.
Reading Integrator Info
Inspect the live configuration of an attached integrator:
local ctx = ooc.create()
local field = ooc.add_field(ctx, {128}, { fill = 0.0 })
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.1,
wavenumber = 0.25,
omega = 0.1
})
local integrator = ooc.create_context_integrator(ctx, "rk4", {
initial_dt = 0.01
})
ooc.set_integrator(ctx, integrator)
local info = ooc.integrator_info(ctx)
if info then
ooc.log("method=%s adaptive=%s current_dt=%.4f tolerance=%.2e",
info.name,
tostring(info.adaptive_enabled),
info.current_dt,
info.tolerance)
end
Tuning Parameters at Runtime
Switch adaptive mode and tighten tolerance mid-run without recreating the integrator:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.02)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.12,
wavenumber = 0.5,
omega = 0.25
})
local integrator = ooc.create_context_integrator(ctx, "rkf45", {
initial_dt = 0.02,
adaptive = true,
tolerance = 1.0e-3
})
ooc.set_integrator(ctx, integrator)
-- First 100 steps: loose tolerance for fast spin-up
ooc.set_integrator_tolerance(ctx, 1.0e-2)
for i = 1, 100 do ooc.step(ctx) end
-- Tighten for the production run
ooc.set_integrator_tolerance(ctx, 1.0e-5)
ooc.set_integrator_safety(ctx, 0.85)
for i = 1, 1000 do ooc.step(ctx) end
Enable noise partway through a run:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.01)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.08,
wavenumber = 0.35,
omega = 0.15
})
local integrator = ooc.create_context_integrator(ctx, "subordination", {
initial_dt = 0.01
})
ooc.set_integrator(ctx, integrator)
for i = 1, 200 do ooc.step(ctx) end
ooc.set_integrator_stochastic(ctx, true)
ooc.set_integrator_stochastic_strength(ctx, 0.05)
ooc.set_integrator_noise(ctx, "gaussian")
for i = 1, 800 do ooc.step(ctx) end
Tracking Step Metrics
Log the most recent step’s timing and error:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.02)
local field = ooc.add_field(ctx, {256}, {
type = "double",
fill = 0.0
})
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 0.1,
wavenumber = 0.45,
omega = 0.2
})
local integrator = ooc.create_context_integrator(ctx, "rkf45", {
initial_dt = 0.02,
adaptive = true,
tolerance = 1.0e-4
})
ooc.set_integrator(ctx, integrator)
for i = 1, 500 do
ooc.step(ctx)
end
local m = ooc.step_metrics_latest(ctx)
ooc.log("last step: index=%d accepted_dt=%.4f rms_err=%.2e wall=%.2fms",
m.step_index, m.accepted_dt, m.rms_error, m.step_wall_ms)
Compute average wall time from the step history:
local history = ooc.step_metrics_history(ctx)
local total_ms = 0.0
local min_dt = math.huge
local max_dt = 0.0
for _, m in ipairs(history) do
total_ms = total_ms + m.step_wall_ms
if m.accepted_dt < min_dt then min_dt = m.accepted_dt end
if m.accepted_dt > max_dt then max_dt = m.accepted_dt end
end
ooc.log("samples=%d avg_wall=%.3fms dt_range=[%.5f, %.5f]",
#history, total_ms / #history, min_dt, max_dt)