step.motion.path.optimize ========================= .. py:module:: step.motion.path.optimize .. autoapi-nested-parse:: Fluent path-optimizer builder — ``optimize([...])``. A thin, fluent wrapper around the existing compiler + executor pipeline. Where ``smooth_path()`` exposes optimization as boolean flags, ``optimize()`` exposes it as a chain of explicit passes — Java-stream style:: optimize([drive_forward(50), turn_right(90), drive_forward(30)]) .cut_corners(5) The builder is itself a ``Step``: it compiles its raw steps through ``PathCompiler`` and runs the result through ``PathExecutor``, mirroring the non-absolute, non-spline execution wiring of ``SmoothPath``. ``DecomposePass`` then ``MergePass`` are ALWAYS-ON — prepended to every compiled pipeline before the user's chained passes — because both are behavior-preserving. Use ``seq()`` instead of ``optimize()`` if you want neither. A small compile-time state machine validates composition of the CHAINED passes. Each pass may optionally declare the segment representation it ``requires`` / ``produces`` and whether it is ``terminal`` (see ``passes/contract.py`` for :class:`Representation` and the defaults); passes that don't declare these (``MergePass`` / ``CornerCutPass``) default to ``EITHER`` / ``SAME`` / non-terminal and keep working unchanged. Exceptions ---------- .. autoapisummary:: step.motion.path.optimize.PathBuildError Classes ------- .. autoapisummary:: step.motion.path.optimize.Optimizer Functions --------- .. autoapisummary:: step.motion.path.optimize.optimize Module Contents --------------- .. py:exception:: PathBuildError Bases: :py:obj:`Exception` Raised when passes are composed in an invalid order. .. py:class:: Optimizer(steps: list) Bases: :py:obj:`step.Step` Fluent path optimizer — compiles motion steps through opt-in passes. Built via the ``optimize()`` factory. Chain ``.cut_corners()``, ``.to_absolute()``, ``.splinify()``, ``.absolute_heading()`` or ``.apply(custom_pass)`` to append compiler passes; each returns ``self`` so calls compose Java-stream style. Execution compiles the raw steps through ``PathCompiler`` and runs the result on ``PathExecutor`` (relative, non-spline path). Two passes are ALWAYS-ON and run before any user pass: ``DecomposePass`` (splits ``after_cm + sensor`` legs in time) then ``MergePass`` (collapses adjacent same-type legs). Both are behavior-preserving RELATIVE→RELATIVE transforms, so they are prepended to the compiled pipeline without going through ``_add`` (they don't affect the representation state the user's passes are validated against). If you want neither, use ``seq()`` rather than ``optimize()``. .. py:attribute:: hz :type: int :value: 100 .. py:method:: to_absolute() -> Optimizer Drive as ABSOLUTE as possible — closed-loop on the localization filter. Turns on absolute mode. On its own, each maximal run of known-endpoint ``linear`` / ``turn`` / ``diagonal`` legs becomes ONE ``GotoWaypoints`` and each sensor-bounded single-axis leg becomes an ``AbsoluteHoldMove`` (cross-axis + heading held absolute, free axis until the sensor) — every leg regulating on the particle filter so accumulated odometry drift is corrected during the move. Chained with ``.splinify()`` the whole path instead becomes one absolute spline (a continuous ``SplineFollow``). Reads localization only — never feeds it. .. py:method:: absolute_heading() -> Optimizer Pin every straight leg to one integrated heading (``AbsoluteHeadingPass``). Integrates a single running heading through the path at compile time (start 0; turns/arcs add their signed angle) and stamps it onto each ``linear`` / ``follow_line`` leg, so every straight leg regulates against ONE coherent heading reference rather than re-zeroing on the previous leg's drifted frame — drift-robust over long paths. The pass emits headings in the path-start frame; the executor adds the path-start world-heading offset H0 (captured once at the first motion segment) at runtime so they become absolute world targets. .. py:method:: cut_corners(radius_cm: float, cut_until: bool = False) -> Optimizer Round corners between straight legs with arcs (``CornerCutPass``). Cuts two corner shapes, trimming ``radius_cm`` from each straight leg: - **Turn corners** (``linear+turn+linear``, same-axis legs) become a circular arc — forward legs give a drive arc, strafe legs a lateral arc. - **Crab corners** (``linear(Forward)+linear(Lateral)``, no turn) become a constant-heading ``crab_arc``: a holonomic base blends forward into strafe over a 90° quarter circle without rotating. By default only corners between known fixed-distance legs are cut. Set ``cut_until=True`` to also round a corner whose EXIT leg is a sensor-bounded ``.until()`` leg: the entry leg is trimmed, the arc rounds the corner, and the ``.until()`` leg then runs from the arc's end until its condition fires (it is kept untrimmed — its endpoint is unknown, and the sensor still triggers at the same place). The ENTRY leg can never be a ``.until()`` leg: the fillet would have to start curving before the sensor fires, which can't be anticipated. ``radius_cm`` is the cut distance trimmed from each (known) straight leg, in centimeters; converted to meters for ``CornerCutPass`` here. .. py:method:: splinify() -> Optimizer Replace the whole relative run with one Catmull-Rom spline (terminal). Collapses the linear/turn path into a single ``spline`` segment driven through its waypoints (``SplinifyPass``). Terminal — nothing may follow. Raises ``ValueError`` at compile time if the path can't be splinified (side actions, arcs, conditions, deferred steps, or < 2 waypoints). .. py:method:: time_optimal(max_speed_cmps: float = 100.0, accel_cmps2: float = 60.0, lateral_accel_cmps2: float = 50.0, sensor_carry_cmps: float = float('inf')) -> Optimizer Carry speed through the WHOLE path — the global generalisation of the per-seam warm-start (``VelocityProfilePass``). Runs a forward/backward sweep that stamps the fastest feasible entry/exit speed on every leg, so the robot only decelerates for REAL stops (turns, direction reversals, blocking side actions, the final leg) and corner/curvature limits — instead of braking to a halt at every segment seam. ``sensor_carry_cmps`` is the "on steroids" knob for the sensor-driven style: a leg that ends on a ``.until()`` SENSOR/time condition normally forces a full stop, but when the NEXT leg continues in the same direction the robot instead FLOWS THROUGH the sensor boundary rather than braking to zero — far cleaner, faster motion on a robot that stops at every sensor. It defaults to ``inf`` (the robot keeps its full cruise through the seam — it is already at cruise when the sensor fires, so braking would only waste time, and the same-direction flow adds no lateral overshoot). Lower it (cm/s) to throttle the flow-through where a sensor needs dense sampling; set 0 to brake at every sensor as before. Accuracy-preserving by construction. PATH-PRESERVING: changes only speed, never geometry, so the endpoint matches the un-profiled path. Most effective after ``cut_corners()`` (it carries speed through the arcs the corner-cut introduced). Limits are in cm: ``max_speed_cmps`` cruise cap, ``accel_cmps2`` accel/decel, ``lateral_accel_cmps2`` the arc curvature cap (``v <= sqrt(a_lat * R)``). .. py:method:: apply(p) -> Optimizer Append a user-supplied compiler pass. .. py:method:: required_resources() -> frozenset[str] Return the hardware resources this step requires exclusive access to. For leaf steps (drive, motor, servo), return the resources this step directly uses. Composite steps override ``collected_resources`` instead to include children — ``required_resources`` stays empty for composites because they don't touch hardware themselves. .. py:method:: collected_resources() -> frozenset[str] Return all resources this step *and its children* require. Used by ``validate_no_overlap`` for static conflict detection at construction time. Leaf steps don't need to override this — the default delegates to ``required_resources``. Composite steps override to union their children's collected resources. .. py:method:: explain() -> str Render the node list before and after each pass — no execution. Lowers the raw steps to IR, then applies each configured pass in turn, capturing the node list after each. Returns a readable multi-line report for debugging pass composition. Does not touch a robot. .. py:function:: optimize(steps) -> Optimizer Build a fluent path optimizer over a list of motion steps. ``optimize()`` is the explicit-pass counterpart to ``smooth_path()``: it wraps the same compiler + executor pipeline, but instead of boolean flags you append optimization passes by chaining methods. Each chain method returns the builder, Java-stream style:: optimize([drive_forward(50), turn_right(90), drive_forward(30)]) .cut_corners(5) ``decompose`` then ``merge`` ALWAYS run first (before any chained pass): both are behavior-preserving, so ``optimize()`` always splits ``after_cm + sensor`` legs and collapses adjacent same-type legs. If you want neither, use ``seq(steps)`` instead. A compile-time state machine validates the order of the CHAINED passes: a pass may declare the segment ``Representation`` it requires/produces and whether it is terminal. Composing passes that disagree (e.g. an absolute-only pass on a relative stream, or any pass after a terminal one) raises ``PathBuildError`` at build time. Available chained passes: - ``.cut_corners(radius_cm)`` — replace ``linear+turn+linear`` corners with a circular arc, trimming ``radius_cm`` from each straight leg. - ``.to_absolute()`` — convert known-endpoint relative runs into one closed-loop navigate-to-pose move. - ``.absolute_heading()`` — pin every straight leg to one integrated heading. - ``.splinify()`` — collapse the relative run into one spline (terminal). - ``.apply(pass)`` — append any custom ``CompilerPass``. Use ``.explain()`` to dump the IR node list after each pass (including the always-on decompose + merge) without executing anything. Prerequisites: ``calibrate_distance()`` if any segment uses distance-based mode. ``mark_heading_reference()`` if any segment uses heading hold. :param steps: A list of motion steps (or a single step / nested ``seq()``). Accepts the same step family as ``smooth_path``. :returns: An ``Optimizer`` step. Chain passes onto it, then run it like any step. Example:: from raccoon.step.motion import optimize, drive_forward, turn_right optimize( [ drive_forward(30), drive_forward(20), turn_right(90), drive_forward(40), ] ).cut_corners(5)