step.motion.path.passes.decompose ================================= .. py:module:: step.motion.path.passes.decompose .. autoapi-nested-parse:: ``decompose`` pass — split a ``_Then`` leg at every known ``after_cm`` boundary. A conditional segment whose stop condition is a sequential ``_Then`` chain that contains a *bare relative* ``after_cm`` leaf covers a KNOWN travel distance over that leaf, surrounded by unknown sensor-driven portions. ``drive_forward().until(after_cm(12) + over_line(sensor))`` drives a fixed 12 cm and THEN keeps driving until it crosses a line — the first 12 cm are geometrically known, the rest is not. ``...until(over_line(sensor) + after_cm(5))`` drives until the line, then a known 5 cm further. The raw lowering can't recover that known distance: ``recover_known_distance`` only promotes a *bare* ``after_cm`` (any combined ``_Then`` wrapper is rejected because the combined condition may stop early — see ``known_distance.py``). So the whole leg stays ``distance_m=None, has_known_endpoint=False`` and is invisible to the geometry passes (merge / to_absolute / splinify). ``a + b`` builds ``_Then(a, b)`` and ``+`` is LEFT-associative, so a chain ``after_cm(20) + after_cm(10) + over_line`` is the LEFT-nested tree ``_Then(_Then(after_cm(20), after_cm(10)), over_line)``. This pass flattens the ``_Then`` tree into its ordered leaf sequence, then walks the leaves left→right, splitting at EVERY bare-relative-``after_cm`` boundary — leading, trailing, or interleaved. It splits such a leg in TIME into segments of the SAME drive (identical kind/axis/sign/speed_scale/heading), one segment per GROUP: 1. Each bare relative ``after_cm`` leaf becomes its OWN known leg — condition = that ``after_cm``, run through ``recover_known_distance`` so ``distance_m`` / ``has_known_endpoint`` get filled. This leg is now a known-endpoint leg the downstream passes can absolutize / splinify. 2. Consecutive NON-(bare-relative-``after_cm``) leaves are GROUPED into one unknown sensor leg — their order-preserving ``_Then`` rebuilt (so ``over_line``'s ``on_black + on_white`` stay together as ONE leg, not split), with ``distance_m=None``, ``has_known_endpoint=False``. Each split-out ``after_cm`` becomes a REAL known distance because the executor calls ``condition.start()`` at every segment start, so each ``after_cm`` measures from its own segment's start. Examples (sensor = on_black/over_line): - ``after_cm(12) + over_line`` → ``[known(0.12), sensor]`` (leading). - ``on_black + after_cm(5)`` → ``[sensor, known(0.05)]`` (trailing). - ``on_black + after_cm + on_black + after_cm`` → ``[sensor, known, sensor, known]``. - ``over_line + after_cm(5)`` → ``[sensor(_Then(on_black, on_white)), known(0.05)]``. - pure ``over_line`` (no after_cm) → unchanged ``[seg]``. Guards (passed through UNCHANGED): - A *bare* ``after_cm`` (not wrapped in ``_Then``) — already promoted at lowering by ``recover_known_distance``; left alone. - A ``_Then`` with NO bare-relative ``after_cm`` anywhere (e.g. ``over_line`` = ``on_black > on_white``) — may stop early; left whole, not fragmented. - An *absolute* ``after_cm`` leaf — measures from odometry origin, not segment start; treated as a sensor leaf (grouped, never promoted to a known leg). - ``turn`` / ``arc`` kinds — path length doesn't map to a usable travel distance. - ``SideAction`` / ``None`` deferred placeholders — pass through untouched. Representation declaration is intentionally left undeclared (defaults to ``EITHER`` / ``SAME`` / non-terminal); run this BEFORE ``to_absolute`` / ``splinify`` so the split-out known leg can be optimized. Classes ------- .. autoapisummary:: step.motion.path.passes.decompose.DecomposePass Module Contents --------------- .. py:class:: DecomposePass Split a ``_Then`` leg at every bare ``after_cm`` boundary. Pure node→node pass. For each ``linear`` / ``follow_line`` ``Segment`` whose condition is a sequential ``_Then`` containing a bare relative ``after_cm`` leaf, splits it into one known-endpoint leg per ``after_cm`` leaf (distance recovered) and one grouped unknown sensor leg per run of consecutive non-``after_cm`` leaves — leading, trailing, and interleaved. A ``_Then`` with no bare ``after_cm`` anywhere, and all non-decomposable nodes, pass through unchanged. Representation/terminal contract left undeclared (defaults to ``EITHER`` / ``SAME`` / non-terminal). .. py:attribute:: name :value: 'decompose' .. py:method:: run(nodes: list[step.motion.path.ir.PathNode | None]) -> list[step.motion.path.ir.PathNode | None]