Resample — constant-spacing arc-length resampling

The pipeline provides three arc-length resamplers, but only one output mode.

Resampler overview

arc_length_resample_warp(points[E,M,2], num)

The general, NaN-aware resampler (kernels _arc_scan_k + _arc_lookup_k). It compacts the finite points per env (drops NaN, in order), builds the closed-loop cumulative arc length in float64, and looks up num arc-uniform targets (searchsorted + lerp). Envs with fewer than 2 real points yield an all-NaN row and count 0. Fused into the generator output as the dense-to-num_points resample and reused by the selected polygon-fallback de-cross path; also used by the standalone gates parity wrapper (dense-to-num_points and dense-to-num_points_per_segment), which the torch oracle tests exercise but the single-pass generator no longer calls.

resample_uniform(center[E,N,2], n, count=None)

The count-aware N→N re-uniformizer (_resample_scan_k + _resample_lookup_k), used after relax (and inside inflate_warp). With count=None all E*N points are real; with count it re-uniformizes each env’s count[e] real points (NaN-padded past count[e]).

resample_constant_spacing(center[E,N,2], spacing, N_max)

The count-aware resampler selected by output_mode="constant_spacing". From a fixed source it picks a per-track count = floor(perimeter/spacing)+1 (decremented while (count-1)·spacing ≥ perimeter, capped at N_max), lays the arc-uniform points into an [E, N_max, 2] buffer NaN-padded past count[e], and matches the geometry.arc_length_resample(spacing=) oracle.

Why constant_spacing is the only output mode

constant_spacing is the only output mode (the dataclass enforces it; any other output_mode raises in __post_init__). Each track is relaxed and emitted at a constant arc spacing: a per-track count[e] = floor(perimeter/spacing)+1 (decremented while (count-1)·spacing ≥ perimeter, capped at N_max, NaN-padded past count[e]).

The legacy fixed mode — every track padded to a constant point count (num_points) — was dropped. A fixed 256 points over-resolves the centerline relative to its half-width (segment ≈ 0.2 m ≪ a 0.5 m half-width), so the slow Jacobi XPBD solve under-converges under the fixed iteration count, producing jagged tracks whose road self-overlaps. Relaxing at a width-appropriate spacing instead lets the same solve converge, yielding smooth, valid tracks on fewer nodes per track (so it is also faster). num_points survives only as the intermediate dense-resample resolution before the constant-spacing step.

count[e] derivation

For each environment e:

\[\text{count}[e] = \left\lfloor \frac{\text{perimeter}[e]}{\text{spacing}} \right\rfloor + 1\]

then decremented while \((\text{count}[e] - 1) \cdot \text{spacing} \geq \text{perimeter}[e]\), and finally capped at N_max.

This derivation matches the geometry.arc_length_resample(spacing=) torch oracle. It ensures that the count[e] - 1 inter-bead gaps each measure no more than spacing along the arc, so the bead chain is never over-packed.

Spacing defaults and sizing

spacing defaults to None, which auto-couples to 0.6·half_width (the relax-friendly rule of thumb) — a fixed spacing default would be wrong as half_width varies. Set it explicitly to override.

Size N_max ≥ max(perimeter)/spacing + 1: a track whose true count exceeds N_max is silently truncated (its closing segment then spans the gap) and fails validity. The fat-band default (half_width=0.5, spacing=0.30, N_max=384) leaves ample headroom (mean ≈ 160, max ≈ 270 points/track).