Operator Calculus, Co.
Browse Studio documentation

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

  1. Register — ooc.add_operator(ctx, name, fn) installs your callback and returns an operator handle.
  2. Capture index — Store the operator index (ooc.operator_count(ctx) - 1) if you need to read parameters inside the callback.
  3. Expose params (optional) — ooc.operator_set_params(ctx, op, params) publishes typed parameters for UI and introspection.
  4. 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:

  • true or 0 — Success (SIM_RESULT_OK)
  • false or 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 wavenumber on operator index 0 (the stimulus)
  • Uses a simple counter t to track simulation steps
  • operator_param_set works 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, and field: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 (or 0) on success. Returning false or 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.