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 enablestyle_sampling=Truewithrad_range,scale_range, andhandle_clamp_frac_range. For the checkpoint generator, increasecheckpoint_countor widencheckpoint_radius_min_frac. For the polar generator, increasepolar_radial_jitterorpolar_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→ auto0.6 * half_width.N_maxmust satisfyN_max >= max_perimeter / spacing + 1; raise it when you lowerhalf_widthorspacing.scalecontrols the track coordinate range; doublingscaleroughly doubles perimeters, soN_maxscales 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:
Non-neighbour separation — pushes beads that are too close apart.
Edge spacing — restores the constant bead-to-bead rest length.
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:
Setting |
Meaning |
Default |
|---|---|---|
|
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 |
|
|
Fixed candidate capacity per bead. |
|
|
Extra broadphase radius as a fraction of |
|
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 smallhandle_clamp_fraccan 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 torad(the default) means the clamp only trims genuine overshoot; setting it belowradmakes 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 highradcan push more tracks into the polygon fallback.Enable
style_sampling=Truewithrad_range,scale_range, andhandle_clamp_frac_rangeto 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_itersmay 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_sitesenriches 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(defaultFalse) 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.