Custom Operators
Creating Custom Operators
Custom operators let you extend the simulation with your own Lua callbacks. They execute alongside built-in operators in the scheduler and have full access to fields, parameters, and simulation state.
Workflow
- Register —
ooc.add_operator(ctx, name, fn)installs your callback and returns an operator handle. - Capture index — Store the operator index (
ooc.operator_count(ctx) - 1) if you need to read parameters inside the callback. - Expose params (optional) —
ooc.operator_set_params(ctx, op, params)publishes typed parameters for UI and introspection. - Modify at runtime — Use
ooc.operator_param_set()to change parameters of any operator (including built-in ones).
Registration API
Method Signature
local op = ooc.add_operator(ctx, name, callback) --> operator_handle
| Parameter | Type | Description |
|---|---|---|
ctx |
context |
Simulation context |
name |
string |
Display name for the operator |
callback |
function |
Function called each simulation step; receives context, returns true/false or a SimResult code |
Return Values
The callback should return:
trueor0— Success (SIM_RESULT_OK)falseor error code — Halt simulation
Accessing the Operator Index
The operator index is needed to read parameters inside callbacks. Capture it immediately after registration:
local my_op_idx
local my_op = ooc.add_operator(ctx, "My Operator", function(c)
-- use my_op_idx here to read params
local value = ooc.operator_param(c, my_op_idx, "gain")
return true
end)
my_op_idx = ooc.operator_count(ctx) - 1
Parameter Schema
Expose typed parameters for UIs and introspection with operator_set_params:
ooc.operator_set_params(ctx, op, {
{ name = "gain", type = "float", default = 1.0, min = 0.0, max = 10.0 },
{ name = "field", type = "field", default = 0 },
{ name = "mode", type = "enum", options = {"fast", "accurate"}, default = "fast" },
})
Supported Types
| Type | Required Keys | Optional Keys |
|---|---|---|
float |
name |
default, min, max, description |
integer |
name |
default, min, max, description |
boolean |
name |
default, description |
enum |
name, options |
default, description |
field |
name |
default, description |
Runtime Parameter Modification
-- Set a float/integer/boolean parameter
ooc.operator_param_set(ctx, operator_index, "param_name", value)
-- Set an enum parameter
ooc.operator_param_enum_set(ctx, operator_index, "param_name", "option_string")
-- Set a field parameter
ooc.operator_param_field_set(ctx, operator_index, "param_name", field_handle)
Tip
You can modify parameters of any operator (built-in or custom) by index. This enables powerful meta-control patterns like parameter sweeps and adaptive algorithms.
Example 1: Parameter Sweep
Dynamically modulate parameters of other operators over time. This pattern is useful for exploring parameter spaces or creating time-varying effects.
local function build_context()
local N = 768
local dt = 0.025
local ctx = ooc.create()
ooc.set_timestep(ctx, dt)
local field = ooc.add_field(ctx, {N}, {
type = "complex_double",
fill = {0.0, 0.0}
})
-- Stimulus operator at index 0
ooc.add_stimulus_operator(ctx, field, {
type = "stimulus_sine",
amplitude = 1.0,
wavenumber = 1,
omega = -0.5,
scale_by_dt = false
})
-- Custom sweep operator
local t = 0
local sweep_op = ooc.add_operator(ctx, "Parameter Sweep", function(c)
t = t + 1
-- Slowly oscillate the wavenumber of the stimulus (operator index 0)
local sweep = math.sin(t * 0.00001) * 2.0
ooc.operator_param_set(ctx, 0, "wavenumber", sweep)
return true
end)
return ctx
end
return build_context()
Key points:
- The sweep operator modifies parameter
wavenumberon operator index0(the stimulus) - Uses a simple counter
tto track simulation steps operator_param_setworks on any operator, not just custom ones
Accessing Profiler Data
Custom operators can query the profiler for performance monitoring:
local profiler_op = ooc.add_operator(ctx, "Profiler", function(c)
if t % 1000 == 0 then
local entries, total_ms = ooc.operator_profiler(ctx)
if entries then
for _, e in ipairs(entries) do
ooc.log("%-20s %.3f ms", e.name, e.inclusive_ms)
end
ooc.log("Total: %.3f ms", total_ms)
end
end
return true
end)
Notes & Best Practices
-
Performance: Keep Lua operators lightweight. Heavy per-element loops bottleneck CPU execution. For numerical work, use built-in operators that run on the GPU.
-
Write masks: The scheduler does not infer write masks for Lua operators. Avoid side effects outside the fields you explicitly manage.
-
Field access: Use
field:values()to get a Lua table of values, andfield:set_values(v)to write back. These copy data between Lua and the simulation. -
Logging: Use
ooc.log(format, ...)for printf-style debug output. Avoid excessive logging in tight loops. -
Return values: Always return
true(or0) on success. Returningfalseor an error code halts the simulation. -
Operator ordering: Operators execute in registration order. If your custom operator depends on another operator’s output, register it after.