step.motion.path.passes.to_absolute =================================== .. py:module:: step.motion.path.passes.to_absolute .. autoapi-nested-parse:: ``to_absolute`` pass — turn dead-reckoning relative runs into ONE navigate-to-pose. A contiguous run of consecutive known-endpoint ``linear`` / ``turn`` / ``diagonal`` segments (no live condition) is converted into a SINGLE ``GotoWaypoints`` navigate-to-pose move. ``GotoWaypoints`` captures the localization pose ONCE at the run start (its ``on_start``) as a single anchor, precomputes the ABSOLUTE world target for each of the run's waypoints (fixed geometry, known at compile time), then regulates onto each target in sequence using the particle filter as feedback — so the open-loop dead-reckoning legs become one closed-loop run that shrugs off odometry drift. Anchor scope is PER RUN: each contiguous run captures its own anchor at its first leg, and every waypoint within the run is relative to that ONE anchor (not chained leg-to-leg), so drift does NOT accumulate across legs. Crucially this requires ZERO executor changes: the replacement node is an inline (blocking) ``SideAction``, and the executor already runs inline side actions via ``await step.run_step(robot)``. ``GotoWaypoints`` is a ``MotionStep``, whose ``run_step`` drives its full ``on_start`` / ``on_update`` / ``on_stop`` loop to completion — so the run drives its closed-loop controller through every target. Run-detection: a MAXIMAL run is a stretch of consecutive ``Segment`` nodes that are ALL ``kind in ("linear", "turn", "diagonal")``, ``has_known_endpoint is True`` and whose condition is "baked" — i.e. ``None``, or a bare relative ``after_cm`` whose distance is folded into ``distance_m`` at lowering time (the runtime odometer stop is kept but the geometric endpoint is exact). Anything else — a ``SideAction``, a ``None`` deferred placeholder, an ``arc`` / ``follow_line`` / ``spline`` segment, a segment with a live (early-stopping / sensor / absolute) condition or an unknown endpoint — BREAKS the run and passes through unchanged. Body-frame pose is integrated from ``(x=0, y=0, heading=0)`` at the run start, reusing the SAME forward/left integration as ``segments_to_spline_waypoints`` (in ``spline.py``). ONE waypoint is collected per LINEAR segment endpoint (preserving the path shape — no corner cutting); turns between linears fold into the following waypoint's target heading. All the run's waypoints feed ONE emitted ``GotoWaypoints``. SENSOR-bounded single-axis linear legs. A ``linear`` segment with NO known endpoint and a real (non-baked) ``condition`` — e.g. ``strafe_left().until( on_black)`` — does NOT join a known-endpoint run, but it pins 2 of 3 world DOF (the cross-axis position + heading) and leaves only the travel axis free until the sensor fires. Such a leg is converted into a single inline ``SideAction(AbsoluteHoldMove)``: a closed-loop-on-localization step that holds the 2 pinned DOF on their absolute targets (correcting drift DURING the move, read-only on the particle filter) while driving the free axis open-loop until the original ``.until()`` condition fires. This keeps the WHOLE to_absolute path on the particle filter rather than odometry dead reckoning. Each ``AbsoluteHoldMove`` re-anchors at its own ``on_start``, so the next known run re-anchors naturally and a sensor leg flushes any in-progress known run. Best-effort: segments that don't qualify are left untouched; the pass never crashes. ``.until(after_cm)`` legs qualify automatically — their distance is recovered at lowering time, so a BAKED ``after_cm`` leg becomes a ``GotoWaypoints`` waypoint, NOT an ``AbsoluteHoldMove``. ``follow_line`` / ``spline`` / ``arc`` / ``diagonal`` sensor legs are left untouched. to_absolute is driven by the builder as a MODE flag (``optimizer._absolute``), not a representation-changing pass. When ``splinify()`` is also chained the whole path becomes ONE absolute spline and this per-leg pass is skipped; with only ``to_absolute()`` the builder appends this pass for the per-leg conversion. It leaves ``produces`` at the default (SAME) so chaining with the terminal ``splinify()`` is allowed. Attributes ---------- .. autoapisummary:: step.motion.path.passes.to_absolute.Waypoint Classes ------- .. autoapisummary:: step.motion.path.passes.to_absolute.ToAbsolutePass Module Contents --------------- .. py:data:: Waypoint One GotoWaypoints leg, fully resolved at COMPILE time: ``(rel_forward_m, rel_left_m, abs_dx_m, abs_dy_m, heading_kind, heading_value)``. GotoWaypoints composes it against the single run anchor as:: world = anchor.pos + Rot(anchor.heading)·(rel_forward, rel_left) + (abs_dx, abs_dy) theta = anchor.heading + heading_value if heading_kind == "rel" heading_value if heading_kind == "abs" .. py:class:: ToAbsolutePass Convert each known-endpoint relative run into ONE closed-loop ``GotoWaypoints``. Pure node→node pass. Replaces each maximal run of qualifying ``linear`` / ``turn`` segments with a SINGLE inline ``SideAction(GotoWaypoints)`` node carrying all the run's body-frame waypoints — one per linear endpoint. The ``GotoWaypoints`` captures one anchor per run at start and drives to the resulting ABSOLUTE world targets in sequence (so drift does not chain leg-to-leg). Additionally converts each SENSOR-bounded single-axis ``linear`` leg (no known endpoint, real non-baked condition — e.g. ``strafe_left().until( on_black)``) into a single inline ``SideAction(AbsoluteHoldMove)`` that holds the cross-axis position + heading absolutely (2 DOF, read-only on localization) while driving the free axis until the sensor fires. Such a leg flushes any in-progress known run first. Non-qualifying nodes pass through unchanged. ``.until(after_cm())`` legs qualify automatically — their distance is recovered at lowering time, so a baked ``after_cm`` leg stays a ``GotoWaypoints`` waypoint (NOT an ``AbsoluteHoldMove``). ``follow_line`` / ``spline`` / ``arc`` / ``diagonal`` sensor legs pass through untouched. to_absolute is driven by the builder as a MODE flag (``optimizer._absolute``), not a representation-changing pass: when ``splinify()`` is also chained the whole path becomes one absolute spline and this per-leg pass is skipped; when it isn't, the builder appends this pass to do the per-leg conversion. It therefore leaves ``produces`` at the default (SAME) so chaining with the terminal ``splinify()`` is allowed. .. py:attribute:: name :value: 'to_absolute' .. py:method:: run(nodes: list[step.motion.path.ir.PathNode | None]) -> list[step.motion.path.ir.PathNode | None]