Custom Operators
Creating Custom Operators
Section titled “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
Section titled “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
Section titled “Registration API”Method Signature
Section titled “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
Section titled “Return Values”The callback should return:
trueor0— Success (SIM_RESULT_OK)falseor error code — Halt simulation
Accessing the Operator Index
Section titled “Accessing the Operator Index”The operator index is needed to read parameters inside callbacks. Capture it immediately after registration:
local my_op_idxlocal 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 trueend)my_op_idx = ooc.operator_count(ctx) - 1Parameter Schema
Section titled “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
Section titled “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
Section titled “Runtime Parameter Modification”-- Set a float/integer/boolean parameterooc.operator_param_set(ctx, operator_index, "param_name", value)
-- Set an enum parameterooc.operator_param_enum_set(ctx, operator_index, "param_name", "option_string")
-- Set a field parameterooc.operator_param_field_set(ctx, operator_index, "param_name", field_handle)Example 1: Parameter Sweep
Section titled “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 ctxend
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
Section titled “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 trueend)Notes & Best Practices
Section titled “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.