Actuation

July 7, 2026 · View on GitHub

The actuation subsystem converts high-level supervisor decisions into bounded, rate-limited control commands. It sits between the supervisor (which decides what to change) and the physical/virtual actuators (which execute the change). This separation of concerns is critical for safety: the supervisor can propose aggressive actions, but the actuation layer enforces physical constraints.

Pipeline position

SupervisorPolicy.decide()


  list[ControlAction]


  ActuationMapper.map_actions()     ← routing by knob + scope


  ActionProjector.project()         ← rate limit + value bounds


  Actuator commands (dict)          → Modbus/gRPC/HTTP output


  UPDEEngine.step(knm + ΔK, zeta + Δζ, ...)  ← next cycle

The actuation subsystem is the output adapter of the SPO pipeline. Without it, supervisor decisions would be unbounded floating-point values that could crash the integrator.


Control Actions

A ControlAction is the universal message format between supervisor and actuators:

ControlAction (dataclass)

FieldTypeDescription
knobstrParameter to change: K, zeta, psi, alpha
scopestrTarget: global or layer_{n}
valuefloatProposed parameter value
ttl_sfloatTime-to-live in seconds
justificationstrHuman-readable reason (audit trail)

Knob semantics

KnobEngine parameterEffect
KCoupling strength K_ijIncreases/decreases synchronisation pull
zetaExternal drive amplitude ζDamping or excitation
psiExternal drive phase ΨPhase of external reference
alphaPhase lag α_ijShifts preferred phase relationships

Actuation Mapper

Maps control actions to actuator-specific command dictionaries.

ActuatorMapping (dataclass)

FieldTypeDescription
namestrActuator identifier
knobstrWhich control knob it responds to
scopestrWhich layer(s) it affects
limitstuple[float, float](lo, hi) value bounds

ActuationMapper

ActuationMapper(mappings: list[ActuatorMapping])

Methods:

MethodSignatureDescription
map_actions(actions: list[ControlAction]) → list[dict]Route actions to actuators
validate_action(action: ControlAction) → boolCheck if any actuator handles this knob+scope

Routing rules

  1. Action's knob must match an actuator's knob
  2. Action's scope must match an actuator's scope (or "global" matches all)
  3. Action's value is clamped to actuator's limits
  4. Unroutable actions are silently dropped (no matching actuator)

Usage

from scpn_phase_orchestrator.actuation.mapper import ActuationMapper, ControlAction
from scpn_phase_orchestrator.binding.types import ActuatorMapping

mappings = [
    ActuatorMapping(name="K_amp", knob="K", scope="global", limits=(0.0, 5.0)),
    ActuatorMapping(name="zeta_drive", knob="zeta", scope="global", limits=(0.0, 1.0)),
]
mapper = ActuationMapper(mappings)

actions = [ControlAction(knob="K", scope="global", value=3.0, ttl_s=5.0,
                         justification="MPC pre-emptive boost")]
commands = mapper.map_actions(actions)

Edge cases

InputBehaviour
Empty mappingsmap_actions() returns [] for any input
Empty actionsReturns []
No matching actuatorAction silently dropped
validate_action() on unroutableReturns False

Performance: map_actions() < 10 μs.

::: scpn_phase_orchestrator.actuation.mapper


Action Projector

Safety layer that enforces value bounds and rate limits on control actions before they reach actuators.

Safety requirements

IDRequirementEnforcement
SR-1Output value within [lo, hi]Value bounds clamp
SR-2Maximum change ≤ rate_limit per stepRate limit clamp

Constructor

ActionProjector(
    rate_limits: dict[str, float],    # {"K": 0.1, "zeta": 0.05}
    value_bounds: dict[str, tuple[float, float]],  # {"K": (0.0, 1.0)}
)

project()

def project(action: ControlAction, previous_value: float) -> ControlAction

Given previous value v_prev and proposed value v_new:

Δ = v_new - v_prev
Δ_clamped = clamp(Δ, -rate_limit, +rate_limit)
v_projected = clamp(v_prev + Δ_clamped, v_min, v_max)

The returned ControlAction has the same knob, scope, ttl_s, and justification — only value is modified.

Rate limit motivation

Rate limits prevent discontinuous jumps that destabilise the phase dynamics. A coupling strength that jumps from 0.1 to 5.0 in one step can cause the Euler integrator to diverge (CFL violation). The projector ensures smooth transitions.

Consecutive-step guarantee

Over N consecutive steps, the maximum total change is bounded by N × rate_limit. This provides a formal guarantee on the maximum slew rate of any actuated parameter.

Unbounded knobs

Knobs not in rate_limits or value_bounds pass through unmodified. This allows domain-specific knobs to bypass the projector when safety constraints are not applicable.

Performance: project() < 10 μs.

::: scpn_phase_orchestrator.actuation.constraints


Closed-loop feedback example

from scpn_phase_orchestrator.actuation.constraints import ActionProjector
from scpn_phase_orchestrator.actuation.mapper import ActuationMapper, ControlAction
from scpn_phase_orchestrator.supervisor.policy import SupervisorPolicy
from scpn_phase_orchestrator.supervisor.regimes import RegimeManager
from scpn_phase_orchestrator.upde.engine import UPDEEngine
from scpn_phase_orchestrator.upde.order_params import compute_order_parameter

# Setup
eng = UPDEEngine(n=8, dt=0.01)
pol = SupervisorPolicy(RegimeManager())
proj = ActionProjector(
    rate_limits={"K": 0.1, "zeta": 0.05},
    value_bounds={"K": (0.0, 5.0), "zeta": (0.0, 1.0)},
)

# Feedback loop
k_current = 0.5
zeta_current = 0.0
for _ in range(1000):
    phases = eng.step(phases, omegas, knm, zeta_current, 0.0, alpha)
    r, psi = compute_order_parameter(phases)
    state = build_upde_state(r, psi)
    actions = pol.decide(state, boundary)
    for a in actions:
        if a.knob == "K":
            safe = proj.project(a, previous_value=k_current)
            k_current = safe.value
        elif a.knob == "zeta":
            safe = proj.project(a, previous_value=zeta_current)
            zeta_current = safe.value

Output protocols

The actuation subsystem can drive multiple output protocols:

ProtocolAdapterUse case
Modbus/TLSmodbus_tlsIndustrial controllers (PLC, DCS)
gRPCgrpc_serviceDistributed SPO nodes
HTTP/RESTserverWeb dashboard, external APIs
Redisredis_storeState persistence, pub/sub
DirectIn-processSame-process engine feedback

For in-process use (most common), the actuation output feeds directly back into the next UPDEEngine.step() call without any serialisation overhead.

TTL (time-to-live) semantics

Each ControlAction carries a ttl_s field. The actuation layer tracks active actions and expires them after TTL elapses. This prevents stale control commands from persisting indefinitely if the supervisor stops producing updates.

TTLMeaning
1.0 sShort-lived corrective action
5.0 sStandard policy action
30.0 sSustained regime response
Permanent override (not recommended)

Safety invariants

The actuation subsystem guarantees:

  1. Bounded output: every actuated value is within [lo, hi]
  2. Bounded rate: |Δv| ≤ rate_limit per step
  3. Monotonic convergence: consecutive project() calls converge toward the proposed value at the rate limit
  4. No side effects: project() is pure — same inputs → same output
  5. Metadata preservation: project() only modifies value, all other ControlAction fields are immutable

Performance summary

OperationBudgetNotes
ActuationMapper.map_actions()< 10 μsDict construction
ActionProjector.project()< 10 μsTwo clamp operations
Full closed-loop overhead< 70 μsdecide + project + map

CFL stability interaction

The ActionProjector's rate limits interact with the CFL stability condition dt × (max_ω + max_K) < π:

  • If rate_limit_K is too large, a single step could violate CFL
  • The recommended rate limit is rate_limit_K ≤ π/(dt × N) - max_ω/N
  • The numerics module's check_stability() should be called after applying actuation to verify the new K_nm is stable

This is not enforced automatically — the rate limits in the binding spec must be chosen to be CFL-compatible. The docs/ASSUMPTIONS.md file documents the derivation.

Relationship to other subsystems

SubsystemInteraction
SupervisorProduces ControlAction list
BindingDeclares ActuatorMapping list
NumericsCFL check after actuation
AuditLogs every actuation command
EngineConsumes modified K_nm, ζ, Ψ

HDL Synthesis Compiler

The KuramotoVerilogCompiler provides a path from high-level topological learning to hard real-time hardware execution. It compiles a stabilized Kuramoto network (KnmK_{nm}, ω\omega) directly into structural Verilog code.

Experimental HDL synthesis path

The HDL synthesis path is a research feature that emits structural Verilog from a Kuramoto network. Motivation: a CPU (even the Rust kernel) can introduce OS scheduling jitter, so mapping the integration loop to parallel hardware can, in principle, reduce latency and timing variance.

This path is experimental and unvalidated. It has no field evidence and no safety certification; it must not be treated as a controller for any live system (fusion, medical, grid, or otherwise). Any latency, jitter, or throughput figure is design-dependent and must be measured on real hardware before use — the words "zero jitter" or a specific "nanosecond" latency are not claimed here as facts.

Implementation Details

The compiler generates a structural Verilog module that implements:

  1. State Registers: Fixed-point or floating-point registers for each θi\theta_i.
  2. Interaction Matrix: Parallel instantiation of sine-calculators (CORDIC or LUT).
  3. Euler Integration: Single-clock cycle updates for the entire manifold.

::: scpn_phase_orchestrator.actuation.hdl_compiler

Verified neural Control Barrier Function safety filter

actuation.control_barrier is a stronger safety layer than the bounds clamp: a Control Barrier Function h(x) defines a safe set S = {x : h(x) ≥ 0}, and the filter admits the supervisor action closest to its proposal that still satisfies the discrete-time CBF condition h(x_{k+1}) ≥ (1 − γ)·h(x_k) under the one-step plant model x_{k+1} ≈ x_k + f + g·u. With the first-order form ∇h(x)·(f + g·u) ≥ −γ·h(x) this is an analytic projection of the nominal control onto a state-dependent half-space, then a clip to the actuator bounds — a constraint derived from the barrier, not a fixed box.

NeuralBarrier is a pure-NumPy ReLU network (no training-framework dependency) exposing value, gradient (reverse-mode), and interval_bounds (sound IBP). ControlBarrierFilter.verify_forward_invariance returns a sound BarrierCertificate: it partitions the state box, bounds h per cell by IBP, and requires that on every boundary-shell cell an actuator-admissible control restores the CBF condition. Because IBP over-approximates h, a passing certificate is never a false guarantee. The certificate now carries both a filter_digest and a verification_digest; runtime callers can prove the certificate belongs to the exact barrier weights, CBF parameters, actuator bounds, control-effect vector, state/drift box, and verifier settings that produced it. Review-only: the filter shapes a proposed action; it never actuates.

::: scpn_phase_orchestrator.actuation.control_barrier

Foundation-model governor

actuation.foundation_model_governor is the harness that makes an external controller — a foundation-model forecaster, a learned policy, any advisory source SPO does not trust — deployable. FoundationModelGovernor.govern takes the proposal as an advisory scalar control and admits only a safe action by composing the trust stack SPO already owns: actuator bounds (clamp), a rate limit against the last admitted action, an optional certified Control Barrier Function projection (forward-invariance, with the state-left-the-safe-set case flagged when h(x) < 0), and any number of named safety predicates (an STL-derived check, an operating-envelope rule) that veto the action. Supplying a CBF filter without a verified matching BarrierCertificate is rejected at governor construction, so the runtime path cannot silently use an uncertified or stale neural barrier. Each call returns a GovernorDecision recording the admitted action, the status (admitted / constrained / rejected), the ordered envelope stages that touched the proposal, the violations, the barrier value, and a canonical-JSON SHA-256 seal — the same hashing the assurance bundle uses, so the governance record is tamper-evident.

The governor competes on governance, not prediction: it never forecasts, and it is review-only — it returns a safe action and a decision, it never actuates a plant. This is the runtime embodiment of EU AI Act Art. 14 (human oversight) and Art. 12 (logging / traceability).

::: scpn_phase_orchestrator.actuation.foundation_model_governor