step.motion.path.passes.absolute_heading ======================================== .. py:module:: step.motion.path.passes.absolute_heading .. autoapi-nested-parse:: ``absolute_heading`` pass — pin each straight leg to an integrated heading. Every straight motion (``linear`` / ``follow_line``) normally re-zeroes its heading reference on whatever world heading the previous segment happened to leave behind (executor priority #3 — ``current_world_heading_rad`` read fresh at each boundary, see ``motion_factory._create_linear_motion``). Across a long path the per-leg odometry drift accumulates. This pass instead integrates a single running heading through the node list at compile time and stamps it onto ``Segment.target_heading_rad`` for every straight leg, so each leg regulates against ONE coherent heading reference rather than chaining off the previous leg's drifted frame. Integration convention (identical to ``to_absolute`` / ``segments_to_spline_waypoints``): running_heading starts at 0.0 (the path-start frame) each ``turn`` segment adds its signed ``angle_rad`` each ``arc`` segment adds its signed ``arc_angle_rad`` straight legs do not change the heading Frame of ``target_heading_rad`` (path-start-relative; executor applies offset): At runtime ``target_heading_rad`` is fed *straight into the PID* as an ABSOLUTE world heading (``motion_factory._create_linear_motion`` line ~182: ``config.target_heading_rad = seg.target_heading_rad``), and "world heading" is the raw, un-zeroed odometry heading (``get_world_heading_rad`` = ``robot.odometry.get_pose().heading``). The headings this pass emits are in the PATH-START frame (start = 0). The executor resolves the frame: ``PathExecutor.run`` captures the path-start world heading H0 once at the first motion segment and adds it to every stamped ``target_heading_rad`` (via ``_with_heading_offset``) before the value reaches ``create_motion`` / position-offset / warm-start, so each leg regulates against the correct absolute world heading regardless of the robot's heading at path start. This pass therefore emits the relative integration only; the offset is an executor concern, now wired in. Guards (conservative — drift correction must never fight an unknown frame): * A ``SideAction`` resets the running heading to 0 and *stops* stamping until the next clean node — a side action may reorient the robot (it can drive), invalidating the integrated frame. * A ``None`` (deferred placeholder) does the same — its motion is unknown. * A ``Segment`` with ``has_known_endpoint is False`` (unknown-angle turn / condition-only move) makes every downstream absolute heading unpredictable: we reset and stop stamping for the remainder of the path. (We do not try to resume after a clean run-restart heuristic — there is no clean run-restart marker in the IR, so the conservative choice is to stop until the list ends. A ``SideAction`` / ``None`` that follows would reset anyway; this only differs by also stopping across a later clean stretch, which we accept rather than risk stamping a wrong frame.) * An explicit user heading already on the segment (``heading_deg`` set, or ``target_heading_rad`` already set) is never overwritten — user intent wins. We still advance the running heading past such legs. ``arc`` / ``spline`` segments are passed through untouched (we never stamp a heading onto them), but an ``arc`` with a known ``arc_angle_rad`` still advances the running heading so following straight legs stay correct. Classes ------- .. autoapisummary:: step.motion.path.passes.absolute_heading.AbsoluteHeadingPass Module Contents --------------- .. py:class:: AbsoluteHeadingPass Stamp an integrated path-start heading onto every straight leg. Pure node→node pass. Walks the node list once, integrating a running heading (start 0; turns/arcs add their signed angle) and filling ``target_heading_rad`` on each ``linear`` / ``follow_line`` segment that has no explicit user heading. Barriers (``SideAction``, ``None``, or an unknown-endpoint segment) reset the frame and stop stamping; see the module docstring for the full guard rules and the absolute-vs-relative frame caveat. .. py:attribute:: name :value: 'absolute_heading' .. py:method:: run(nodes: list[step.motion.path.ir.PathNode | None]) -> list[step.motion.path.ir.PathNode | None]