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 ooc.LOG_LEVEL_TRACE|DEBUG|INFO|WARN|ERROR|FATAL.
  • Remaining arguments follow Lua string.format semantics; the message is formatted before printing.

Examples:

ooc.log("Hello from Lua")
ooc.log(ooc.LOG_LEVEL_DEBUG, "value=%.3f", 1.234)
ooc.log(ctx, ooc.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 = ooc.sim_async_logger_create(0)
logger:log(ooc.LOG_LEVEL_INFO, "Async Logger test...")
logger:log(ooc.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. Scheduler profiling requires enable_profiling = true in the config passed to sim_run. Adaptive timestep heuristics can be toggled with enable_timestep_heuristics (defaults to true).

sim_context_metrics(ctx) -> table

Returns summary metadata:

local metrics = ooc.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
ooc.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 = ooc.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 = ooc.sim_field_stats(ctx, 0)
-- Create a stats operator
local stats_op = ooc.sim_add_operator(ctx, "stats_op", function(context)
t = t + 1
-- Print results every 1000 steps
if t % 1000 == 0 then
ooc.log("-----------[ Field Stats ]----------")
ooc.log("%-24s %.10f", "Mean Real", stats.mean_re)
ooc.log("%-24s %.10f", "Mean Imag", stats.mean_im)
ooc.log("%-24s %.10f", "Var Real", stats.var_re)
ooc.log("%-24s %.10f", "Var Imag", stats.var_im)
ooc.log("%-24s %.10f", "Var Abs", stats.var_abs)
ooc.log("%-24s %.10f", "RMS", stats.rms)
ooc.log("%-24s %.10f", "Max Abs", stats.max_abs)
ooc.log("%-24s %.10f", "Phase Coherence", stats.phase_coherence)
ooc.log("%-24s %.10f", "Phase Coherence Weighted", stats.phase_coherence_weighted)
ooc.log("%-24s %.10f", "Phase Coherence EMA", stats.phase_coherence_ema)
ooc.log("%-24s %.10f", "Phase Coherence k0", stats.phase_coherence_k0)
ooc.log("%-24s %.10f", "Phase Regime", stats.phase_regime)
ooc.log("%-24s %.10f", "Circularity", stats.circularity)
ooc.log("%-24s %.10f", "Spectral Entropy", stats.spectral_entropy)
ooc.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

These helpers are lower-level building blocks behind sim_field_stats(...). They’re useful for tooling (custom UI, offline analysis, or custom sampling loops) where you want explicit control.

sim_field_view_from_field(field) -> view_table
sim_field_stats_compute(field) -> stats_table

view_table is a lightweight description of a field buffer:

  • data (lightuserdata) raw pointer (only present when available)
  • count (integer) number of elements
  • type (integer) field data type enum
  • type_name ("double" or "complex_double")

Use the accumulator when you want to feed samples manually (without having a Field):

sim_field_stats_accumulator_begin([acc_or_nil]) -> acc
sim_field_stats_accumulate_real(acc, value)
sim_field_stats_accumulate_complex(acc, re, im)
sim_field_stats_accumulator_finish(acc) -> stats_table
sim_field_stats_compute_spectral_view(field_or_view, stats_table) -> ok, dominant_k
sim_field_stats_compute_phase_metrics(field_or_view, stats_table, [phase_config], [dominant_k]) -> nil
sim_field_stats_update_phase_lock(ctx, field_index, stats_table) -> nil
  • sim_field_stats_compute_spectral_view updates stats_table (e.g. spectral entropy/bandwidth) and returns dominant_k (a frequency-bin index) when available.
  • sim_field_stats_compute_phase_metrics updates phase metrics (coherence/EMA/lock state) in-place; you can pass an explicit phase_config table and/or dominant_k.
  • sim_field_stats_update_phase_lock writes lock-state history back into the context for a given field_index (mutating stats_table in-place).
sim_field_stats_get_phase_config() -> config_table
sim_field_stats_set_phase_config(config_table)

config_table keys:

  • abs_threshold, rel_threshold
  • weighted (boolean)
  • lock_on, lock_off
  • smoothing_constant
  • deramp_enabled

These are the same underlying knobs as sim_set_phase_coherence_* (those convenience helpers update this global config).

These functions tune the phase coherence and phase-lock heuristics used by sim_field_stats. They accept a context for convenience, but update the global phase configuration (shared across contexts).

sim_set_phase_coherence_thresholds(ctx, abs_threshold, rel_threshold)
sim_set_phase_coherence_weighted(ctx, weighted)
sim_set_phase_coherence_lock_thresholds(ctx, lock_on, lock_off)
sim_set_phase_coherence_smoothing(ctx, smoothing_seconds)
sim_set_phase_coherence_deramp(ctx, enabled)

Notes:

  • sim_set_phase_coherence_thresholds treats negative inputs as “leave default”.
  • sim_set_phase_coherence_weighted(true) makes lock/EMA logic prefer phase_coherence_weighted over phase_coherence.
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 = ooc.sim_add_operator(ctx, "snap_op", function(context)
if t % 1000 == 0 then
ooc.log("-------[ Profiler Snapshot ]--------")
local snap = ooc.sim_profiler_snapshot(ctx)
ooc.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 = ooc.sim_add_operator(ctx, "profiler_op", function(context)
if t % 1000 == 0 then
ooc.log("-------[ Operator Profiler ]--------")
local entries, total_ms = ooc.sim_operator_profiler(ctx)
if entries then
for _, e in ipairs(entries) do
ooc.log("%-24s %.5f ms", e.name, e.inclusive_ms)
end
ooc.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

These are low-level profiler views intended for tooling and headless scripts:

sim_context_profiler_snapshot(ctx) -> table|nil
sim_context_profiler_counters(ctx) -> counters|nil

sim_context_profiler_counters returns an array of counter tables (plan index order) with:

  • inclusive_ns
  • invocations
  • delta_rms_sum
  • delta_sample_count

When the scheduler is running with profiling enabled, you can also query the scheduler’s last frame:

sim_scheduler_profiler_snapshot(ctx) -> table|nil
sim_scheduler_profiler_counters(ctx) -> counters|nil
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:record_operator_delta(index, delta_rms, [sample_count])
  • profiler:snapshot() -> {frame_start_ns, frame_end_ns, total_ns, average_operator_ns}

record_operator_delta records RMS-change metadata for an operator (native: sim_profiler_record_operator_delta).

Example:

local p = ooc.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 ooc.sim_run(ctx, config_table):

  • enable_profiling = true to collect scheduler profiler data for sim_profiler_snapshot / sim_operator_profiler (and sim_scheduler_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).
ooc.sim_run(ctx, {
enable_profiling = true,
enable_logging = true,
enable_timestep_heuristics = true,
})