step.motion.path.optimize

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 Representation and the defaults); passes that don’t declare these (MergePass / CornerCutPass) default to EITHER / SAME / non-terminal and keep working unchanged.

Exceptions

PathBuildError

Raised when passes are composed in an invalid order.

Classes

Optimizer

Fluent path optimizer — compiles motion steps through opt-in passes.

Functions

optimize(→ Optimizer)

Build a fluent path optimizer over a list of motion steps.

Module Contents

exception step.motion.path.optimize.PathBuildError

Bases: Exception

Raised when passes are composed in an invalid order.

class step.motion.path.optimize.Optimizer(steps: list)

Bases: 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().

hz: int = 100
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.

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.

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.

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).

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)).

apply(p) Optimizer

Append a user-supplied compiler pass.

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.

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.

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.

step.motion.path.optimize.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.

Parameters:

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)