cu-rrt-star 1.2.1

An anytime RRT* path planner component for the Copper project
# cu-rrt-star: an anytime RRT* path planner for Copper

An RRT* planner (Karaman and Frazzoli) packaged as a Copper anytime task.
`base()` grows the tree until it has a first path and publishes it; every
`refine()` runs one more block of iterations and republishes only when the
path got shorter. The RON `anytime:` policy — not the task — decides how many
refinement quanta run.

### Input and output

- Input: `cu_rrt_star::PlanRequest` — the world (up to `MAX_OBSTACLES` (16)
  round obstacles in a rectangle), start and goal. The map travels with the
  job: the planner has no map of its own, so the source may change the map
  between jobs. Internally the algorithm only sees a sampling and clearance
  trait, so the round-obstacle world is one implementation, not a mandate.
- Output: `cu_rrt_star::PlanPath` — up to `MAX_WAYPOINTS` (32) waypoints.
  Every consecutive pair of waypoints is collision free, so the path can be
  driven as published, whichever stop point the policy picked. `cost` is the
  RRT* tree path cost, an upper bound on the distance actually driven.
- Points and bounds are `Point2f` and `BBox2f` from `cu-spatial-payloads`.
  Distances and costs are `cu29-units` lengths in meters; the quality is a
  `Ratio`. The tree keeps its positions in a `Point2fSoa`, so the two scans
  every iteration runs are one vectorized pass.

### 2D and 3D

`RrtStar` is generic over its space, and the dimension comes from the space's
point type: implement `Clearance` and `RrtSpace` with `Point = Point3f` and
the same planner solves 3D jobs, `Point3fSoa` storage and all. `PlanPoint` is
implemented for `Point2f` and `Point3f`; `tests` drives the planner through a
3D room to keep that honest.

The shipped task is 2D because a Copper node names one concrete type in RON:
`PlanRequest`/`PlanPath` carry `Point2f` and `World` is a rectangle of round
obstacles. A 3D node is a second space plus a second task over the same
`RrtStar`.

### Usage

The planner draws its randomness from a `cu_rng::CuRngBundle` resource, so
the node needs a resource and a binding:

```ron
resources: [
    ( id: "planner_rng", provider: "cu_rng::CuRngBundle", config: {"seed": 1} ),
],
tasks: [
    (
        id: "planner",
        type: "cu_rrt_star::RrtStarPlanner",
        resources: { "rng": "planner_rng.rng" },
        config: {
            "base_iterations": 400,
            "block_iterations": 256,
            "gamma": 0.0,
        },
        anytime: (
            max_refines: 16,
            time_budget_ms: 30.0,
            max_stall: 4,
            quality_floor: 0.05,
        ),
    ),
],
```

### Configuration

RON values are plain numbers — meters for lengths, a 0..1 fraction for
`goal_bias`. `RrtParams` holds them as `cu29-units` `Length` and `Ratio`, so
the Rust API cannot mix a length with a count.

- `base_iterations` (default 400): iterations of the base block, aiming at a
  first path.
- `block_iterations` (default 256): iterations of one refinement quantum.
- `step_size` (default 0.8): longest edge added in one extension, in meters.
- `goal_bias` (default 0.05): probability of sampling the goal.
- `goal_threshold` (default 0.5): a node this close to the goal closes a path.
- `gamma` (default 0.0): rewiring radius constant; `0.0` derives it from the
  map, which is the value RRT* needs to converge to the optimum.
- `prune_interval` (default 512): branch-and-bound prune every N iterations;
  0 disables pruning.
- `max_nodes` (default 4000): hard cap on the tree size, clamped to
  `MAX_NODES` (4096) — the capacity of the SoA position set. A larger value
  is capped with a warning.

### Anytime policy

All the knobs of the RON `anytime:` block apply: `max_refines`,
`time_budget_ms`, `max_age_ms`, `quality_target`, `quality_floor`,
`max_stall`. The reported quality is `straight-line distance / best path
cost` in `0.0..=1.0`: 0.0 means no path yet, 1.0 means the path is as short
as the world allows, so a `quality_target` is portable between maps.

A job that found no path reports quality 0.0, which stays under any
configured `quality_floor`: the node then publishes nothing for that
copperlist. A copperlist without a request also publishes nothing, and a
request whose start already lies on the goal converges at once with
quality 1.0.

### Determinism and debugging

Randomness comes from the `CuRng` resource (ChaCha8): job N's stream is a
pure function of the resource seed and N, and the job counter is part of the
frozen task state, so replay reruns every job on the stream it used live. A
remote debug session sees a small `PlannerDebugState` projection (iterations,
tree size, best cost, published quality) instead of the whole tree.

### See also

- `examples/cu_anytime_rrt_star`: a closed-loop navigation demo with a Rerun
  viewer.
- `tests/`: the same planner under a quick and a thorough policy, compared
  per copperlist.