Tuning guide

This page collects practical tuning advice for TrackGenConfig. For the complete parameter descriptions, see the Configuration reference.

Yield vs. diversity

Yield is the fraction of environments that pass the post-relaxation validity gate. Diversity is how varied the resulting track shapes are across a batch. These two goals pull in opposite directions: the settings that reliably produce valid tracks tend to constrain the shape space.

A few principles:

  • Raise ``relax_iters`` first. The default of 50 Chebyshev-accelerated sweeps is calibrated for approximately 99.9 % yield at the library defaults (matching what 150 plain-Jacobi sweeps used to need). If yield is low, the most conservative fix is to increase relax_iters; cost is linear in sweeps and it does not restrict shapes. See Track relaxation — convergence and tuning for the measured yield-vs-sweeps curves.

  • Widen the generator’s shape range for diversity. For the Bezier generator, widen [min_num_points, max_num_points] and enable style_sampling=True with rad_range, scale_range, and handle_clamp_frac_range. For the checkpoint generator, increase checkpoint_count or widen checkpoint_radius_min_frac. For the polar generator, increase polar_radial_jitter or polar_num_knots. Wider shape ranges can introduce harder-to-relax geometries, so watch yield when you push these.

  • Mix generators across workers rather than pushing a single generator to its limits. Each centerline generator ("bezier", "hull", "polar", "voronoi", "checkpoint") produces a visually distinct family; using different configs across parallel workers is the lowest-friction path to a broad distribution.

  • Use the parameter explorer to iterate quickly. The interactive Gradio UI (.venv/bin/python -m viz.param_explorer) shows valid-yield and quality stats over a full batch in real time, so shape/yield trade-offs are immediately visible.

half_width and spacing coupling

spacing controls the arc-length step between adjacent output points. half_width controls the track width and feeds directly into the XPBD relaxation geometry.

When spacing is left at its default (None), it is auto-coupled to 0.6 * half_width. This is the relax-friendly rule of thumb: at that spacing each bead is roughly 0.6 road-widths apart, which gives the Jacobi XPBD solver enough room to converge within the default iteration budget.

Warning

A fixed spacing value does not scale with half_width. If you change half_width without adjusting spacing, the per-bead geometry shifts and the solver may under-converge. Unless you have a specific reason to override, leave spacing=None and let the auto-coupling handle it.

The N_max buffer must be large enough to hold the longest track in the batch. The formula for the required size is:

N_max >= floor(max_perimeter / spacing) + 1

A track whose real point count exceeds N_max is silently truncated; the closing segment then spans the gap and the track fails validity. At the library defaults (half_width=0.1, auto spacing=0.06) the default N_max=384 gives ample headroom (mean ~141, max ~281 points per track). For a thin-track regime such as half_width=0.03 (auto spacing=0.018, ~468 max points per track), raise N_max to at least 512 to avoid truncation-driven invalids.

Summary of the coupling rules:

  • spacing = None → auto 0.6 * half_width.

  • N_max must satisfy N_max >= max_perimeter / spacing + 1; raise it when you lower half_width or spacing.

  • scale controls the track coordinate range; doubling scale roughly doubles perimeters, so N_max scales proportionally.

Relaxation knobs

The XPBD solve has three classes of knobs.

Iteration count and solver constraints

relax_iters (default 50) is the number of Jacobi sweeps. Each sweep applies three corrections to every bead:

  1. Non-neighbour separation — pushes beads that are too close apart.

  2. Edge spacing — restores the constant bead-to-bead rest length.

  3. Bending/radius guard — removes sharp kinks introduced by separation pushes.

Chebyshev acceleration

relax_accel (default "chebyshev") accelerates the Jacobi sweeps with the Chebyshev semi-iterative method: after relax_cheby_start (default 8) plain warmup sweeps, each sweep blends the Jacobi update with the current and previous iterates using a precomputed omega schedule driven by relax_cheby_rho (default 0.98) and damped by relax_cheby_gamma (default 0.9). This is why the default sweep count is 50 rather than 150. relax_accel="none" restores plain Jacobi. The defaults are a measured operating point — relax_cheby_rho >= 0.99 or relax_cheby_start <= 5 were measured to reduce yield. For the full account (mechanics, measured tables, and dead ends) see Track relaxation — the solver and Track relaxation — convergence and tuning.

relax_margin (default 0.15) expands the separation target beyond the exact road diameter:

target = 2 * half_width * (1 + relax_margin)

A 15 % margin leaves room for the downstream thickness validity gate (the gate checks that minimum thickness is at least (1 - relax_tol) * half_width). Raising it buys yield but was measured to cost 20–40 % spacing-RMS quality — prefer raising relax_iters instead. Decreasing it tightens the packing but produces more near-threshold invalids; see the gate-coupling discussion in Track relaxation — setup and constraints.

relax_tol (default 0.02) is the validity thickness tolerance: the minimum accepted per-point half-width is (1 - relax_tol) * half_width.

Smoothing tail

After the main sweeps, a short post-solve smoothing tail runs: relax_smooth_passes (default 5) shrink-free Taubin smoothing passes followed by relax_smooth_spacing_iters (default 10) spacing-only polish sweeps. This strips the fine sub-R_min curvature noise the bending guard’s deadband cannot see, cutting curvature-difference RMS by roughly a third with no validity loss. Set both to 0 to disable the tail. For the mechanics (Taubin lambda/mu, why a plain Laplacian is not substituted, and the graph-capture note) see Track relaxation — the solver.

Per-constraint relaxation factors

Three scale factors control how aggressively each correction is applied per sweep:

  • relax_sep_relax (default 1.0) — separation correction scale. Reduce toward 0 to soften separation enforcement (useful if the solver oscillates).

  • relax_spc_relax (default 1.0) — edge-spacing correction scale.

  • relax_bend_relax (default 1.5) — bending/radius-guard correction scale. The default slightly over-corrects to quickly remove sharp corners; reduce if the solve oscillates.

Neighbour exclusion band

relax_band (default None) sets the bead-index exclusion band for separation: beads within band positions of each other along the chain are not separated. None auto-computes round(2 * half_width / L0) per track, where L0 is the rest edge length. Override it only if you observe the road trying to push adjacent beads apart — and never widen it to save separation work: auto-band + 1 was measured to collapse defaults-regime yield from 0.99 to 0.74 (see the gate coupling in Track relaxation — setup and constraints).

Separation-cache throughput recipe

The dominant cost in the XPBD solve is the non-neighbour separation scan, which is O(count[e]^2) per track per sweep in the dense baseline. Three advanced knobs expose a graph-capturable broadphase cache:

Separation-cache knobs

Setting

Meaning

Default

relax_sep_every

Broadphase refresh interval K. Without a cache this is a naive skip cadence; with cache enabled it rebuilds candidates every K sweeps. Keep it well below relax_iters (see the warning below).

20

relax_sep_cache_slots

Fixed candidate capacity per bead. 0 disables caching. Larger values use more memory and narrowphase work but reduce candidate-overflow risk.

16

relax_sep_cache_skin

Extra broadphase radius as a fraction of target: cache radius is target * (1 + skin). The exact separation push is still applied only for dist < target.

0.5

Cached mode is active when relax_sep_cache_slots > 0 and relax_sep_every > 1. In cached mode, _build_sep_cache_kernel rebuilds a fixed-size per-bead candidate list every K sweeps using the broadphase radius. _step_cached_kernel then runs every sweep, re-testing each cached candidate with the exact narrowphase dist < target before applying the push. This is fundamentally different from the naive cadence path (relax_sep_cache_slots=0, relax_sep_every > 1), which simply skips separation on intermediate sweeps.

skin=0.0 stores only pairs already inside the exact separation target at refresh time. Positive skin retains near-pairs that may enter contact during a long refresh interval, which is safer for large K but slower.

Warning

The candidate cache is built at sweep 0 and refreshed only at multiples of relax_sep_every. A run shorter than relax_sep_every sweeps never refreshes it mid-solve and silently misses contacts that emerge during the run — under the old default of 40 this produced a measured yield cliff between 40 and 50 sweeps. That is why the default dropped to 20 alongside the relax_iters 150 → 50 change. If you shorten relax_iters, keep relax_sep_every comfortably below it. See Track relaxation — convergence and tuning.

A high-throughput config from the E=8192, half_width=0.03, relax_iters=150 CUDA graph benchmark (note the long 150-sweep budget is what makes relax_sep_every=40 safe there):

TrackGenConfig(
    ...,
    relax_sep_every=40,
    relax_sep_cache_slots=16,
    relax_sep_cache_skin=0.0,
)

On the benchmark machine this ran in approximately 0.066 s per graph replay versus 0.366 s for the dense baseline (relax_sep_cache_slots=0), with effectively unchanged validity in the checked runs.

Note

Keep the dense baseline (relax_sep_cache_slots=0) for maximum conservatism. Use the cache knobs when throughput matters and validate yield in your target regime before committing to the cached config.

Per-generator tuning tips

Bezier (generator="bezier")

  • rad (default 0.4) is the primary curviness dial — the Bezier handle length as a fraction of each segment chord. Higher values produce rounder corners; very high values combined with a small handle_clamp_frac can introduce self-crossings that fall back to the polygon rescue path.

  • handle_clamp_frac (default 0.4) prevents handles from overshooting nearby corners. Setting it equal to rad (the default) means the clamp only trims genuine overshoot; setting it below rad makes every handle respect the shorter adjacent edge.

  • min_num_points / max_num_points (default 9–13) control the corner count per env. A wider range increases diversity; many corners with high rad can push more tracks into the polygon fallback.

  • Enable style_sampling=True with rad_range, scale_range, and handle_clamp_frac_range to let each env draw its own style, broadening the family without adding a separate config.

Hull (generator="hull")

  • hull_displacement (default 0.15) is the main shape dial — the maximum radial midpoint displacement as a fraction of the centroid-to-midpoint distance. At 0.0 the method collapses toward a plain angle-sorted loop; larger values (typical range 0.1–0.4) create stronger lobes and straights, but can increase the downstream relaxation burden.

Polar (generator="polar")

  • polar_num_knots (default 12) controls how many frequency components the spline has. More knots add higher-frequency features.

  • polar_radial_jitter (default 0.60) is the fractional radial jitter per knot. Larger values produce more eccentric shapes.

  • polar_angular_jitter (default 0.30) adds angular irregularity. The implementation clamps it so knots stay in sorted angular order.

  • The polar generator has no local fallback: any rare bad geometry is handled by the shared post-relaxation validity gate, so unusually high jitter at low relax_iters may reduce yield.

Voronoi (generator="voronoi")

  • voronoi_site_layout (default "void_ring") selects the site distribution. "void_ring" biases sites into an annular band with a center void, which tends to produce usable closed loops. "ring" fills the bounding box uniformly (not an annular band despite the name). "clustered" and "mixed" create more complex layouts.

  • voronoi_control_points (default 18) sets how many angular anchors are selected from the site field. More control points add local features and can raise the relaxation burden.

  • voronoi_radial_variation (default 0.62) controls how eccentric the anchor ring is. At 0 all anchors are placed at a uniform annulus radius.

  • Higher voronoi_num_sites enriches the anchor snap targets but increases the all-sites scan cost per bead.

Checkpoint (generator="checkpoint")

  • checkpoint_count (default 12) controls the number of radial checkpoints. More checkpoints produce wavier, chicane-rich tracks; fewer produce calmer, rounder shapes.

  • checkpoint_best_of_k (default 4) generates K decorrelated candidates per env and keeps the one with the fewest self-intersections. Increasing K reduces pre-relaxation self-crossings at the cost of extra generation work.

  • checkpoint_turn_rate (default 0.42 rad/step) bounds the path curvature. Too large allows kinks; too small prevents the path from tracking tight inlets.

  • checkpoint_clip_fallback (default False) opts into a single-crossing clip rescue. Leave it off unless you are seeing a meaningful fraction of crossing survivors that the validity gate is discarding.