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)
- Clamp value to
[lo, hi]. - If
|value - previous| > rate_limit, cap the delta atrate_limit. - 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