Skip to content

Action Compose

ControlAction

@dataclass
class ControlAction:
    knob: str       # "K", "alpha", "zeta", or "Psi"
    scope: str      # "global" or "layer_{n}"
    value: float    # target value or delta
    ttl_s: float    # time-to-live in seconds
    justification: str

Valid knobs: {"K", "alpha", "zeta", "Psi"}.

ActuationMapper

Maps ControlAction to domain-specific actuator commands.

Initialised with list[ActuatorMapping] from the binding spec:

actuators:
  - name: coupling_knob
    knob: K
    scope: global
    limits: [0.0, 5.0]
  - name: damping_knob
    knob: zeta
    scope: global
    limits: [0.0, 2.0]

map_actions(actions) returns a list of command dicts:

[
    {
        "actuator": "coupling_knob",
        "knob": "K",
        "scope": "global",
        "value": 0.5,   # clipped to limits
        "ttl_s": 10.0,
    }
]

Values are clamped to [limits[0], limits[1]]. Scope matching: a global action maps to all actuators for that knob. A layer-scoped action maps only to matching-scope actuators.

validate_action(action) returns True if the action's knob is valid and value is within limits for at least one matching actuator.

ActionProjector

Rate-limits and clips control actions relative to the previous value:

projector = ActionProjector(
    rate_limits={"K": 0.1, "alpha": 0.05, "zeta": 0.2},
    value_bounds={"K": (0.0, 5.0), "alpha": (-3.14, 3.14)},
)
projected = projector.project(action, previous_value=0.45)
  1. Clamp value to [lo, hi].
  2. If |value - previous| > rate_limit, cap the delta at rate_limit.
  3. Re-clamp after rate limiting.

Returns a new ControlAction with the projected value.

Composition Order

SupervisorPolicy.decide()
    → list[ControlAction]
    → ActionProjector.project() (per action)
    → ActuationMapper.map_actions()
    → list[dict] (actuator commands)
    → Domain-specific execution

Positioning in an operations stack

This is the boundary where policy intent becomes safe executable action.

By keeping this path explicit, teams can independently inspect each stage:

  • Which knobs were requested.
  • Which constraints were applied.
  • Which actuators received each command.

That auditability is valuable because policy rules, safety limits, and actuator interfaces evolve at different cadences. This module keeps those cadences ordered and reviewable.

Why this shape matters

  • ControlAction enforces a compact, typed command vocabulary.
  • ActionProjector makes transient oscillation shocks less likely to create unsafe actuator jumps.
  • ActuationMapper centralizes domain adaptation so supervisory logic stays platform-agnostic.

When this interface is respected, a policy change can be tested in replay, simulated in the same composition path, and only then promoted with full context and command traces.

References

Rate limits and value bounds are empirical — see ASSUMPTIONS.md § Rate Limits. Knob definitions follow the Sakaguchi–Kuramoto model documented in knobs_K_alpha_zeta_Psi.md.

Production interpretation

  • This is the canonical place where high-level intent is converted into bounded, auditable actuator commands.
  • Layer scope and knob validation are both policy boundaries: they prevent accidental cross-layer bleed-through.
  • Projection + mapping order is intentional; it ensures every downstream command is clamped and constrained before domain adapters execute.