UnitFrame¶
The one value object that makes "runs but does not move" impossible.
from strands_robots.core.units import Space, UnitFrame
policy_frame = UnitFrame(Space.DEGREES, dof=6) # scale defaults to pi/180
arm_frame = UnitFrame(Space.RADIANS, dof=6)
policy_frame.compatible_with(arm_frame) # False: not the same space
policy_frame.convertible_to(arm_frame) # True: the scalar ladder converts in the runner
UnitFrame(Space.NORMALIZED, dof=6).convertible_to(arm_frame) # False: normalized is a per-joint map
| field | meaning |
|---|---|
space |
normalized, degrees, radians, cartesian, ticks, velocity |
dof |
how many values one action carries |
scale, offset |
the linear map to SI radians for degrees/radians (si = (value + offset) * scale; degrees default pi/180) |
Every Policy declares the frame it emits (action_frame). Every Driver declares the
frame it consumes (native_frame). Robot.preflight(policy) compares the two:
| policy -> robot | result |
|---|---|
| same space, same dof | accepted |
| degrees <-> radians (the scalar ladder) | accepted, converted by the runner |
| normalized -> any joint frame | accepted when every joint has limits: [-1, 1] maps onto each joint's (lo, hi) from Robot.limits(); refused FRAME_MISMATCH naming the joints without limits |
| ticks: the servo drivers' own frame | a policy emits ticks only for a robot whose driver consumes ticks (mode="twin"/real servo arms); no scalar conversion exists |
no action_frame on a frame_agnostic policy (the mock) |
accepted: it adopts the robot's frame at preflight |
different dof |
refused: DOF_MISMATCH, names both counts |
| cartesian <-> joint | refused: FRAME_MISMATCH, names both spaces |
A refusal happens at setup, as a typed RefusalError, not as a motionless arm mid-rollout.
compatible_with is same space and dof; convertible_to is the scalar ladder; to_si and from_si
do that conversion (normalized, ticks, cartesian and velocity refuse them); describe() prints radians[6]. The six contracts are here.