Skip to content
Oakfield Operator Calculus Function Reference Site

Running Simulations

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.

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)

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.

For host integration and debugging, use:

driver_state(ctx) -> state|nil

The returned table includes:

  • active_mode and owner for the current driver arrangement
  • scheduler_state, running, and paused
  • scheduler_initialized
  • integrator_attached and integrator_sequence_count
  • step_metrics_available and step_metrics_count
  • external_driver_depth
  • inspect_allowed, export_allowed, and mutation_allowed
  • caller_loop_control_allowed and scheduler_control_allowed
  • access_reason
  • loop_progress when 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

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)

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_index
  • sim_time
  • pending_cancel
  • should_stop
  • last_stop_reason
  • has_error
  • last_error
  • progress
  • driver

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_seconds
  • sleep_seconds
  • sleep_applied
  • sleep_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_drain_logs(ctx, [min_level], [callback]) -> boolean

Accepted call patterns:

  • scheduler_drain_logs(ctx) drains and discards all pending records
  • scheduler_drain_logs(ctx, "warn") drains only warn and above
  • scheduler_drain_logs(ctx, function(level, message) ... end) drains and invokes the callback
  • scheduler_drain_logs(ctx, "info", function(level, message) ... end) combines filtering and callbacks

This only produces records when scheduler logging is enabled.

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.

apply_snapshot(ctx, snapshot_table) -> applied_count

Each snapshot entry must include:

  • name
  • schema
  • blob as a hex string

Optional fields:

  • index as an operator index hint
  • size as 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)