Oakfield API

Accurate, implementation-derived guide to scripting Oakfield simulations.

1. Overview

Oakfield exposes a composable operator calculus through Lua. A script creates a SimContext, adds fields and operators, assigns an integrator, then advances time. All APIs documented here are verified against the current source.

Key pillars:
Fields: discrete storage (real or complex) over an N‑D grid
Operators: registered transformations (native kernels or Lua callbacks)
Integrators: advance context by dt (adaptive or fixed)
Runtime services: async logger, profiler, visual mode selection

2. Indexing & Conventions

  • Field indices, operator indices, and integrator references inside the engine are zero-based.
  • Lua tables returned to user (e.g. field:shape(), field:values()) are 1-based (Lua default).
  • Complex numbers are passed either as a plain number (real) or {re, im} table.
  • Enum values and modes exposed as integers also have string helpers where provided.

3. Userdata & GC Semantics

TypeGC Behavior
ContextDestroys context, releases managed integrators, frees native memory
FieldDetaches handle only; underlying field owned by context until context GC
OperatorDetaches handle only; operator lifetime owned by context
IntegratorDestroys integrator and frees workspace
Logger / ProfilerDestroys async logger/profiler and frees memory

Detaching (field/operator) means subsequent method calls on that handle error (expected Field userdata).

4. Context API

Creation

sim.sim_create([config]) -> context

Optional config table keys include continuity override and runtime tuning. Boundary accepts neumann|dirichlet|periodic|reflective (default neumann).

Time & Timestep

sim.sim_set_timestep(ctx, dt)
sim.sim_get_timestep(ctx) -> number
sim.sim_get_time(ctx) -> number
sim.sim_get_step_index(ctx) -> integer

Visual Mode

sim.sim_set_visual_mode(ctx, mode)
sim.sim_get_visual_mode(ctx) -> (int, string)

Mode can be integer or one of: phase_portrait, waveform, phase, energy, frequency, polar.

5. Field API

Field Creation

sim.sim_add_field(ctx, shape_table[, options]) -> Field

Parameters:

  • shape_table: 1+ positive integers
  • options.type: double (default), float, complex_double, complex_float
  • options.fill: number or {re, im}
  • options.initializer: function(i, coord) → value (1D only)
  • options.origin, options.spacing: coordinate generation

Field Methods

field:rank() -> integer
field:shape() -> {dims...}
field:components() -> integer
field:values() -> { ... }

Example: 1D Field with Sinusoidal Initialization

local field = sim.sim_add_field(ctx, {512}, {
    initializer = function(i)
        return math.sin(2 * math.pi * i / 512)
    end
})

Example: Complex Field

local complex_field = sim.sim_add_field(ctx, {512}, {
    type = "complex_double",
    initializer = function(i)
        local x = i * 0.01
        return {math.cos(x), math.sin(x)}
    end,
    spacing = 0.01
})

6. Operator API

User-Defined Operators

sim.sim_add_operator(ctx, name, callback[, dependencies]) -> Operator

Callback receives context and returns integer SimResult code or boolean (true=OK, false=error).

local op = sim.sim_add_operator(ctx, 'tick_counter', function(context)
    tick = tick + 1
    return true  -- or return 0 (SIM_RESULT_OK)
end)

Stimulus Operators

sim.sim_add_stimulus_operator(ctx, field, config)

Stimulus types: sine, standing_wave, chirp, gaussian_pulse, gaussian_travel, fbm, checkerboard, white_noise, random_fourier, spectral_lines, gabor

-- Sine wave stimulus
local stim = sim.sim_add_stimulus_operator(ctx, field, {
    type = "sine",
    amplitude = 0.5,
    frequency = 2.0,
    phase = 0.0
})

Linear Dissipative Operators

sim.sim_add_linear_dissipative_operator(ctx, field, config)

Config: viscosity, alpha (fractional order ∈ (0,2]), spacing

-- Classical diffusion
sim.sim_add_linear_dissipative_operator(ctx, field, {
    viscosity = 2.5,
    alpha = 1.0,
    spacing = 0.1
})

Stochastic Noise Operators

sim.sim_add_stochastic_noise_operator(ctx, field, config)

Config: sigma (intensity), tau (correlation time), alpha (spectral exponent), seed, law (ito/stratonovich)

Analytic Warp Operators

sim.sim_add_analytic_warp_operator(ctx, field, config)

Profiles: digamma, trigamma, power, tanh

sim.sim_add_analytic_warp_operator(ctx, field, {
    profile = "tanh",
    lambda = 0.5
})

Mixer Operators

sim.sim_add_mixer_operator(ctx, lhs_field, rhs_field, output_field, config)

Modes: linear, multiply, divide

-- Compute u²
local u2 = sim.sim_add_field(ctx, {N}, {fill = 0.0})
sim.sim_add_mixer_operator(ctx, u, u, u2, {
    mode = "multiply"
})

7. Integrator API

Creation

sim.sim_create_context_integrator(ctx, name[, config]) -> Integrator

Available integrators: euler, euler_deterministic, heun, rk4, rkf45, backward_euler, crank_nicolson, subordination

Example: Fixed-Step RK4

local integrator = sim.sim_create_context_integrator(ctx, "rk4", {
    initial_dt = 0.01,
    adaptive = false
})
sim.sim_set_integrator(ctx, integrator)

Example: Adaptive RKF45

local integrator = sim.sim_create_context_integrator(ctx, "rkf45", {
    initial_dt = 0.01,
    adaptive = true,
    min_dt = 1e-4,
    max_dt = 0.1,
    tolerance = 1e-6,
    safety = 0.8
})

8. Simulation Control

Basic Stepping

sim.sim_step(ctx) -> boolean

Prepares plan, executes one step, updates time & index.

Example: Simulation Loop

for step = 1, 1000 do
    local success = sim.sim_step(ctx)
    if not success then
        print("Simulation failed at step", step)
        break
    end
    
    -- Periodic monitoring
    if step % 100 == 0 then
        local values = field:values()
        -- Process values...
    end
end

9. Logging API

Creation

sim.sim_async_logger_create(ctx[, config]) -> Logger

Methods

logger:log(level_int, message) -> true
logger:pop() -> { timestamp_ns, thread_id, level, message } | nil
logger:clear()

Log levels: LOG_LEVEL_TRACE, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL

10. Special Functions

Scalar / complex evaluations with automatic argument dispatch (number → real, {re,im} → complex):

sim.digamma(z) - Digamma function ψ(x) = d/dx ln Γ(x)
sim.trigamma(z) - Trigamma function ψ'(x)
sim.hyperexp(x, ...) - Hyperexponential function
sim.qhyperexp(phi, epsilon, n, q) - q-deformed hyperexponential
sim.qnumber(x, q) - q-number
sim.qzeta(s, q) - q-zeta function
sim.qdigamma(z, q) - q-digamma function

Example Usage

local psi = sim.digamma(1.25)
local psi_complex = sim.digamma({0.75, 0.5})
local phi = sim.hyperexp(0.5, 0.05, 32)
local qphi = sim.qhyperexp({0.4, 0.25}, {0.0, -0.1}, 24, 0.9)

11. Complete Examples

Example 1: Basic Heat Equation

Simple diffusion with visualization hint.

local sim = require("libsimcore")

local N = 512
local dt = 0.01

local ctx = sim.sim_create()
sim.sim_set_timestep(ctx, dt)
sim.sim_set_visual_mode(ctx, "waveform")

-- Initial condition: Gaussian pulse
local field = sim.sim_add_field(ctx, {N}, {
    initializer = function(i)
        local x = (i - N/2) / N
        return math.exp(-50 * x * x)
    end
})

-- Diffusion operator
sim.sim_add_linear_dissipative_operator(ctx, field, {
    viscosity = 1.0,
    alpha = 1.0,
    spacing = 1.0 / N
})

-- Fixed-step integration
local integrator = sim.sim_create_context_integrator(ctx, "rk4", {
    initial_dt = dt,
    adaptive = false
})
sim.sim_set_integrator(ctx, integrator)

-- Run simulation
for step = 1, 1000 do
    sim.sim_step(ctx)
end

return ctx

Example 2: Stochastic Differential Equation

Combines dissipation and noise.

local sim = require("libsimcore")

local N = 256
local dt = 0.005

local ctx = sim.sim_create()
sim.sim_set_timestep(ctx, dt)

-- Initialize with sine wave
local field = sim.sim_add_field(ctx, {N}, {
    initializer = function(i)
        return math.sin(2 * math.pi * i / N)
    end
})

-- Fractional dissipation
sim.sim_add_linear_dissipative_operator(ctx, field, {
    viscosity = 2.0,
    alpha = 0.7,
    spacing = 0.1
})

-- Additive noise
sim.sim_add_stochastic_noise_operator(ctx, field, {
    sigma = 0.1,
    tau = 0.05,
    seed = 42
})

-- Adaptive integration
local integrator = sim.sim_create_context_integrator(ctx, "rkf45", {
    initial_dt = dt,
    adaptive = true,
    min_dt = 1e-4,
    max_dt = 0.05,
    tolerance = 1e-5
})
sim.sim_set_integrator(ctx, integrator)

for step = 1, 500 do
    sim.sim_step(ctx)
    
    if step % 50 == 0 then
        local metrics = sim.sim_step_metrics_latest(ctx)
        if metrics then
            sim.log(sim.LOG_LEVEL_INFO, "Step %d: dt=%.6f", 
                    step, metrics.accepted_dt)
        end
    end
end

return ctx

Example 3: Nonlinear PDE with Thermostat

Nonlinear dynamics with energy regulation.

local sim = require("libsimcore")

local N = 128
local dt = 0.01

local ctx = sim.sim_create()
sim.sim_set_timestep(ctx, dt)

-- Primary field
local u = sim.sim_add_field(ctx, {N}, {
    initializer = function(i)
        return 0.5 * math.sin(4 * math.pi * i / N)
    end
})

-- Dissipation
sim.sim_add_linear_dissipative_operator(ctx, u, {
    viscosity = 0.5,
    alpha = 1.0,
    spacing = 1.0 / N
})

-- Compute u^3 for cubic nonlinearity
local u2 = sim.sim_add_field(ctx, {N}, {fill = 0.0})
sim.sim_add_mixer_operator(ctx, u, u, u2, {mode = "multiply"})

local u3 = sim.sim_add_field(ctx, {N}, {fill = 0.0})
sim.sim_add_mixer_operator(ctx, u, u2, u3, {mode = "multiply"})

-- Add nonlinear term: -0.5*u + 0.2*u^3
local nl = sim.sim_add_field(ctx, {N}, {fill = 0.0})
sim.sim_add_mixer_operator(ctx, u, u3, nl, {
    mode = "linear",
    lhs_gain = -0.5,
    rhs_gain = 0.2,
    accumulate = true
})

-- Energy regulation
sim.sim_add_thermostat_operator(ctx, u, {
    mode = sim.THERMOSTAT_SOFT_LAMBDA,
    E_target = 1.0,
    lambda_soft_gain = 0.15
})

-- Integrator
local integrator = sim.sim_create_context_integrator(ctx, "rk4", {
    initial_dt = dt,
    adaptive = false
})
sim.sim_set_integrator(ctx, integrator)

for step = 1, 1000 do
    sim.sim_step(ctx)
end

return ctx

12. Best Practices

1. Initialize Fields Carefully

Field initializer callbacks receive 0-based indices from C, but Lua arrays are 1-based. Be mindful of the indexing when mapping to coordinates.

-- ✓ Correct: Index i is already 0-based from C
local field = sim.sim_add_field(ctx, {N}, {
    initializer = function(i)
        local x = i / (N - 1)  -- Map to [0, 1]
        return math.sin(2 * math.pi * x)
    end
})

2. Use Appropriate Integrators

  • Deterministic, smooth systems: rkf45 (adaptive) or rk4 (fixed)
  • Simple testing / fixed step: euler, rk4
  • Implicit / stiff drift: backward_euler, crank_nicolson
  • Fractional derivatives: subordination
  • Stochastic systems: euler, heun

3. Monitor Stability

Check field values periodically to detect numerical instabilities:

local values = field:values()
local max_abs = 0
for _, v in ipairs(values) do
    max_abs = math.max(max_abs, math.abs(v))
end
if max_abs > 1e10 then
    sim.log(sim.LOG_LEVEL_ERROR, "Instability detected!")
    break
end

4. Use Constants Over Strings

Prefer enum constants for better performance and type safety:

-- ✓ Better
profile = sim.ANALYTIC_WARP_PROFILE_TRIGAMMA
mode = sim.THERMOSTAT_SOFT_LAMBDA

-- ✗ Less efficient
profile = "trigamma"
mode = "soft_lambda"

5. Handle Complex Fields Correctly

Complex fields return tables of {real, imag}:

local values = complex_field:values()
for i, v in ipairs(values) do
    local re, im = v[1], v[2]
    local magnitude = math.sqrt(re^2 + im^2)
    local phase = math.atan2(im, re)
    -- ...
end

Ready to start building?