Skip to content
Oakfield Operator Calculus Function Reference Site

Logging & Profiling

log([ctx], [level], fmt, ...)
  • If the first argument is a context, it is ignored for logging but allowed for convenience.
  • level is optional; defaults to INFO. Use sim.LOG_LEVEL_TRACE|DEBUG|INFO|WARN|ERROR|FATAL.
  • Remaining arguments follow Lua string.format semantics; the message is formatted before printing.

Examples:

sim.log("Hello from Lua")
sim.log(sim.LOG_LEVEL_DEBUG, "value=%.3f", 1.234)
sim.log(ctx, sim.LOG_LEVEL_WARN, "step %d exceeded threshold", step)
NameLevelDescription
SIM_LOG_LEVEL_TRACE0Very detailed debug information
SIM_LOG_LEVEL_DEBUG1General debug information
SIM_LOG_LEVEL_INFO2Informational messages
SIM_LOG_LEVEL_WARN3Warning conditions
SIM_LOG_LEVEL_ERROR4Error conditions
SIM_LOG_LEVEL_FATAL5Fatal conditions
sim_async_logger_create([capacity]) -> logger

Allocates an in-memory async ring-buffer logger (default capacity 256, floored to at least 64) and returns a Logger userdata. It does not touch the filesystem; records are queued for a consumer to read—safe for multi-threaded or asynchronous pipelines. Pass 0 or omit the argument to use the default capacity.

Logger methods:

  • logger:log(level, message): enqueue a record.
  • logger:pop(): returns the next record table {timestamp_ns, thread_id, level, message} or nil if empty.
  • logger:clear(): drop all queued records.

Example:

local logger = sim.sim_async_logger_create(0)
logger:log(sim.LOG_LEVEL_INFO, "Async Logger test...")
logger:log(sim.LOG_LEVEL_TRACE, string.format("N = %d, dt = %f", N, dt))
while true do
local rec = logger:pop()
if not rec then
break
end
print(ctx, string.format("[%d] %s", rec.level, rec.message))
end
  • log prepends level tags when not in GUI mode; in GUI mode it emits raw text.
  • Formatting errors raise a Lua error with the reason (log("%s %d", 1) will fail).
  • Use LOG_LEVEL_* constants exposed by luaopen_libsimcore for clarity.
  • When integrating with sim_on_step, keep log volume low to avoid slowing the simulation.

These helpers surface runtime data to Lua. Set enable_profiling = true in your config passed to sim_run to collect profiler counters. Adaptive timestep heuristics can be toggled with enable_timestep_heuristics (defaults to true).

sim_context_metrics(ctx) -> table

Returns summary metadata:

local metrics = sim.sim_context_metrics(ctx)
-- fields: field_count, operator_count, plan_operator_count, plan_valid,
-- backend ("CPU"|"CUDA"|"Metal"), step_index, time_seconds, dt, total_bytes
sim.log("Backend: %s, Field Count=%d, Operator Count=%d, dt=%.4f, Plan Valid=%s",
metrics.backend, metrics.field_count, metrics.operator_count, metrics.dt,
metrics.plan_valid and "True" or "False")
sim_field_stats(ctx_or_field, [field_index]) -> table|nil

Computes per-field statistics (mean, RMS, spectral entropy, phase coherence, continuity counts, etc.). You can pass either a field handle or an index. Returns nil if unavailable.

Available statistics:

StatisticDescription
mean_reMean of real parts
mean_imMean of imaginary parts
mean_absMean of magnitudes
rmsRoot mean square of magnitudes
var_reVariance of real parts
var_imVariance of imaginary parts
var_absVariance of magnitudes
max_absMaximum magnitude
phase_coherencePhase coherence metric
circularityCircularity metric
spectral_entropySpectral entropy
spectral_bandwidthSpectral bandwidth
phase_coherence_weightedWeighted phase coherence
phase_coherence_emaEMA of phase coherence
phase_coherence_k0Phase coherence k0
phase_sample_countNumber of phase samples
phase_lock_statePhase lock state
phase_regimePhase regime
countNumber of samples
-- Create a field
local field = sim.sim_add_field(ctx, {N}, {
type = "complex_double",
fill = {0.0, 0.0}
})
-- Local time step counter
local t = 0
-- Pointer to stats of field index 0
local stats = sim.sim_field_stats(ctx, 0)
-- Create a stats operator
local stats_op = sim.sim_add_operator(ctx, "stats_op", function(context)
t = t + 1
-- Print results every 1000 steps
if t % 1000 == 0 then
sim.log("-----------[ Field Stats ]----------")
sim.log("%-24s %.10f", "Mean Real", stats.mean_re)
sim.log("%-24s %.10f", "Mean Imag", stats.mean_im)
sim.log("%-24s %.10f", "Var Real", stats.var_re)
sim.log("%-24s %.10f", "Var Imag", stats.var_im)
sim.log("%-24s %.10f", "Var Abs", stats.var_abs)
sim.log("%-24s %.10f", "RMS", stats.rms)
sim.log("%-24s %.10f", "Max Abs", stats.max_abs)
sim.log("%-24s %.10f", "Phase Coherence", stats.phase_coherence)
sim.log("%-24s %.10f", "Phase Coherence Weighted", stats.phase_coherence_weighted)
sim.log("%-24s %.10f", "Phase Coherence EMA", stats.phase_coherence_ema)
sim.log("%-24s %.10f", "Phase Coherence k0", stats.phase_coherence_k0)
sim.log("%-24s %.10f", "Phase Regime", stats.phase_regime)
sim.log("%-24s %.10f", "Circularity", stats.circularity)
sim.log("%-24s %.10f", "Spectral Entropy", stats.spectral_entropy)
sim.log("%-24s %.10f", "Spectral Bandwidth", stats.spectral_bandwidth)
end
return true -- or return 0 (SIM_RESULT_OK)
end)

Example output:

Terminal window
-----------[ Field Stats ]----------
Mean Real 0.0004454985
Mean Imag -0.0003130782
Var Real 0.0365573493
Var Imag 0.0622247355
Var Abs 0.0198729688
RMS 0.3142966455
Max Abs 0.4487944696
Phase Coherence 0.0232840599
Phase Coherence Weighted 0.0019383776
Phase Coherence EMA 0.0019383776
Phase Coherence k0 0.8311182350
Phase Regime 3.0000000000
Circularity 0.2664554384
Spectral Entropy 0.2546378467
Spectral Bandwidth 3.7603059001
sim_profiler_snapshot(ctx) -> table|nil

Returns a snapshot of the last profiled frame with fields:

  • frame_start_ns
  • frame_end_ns
  • total_ns
  • average_operator_ns
local t = 0
local snap_op = sim.sim_add_operator(ctx, "snap_op", function(context)
if t % 1000 == 0 then
sim.log("-------[ Profiler Snapshot ]--------")
local snap = sim.sim_profiler_snapshot(ctx)
sim.log("Frame=%g ns, Average Op=%g ns",
snap.total_ns, snap.average_operator_ns)
end
return true
end)
sim_operator_profiler(ctx) -> entries, total_ms

Per-operator timing and RMS deltas for the last profiled frame. Returns a list of entries with fields:

  • name
  • inclusive_ms
  • rms_delta
local profiler_op = sim.sim_add_operator(ctx, "profiler_op", function(context)
if t % 1000 == 0 then
sim.log("-------[ Operator Profiler ]--------")
local entries, total_ms = sim.sim_operator_profiler(ctx)
if entries then
for _, e in ipairs(entries) do
sim.log("%-24s %.5f ms", e.name, e.inclusive_ms)
end
sim.log("%-24s %.5f ms", "total", total_ms)
end
end
return true
end)

Example output:

Terminal window
-------[ Operator Profiler ]--------
mixer_53#0 0.00029 ms
stimulus_sine_54#0 0.00000 ms
stimulus_sine_55#0 0.00000 ms
mixer_56#0 0.00000 ms
profiler_op 0.00000 ms
total 0.00029 ms
sim_profiler_new(operator_count) -> profiler

Creates a standalone profiler userdata you can drive manually (distinct from the scheduler profiler). Methods:

  • profiler:begin_frame()
  • profiler:end_frame()
  • profiler:record_operator(index, duration_ns)
  • profiler:snapshot() -> {frame_start_ns, frame_end_ns, total_ns, average_operator_ns}

Example:

local p = sim.sim_profiler_new(3)
p:begin_frame()
-- ... measure work ...
p:record_operator(1, 250000) -- 0-based index
p:end_frame()
local snap = p:snapshot()
print("manual frame ns", snap.total_ns)

Pass these in the table to sim.sim_run(ctx, config_table):

  • enable_profiling = true to collect scheduler profiler data for sim_profiler_snapshot / sim_operator_profiler.
  • enable_logging = true to turn on the async logger backend.
  • enable_timestep_heuristics = false to disable adaptive dt decisions (and sim_timestep_decision will report available = false).
sim.sim_run(ctx, {
enable_profiling = true,
enable_logging = true,
enable_timestep_heuristics = true,
})