Running Simulations
On this page
Drive Modes
Oakfield contexts can be driven in three common ways:
- Call
step(ctx)yourself in a script-controlled loop. - Call
run(ctx, [config])and let the scheduler own the loop. - Claim external-driver ownership and cooperate with host or embedder control APIs.
Script-controlled Stepping
When running simulations from the command line, you will usually own the loop directly:
local ctx = ooc.create({ worker_count = 1 })
-- add fields/operators...
local integrator = ooc.create_context_integrator(ctx, "rkf45", {
initial_dt = 0.01,
adaptive = true,
})
ooc.set_integrator(ctx, integrator)
for step = 1, 10000 do
if not ooc.step(ctx) then
ooc.log(ooc.LOG_LEVEL_ERROR, "step %d failed", step)
break
end
if step % 100 == 0 then
local m = ooc.step_metrics_latest(ctx)
if m then
ooc.log("t=%.3f dt=%.4f rms=%.2e",
ooc.get_time(ctx), m.accepted_dt, m.rms_error)
end
end
end
ooc.shutdown(ctx)
Note: How
stepExecutes
step(ctx)always advances exactly one accepted step, but the runtime may choose either:
- a scheduler-backed single-step path, or
- the legacy single-threaded path
The scheduler path is typically chosen when multi-worker execution is requested, an integrator is attached, and no Lua operator callbacks force a fallback.
Scheduler Ownership
These helpers start and control the long-running scheduler loop:
run(ctx, [config])
pause(ctx)
resume(ctx)
shutdown(ctx)
is_running(ctx) -> boolean
run(ctx, [config]) reuses the context’s stored config and optionally patches it with the same recognized keys as create, such as worker_count, enable_logging, enable_profiling, enable_timestep_heuristics, frame_time_budget_ms, backend, backend_fallback, and log_path.
ooc.run(ctx, {
worker_count = 4,
enable_logging = true,
log_path = "logs/scheduler.log",
})
Use pause / resume only when the scheduler currently owns the loop. shutdown stops the context and releases runtime resources.
Driver State Snapshot
For host integration and debugging, use:
driver_state(ctx) -> state|nil
The returned table includes:
active_modeandownerfor the current driver arrangementscheduler_state,running, andpausedscheduler_initializedintegrator_attachedandintegrator_sequence_countstep_metrics_availableandstep_metrics_countexternal_driver_depthinspect_allowed,export_allowed, andmutation_allowedcaller_loop_control_allowedandscheduler_control_allowedaccess_reasonloop_progresswhen a bounded or cooperative loop is active
Minimal example:
local state = ooc.driver_state(ctx)
if state then
ooc.log("owner=%s running=%s paused=%s",
state.owner, tostring(state.running), tostring(state.paused))
end
External Driver Control
Embedders can explicitly claim loop ownership:
begin_external_driver(ctx) -> depth
end_external_driver(ctx) -> depth
with_external_driver(ctx, fn) -> ...
with_external_driver is the safest pattern. It acquires ownership, calls fn(ctx), then releases ownership even if you nest driver scopes elsewhere.
ooc.with_external_driver(ctx, function(context)
for i = 1, 128 do
if not ooc.step(context) then
break
end
end
end)
Cooperative Loop Helpers
These APIs help external drivers coordinate cancellation, pause, and bounded sleeps:
loop_checkpoint(ctx) -> checkpoint
request_loop_stop(ctx) -> checkpoint
clear_loop_stop(ctx) -> checkpoint
loop_cooperate(ctx, [opts]) -> checkpoint
The checkpoint table includes:
step_indexsim_timepending_cancelshould_stoplast_stop_reasonhas_errorlast_errorprogressdriver
loop_cooperate(ctx, { sleep_seconds = x }) clamps the cooperative sleep request into the runtime’s safe range before sleeping, then returns the same checkpoint table plus:
sleep_requested_secondssleep_secondssleep_appliedsleep_clamped
Example:
ooc.with_external_driver(ctx, function(context)
while true do
if not ooc.step(context) then
break
end
local cp = ooc.loop_cooperate(context, { sleep_seconds = 0.01 })
if cp.should_stop then
break
end
end
end)
Scheduler Logs
scheduler_drain_logs(ctx, [min_level], [callback]) -> boolean
Accepted call patterns:
scheduler_drain_logs(ctx)drains and discards all pending recordsscheduler_drain_logs(ctx, "warn")drains onlywarnand abovescheduler_drain_logs(ctx, function(level, message) ... end)drains and invokes the callbackscheduler_drain_logs(ctx, "info", function(level, message) ... end)combines filtering and callbacks
This only produces records when scheduler logging is enabled.
GUI / Host-returned Contexts
In GUI or embedder scripts, the common pattern is to build a context and return it:
local ctx = ooc.create()
ooc.set_timestep(ctx, 0.025)
local field = ooc.add_field(ctx, {512}, {
type = "complex_double",
fill = {0.0, 0.0},
})
ooc.add_zero_field_operator(ctx, field)
ooc.set_integrator(ctx, ooc.create_context_integrator(ctx, "euler"))
return ctx
The host can then inspect driver_state, claim external ownership, or run the scheduler around that returned context.
Applying Snapshots
apply_snapshot(ctx, snapshot_table) -> applied_count
Each snapshot entry must include:
nameschemablobas a hex string
Optional fields:
indexas an operator index hintsizeas the expected decoded blob size
Entries that do not decode cleanly are skipped. If the native snapshot apply fails, the binding raises a Lua error and reports how many overrides were already applied.
local applied = ooc.apply_snapshot(ctx, {
{
name = "stimulus_sine#0",
schema = "stimulus_sine",
blob = "<hex...>",
}
})
ooc.log("applied %d snapshot entries", applied)