"""Shared dataclasses for the track generation pipeline.
This module imports nothing from the rest of the package (no generators, no
inflation, no track_generator, no rng_utils). It is the shared home for
``TrackGenConfig`` and ``Track`` so that ``warp_pipeline.py`` and the facade can
both import them without a circular import. Warp is a core dependency (``Track``
fields are ``wp.array``).
"""
import math
from dataclasses import dataclass
import warp as wp
def _validate_z_fields(cfg) -> None:
"""Shared Z-profile validation for ``TrackGenConfig`` and ``GateGenConfig``.
Operates purely on the eight ``z_*`` attribute names both configs carry
on the same terms (profile membership, min/max ordering, non-negative
knobs), so a single call site works for either dataclass's
``__post_init__``.
"""
if cfg.z_profile not in {"flat", "uniform", "random_walk", "noise"}:
raise ValueError(
"z_profile must be one of "
"{'flat', 'uniform', 'random_walk', 'noise'}, got "
f"{cfg.z_profile!r}"
)
if float(cfg.z_min) > float(cfg.z_max):
raise ValueError(
f"z_min must be <= z_max, got {cfg.z_min!r} > {cfg.z_max!r}"
)
if float(cfg.z_max_step) < 0.0:
raise ValueError(
f"z_max_step must be >= 0, got {cfg.z_max_step!r}"
)
if float(cfg.z_noise_amplitude) < 0.0:
raise ValueError(
"z_noise_amplitude must be >= 0, got "
f"{cfg.z_noise_amplitude!r}"
)
if int(cfg.z_noise_harmonics) < 1:
raise ValueError(
f"z_noise_harmonics must be >= 1, got {cfg.z_noise_harmonics!r}"
)
if float(cfg.z_valid_grade) < 0.0:
raise ValueError(
f"z_valid_grade must be >= 0, got {cfg.z_valid_grade!r}"
)
[docs]
@dataclass
class TrackGenConfig:
"""Single configuration object passed to every stage of the pipeline.
Fields configure the fixed-shape Warp pipeline. Scalar style knobs are used by
default; opt-in per-env style sampling is available through ``style_sampling`` and
the ``*_range`` fields for the Bezier generator. Only ``relax_solver="xpbd"`` is
used by the runtime path; the ``energy_*``, ``tp_*``, and ``smooth_finish*`` fields
serve the torch oracle backends in ``tests/_oracle/`` and are **not used by
TrackGenerator**. The Fourier fields (``num_harmonics``, ``decay_p``,
``amplitude``, ``num_centerline_samples``) are retained only for compatibility with
``track_gen._experimental.fourier`` and older experiment sweeps.
Attributes
----------
generator : str
Registered generator name. One of ``"bezier"`` (default), ``"hull"``,
``"polar"``, ``"voronoi"``, or ``"checkpoint"``. Resolved through
``track_gen._src.generator_registry`` at ``TrackGenerator`` construction time.
device : str
Warp device string: ``"cpu"`` (GPU-free, for tests/CI) or ``"cuda"``
(captures a replay graph on first ``generate()`` call).
num_envs : int
Number of environments in the batch. Must be >= 1. Determines the
leading dimension of all output arrays.
min_num_points : int
Minimum Bezier/hull corner-anchor count per env. Must be >= 2. Shared
by the ``"bezier"`` and ``"hull"`` generators; feeds ``wp.randi`` with the
range ``[min_num_points, max_num_points]``.
max_num_points : int
Maximum corner-anchor count per env. Must be >= ``min_num_points``. A
wider range adds more per-env diversity in corner count.
num_points_per_segment : int
Dense Bezier (or Catmull-Rom) samples emitted per corner-to-corner segment
before arc-resampling to ``num_points``. Must be >= 2; higher values give
the arc-resampler a finer cumulative-arc-length grid, reducing interpolation
error.
min_point_distance : float
Minimum grid-cell spacing for corner-position sampling. Must be in
``(0, 0.5]``. Governs the cell grid as
``num_cells = int(1 / (min_point_distance * 2))``.
min_angle : float
Minimum interior angle (radians) at constrained Bezier corners; default
~0.218 rad (12.5 deg). Used by the gate-angle validity kernel in
``warp_generate.py``; not applied directly to track-centerline generation.
rad : float
Cubic-Bezier handle length as a fraction of the segment chord — the primary
curviness dial. Typical range ``[0.2, 0.6]``. Only live when
``rad <= handle_clamp_frac``; if ``rad`` exceeds the clamp,
``handle_clamp_frac`` binds every segment and ``rad`` has no effect (see
``handle_clamp_frac``).
edgy : float
Corner-tangent blend weight. Converted internally as
``p = atan(edgy) / pi + 0.5``. At 0.0 (default), ``p = 0.5`` gives a
symmetric bisector tangent; larger positive values push ``p`` toward 1,
making turns asymmetrically sharper (outgoing-edge-dominant).
scale : float
Isotropic scale multiplier applied to sampled corner positions. Scales all
track coordinates; interacts with ``spacing`` and ``half_width`` through
the coordinate range.
handle_clamp_frac : float
Adaptive Bezier-handle clamp. Each corner's handle is capped at
``handle_clamp_frac * (its shorter incident edge)`` to prevent overshoot
past a nearby corner. Set equal to ``rad`` (the default) so the clamp only
trims genuine overshoot; set below ``rad`` to bind every handle regardless
of ``rad`` (producing near-polygonal tracks); set very large to disable.
Tracks that still self-cross after clamping fall back to their corner
polygon (~5% at 0.4), which XPBD re-rounds.
hull_displacement : float
*Hull generator only.* Maximum per-edge 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]``)
bulge or pinch lobes for more racing-shape variety, at the cost of a higher
downstream relaxation burden.
checkpoint_count : int
*Checkpoint generator only.* Number of radial checkpoints (``C``). Must
be >= 3. More checkpoints produce wavier tracks (chicane count scales ~C);
fewer produce calmer, rounder tracks. CarRacing canonical default: 12.
checkpoint_radius_min_frac : float
Inner-radius fraction for checkpoint placement. Checkpoint radii are drawn
from ``U(checkpoint_radius_min_frac * R, R)`` where ``R = 1``. Must be in
``[0, 1)``. The CarRacing canonical value 0.33 gives the inlets.
checkpoint_angle_jitter : float
Angular jitter as a fraction of the per-checkpoint angular slot
(``slot = 2*pi/C``). Kept below 1.0 so the checkpoint sequence stays
angle-monotone, ensuring the steered path winds once around. Default 0.55.
checkpoint_turn_rate : float
Maximum heading change (radians) per steering step. Bounds path curvature;
too large allows kinks, too small prevents tracking the inlets. Default 0.42.
checkpoint_steer_gain : float
Proportional steering gain toward the current target bearing, in ``(0, 1]``.
Controls how aggressively the heading chases each checkpoint per step, coupled
to one lap by the fixed step length ``dl``. Default 0.65.
checkpoint_lookahead_frac : float
Advance-to-next-checkpoint threshold as a fraction of outer radius ``R``.
The path moves to the next checkpoint once within
``checkpoint_lookahead_frac * R`` of the current target, keeping flow from
stalling at each checkpoint. Default 0.16.
checkpoint_best_of_k : int
Best-of-K candidate count. Must be >= 1. Generates ``K`` decorrelated
candidates per env and keeps the one with the fewest self-intersections
(deterministic argmin; ties broken by lowest ``k``). Replaces CarRacing's
unbounded reject-retry with a bounded, graph-capturable pool; K=4 leaves
≲0.4% of tracks with a pre-relax self-intersection at the default config.
checkpoint_clip_fallback : bool
Opt-in single-crossing clip fallback (default ``False``). When ``True``,
the first self-crossing of the selected centerline is clipped: split at the
intersection point, keep the longer sub-loop arc, resample to ``num_points``.
A capture-time Python branch; the captured graph stays fixed either way.
style_sampling : bool
Opt-in per-env Bezier style randomisation (default ``False``). When
``False``, the scalar ``rad`` / ``scale`` / ``handle_clamp_frac`` are used
for all envs unchanged. When ``True``, each env draws its own values from
the ``*_range`` fields via the per-env Warp RNG, so a single batch spans a
*family* of styles. Resolved at CUDA-graph capture time (a Python branch);
per-env values are device data so the captured graph remains fixed.
rad_range : tuple of float or None
``(lo, hi)`` range for per-env ``rad`` when ``style_sampling=True``.
``None`` collapses to the scalar ``rad`` for all envs.
scale_range : tuple of float or None
``(lo, hi)`` range for per-env ``scale`` when ``style_sampling=True``.
``None`` collapses to the scalar ``scale`` for all envs.
handle_clamp_frac_range : tuple of float or None
``(lo, hi)`` range for per-env ``handle_clamp_frac`` when
``style_sampling=True``. ``None`` collapses to the scalar
``handle_clamp_frac`` for all envs.
polar_num_knots : int
*Polar generator only.* Number of periodic cubic-spline control knots.
More knots add higher-frequency shape variety. Default 12.
polar_radial_jitter : float
*Polar generator only.* Radial jitter magnitude for polar control knots, as
a fraction of the base radius. Clamped internally to keep radii positive;
larger values produce more eccentric track shapes. Default 0.60.
polar_angular_jitter : float
*Polar generator only.* Angular jitter per polar knot, as a fraction of a
half angular cell, so the sorted order is preserved without a runtime sort.
Larger values add more angular irregularity. Default 0.30.
voronoi_num_sites : int
*Voronoi generator only.* Total cell sites in the fixed site field. Must
be >= ``voronoi_control_points``. More sites enrich anchor-snap targets at
the cost of a larger all-sites scan. Default 256.
voronoi_site_layout : str
*Voronoi generator only.* Site distribution layout. One of ``"ring"``
(uniform box fill — NOT an annular band; name kept for config compatibility),
``"void_ring"`` (default — annular band with center void,
r ∈ [0.14·box, 0.52·box], biased for usable closed loops),
``"clustered"`` (6 radial Gaussian clusters with ~22% annular fallback per
site), or ``"mixed"`` (65% annular sites blended with 35% uniform box fill).
voronoi_control_points : int
*Voronoi generator only.* Number of angular anchor sites selected from the
site field (``K``). Must be >= 6 for Chaikin/Catmull-Rom densification to
produce a smooth loop. More control points add local features and can raise
the downstream relaxation burden.
voronoi_radial_variation : float
*Voronoi generator only.* Radial modulation of anchor target positions.
Must be >= 0. At 0, all targets are placed at a uniform annulus radius;
larger values allow more eccentric loops. Default 0.62.
voronoi_angular_jitter : float
*Voronoi generator only.* Angular jitter for anchor sector targets, as a
fraction of a full sector. Must be >= 0. Larger values randomise the
angular spacing of targets. Default 0.08.
repulsive_grow_mult_min : float
*Repulsive generator only.* Lower bound of the per-env target-perimeter
multiplier draw ``U(min, max)``: the grown loop's target perimeter is
``grow_mult * 2π * r_init``. Higher → denser mazes at some yield cost.
.. warning::
The ``repulsive`` generator is a **host-driven, non-graph-capturable
optimizer** — roughly **1000× slower** than ``bezier`` (~0.18 s @ E=64,
~5.1 s @ E=8192 on an RTX 4090, vs ~2 ms for bezier). It runs eagerly on
CUDA every call (no captured replay). Regenerate on a **slow cadence** or
in **staggered per-env slices**, not every frame.
repulsive_grow_mult_max : float
*Repulsive generator only.* Upper bound of the target-perimeter multiplier
draw. Must be >= ``repulsive_grow_mult_min``. Overfill ratio ≈
``grow_mult / repulsive_domain_init_ratio``.
repulsive_domain_frac : float
*Repulsive generator only.* Confinement-disc radius as a fraction of the
world-scale reference length (``r_dom = repulsive_domain_frac * scale_ref *
config.scale``). Sets the grown curve's absolute scale vs ``half_width``.
Must be > 0. Tighter → richer folds.
repulsive_domain_init_ratio : float
*Repulsive generator only.* Ratio ``r_dom / r_init`` of the domain radius to
the initial seed-circle radius. Must be > 1. Reference value 4.0.
repulsive_obstacle_count_min : int
*Repulsive generator only.* Lower bound of the per-env inner-disc obstacle
count ``k ~ randi[min, max]``. Must be >= 1.
repulsive_obstacle_count_max : int
*Repulsive generator only.* Upper bound of the inner-disc count. Must be >=
``repulsive_obstacle_count_min``.
repulsive_obstacle_radius_min_frac : float
*Repulsive generator only.* Lower bound of the inner-disc radius as a fraction
of ``r_dom``. Must be > 0.
repulsive_obstacle_radius_max_frac : float
*Repulsive generator only.* Upper bound of the inner-disc radius fraction.
Must be >= ``repulsive_obstacle_radius_min_frac``.
repulsive_ratchet_rate : float
*Repulsive generator only.* Per-iteration length-growth factor of the hard
ratchet. Must be > 0. ``<= 0.013`` holds full yield with folds; ``>= 0.016``
collapses the folds (physical limit).
repulsive_alpha : float
*Repulsive generator only.* Tangent-point energy exponent alpha. Must be > 0.
repulsive_beta : float
*Repulsive generator only.* Tangent-point energy exponent beta; obstacle
inverse power is ``p = beta - alpha``. Must be > 0.
repulsive_tau : float
*Repulsive generator only.* Normalized flow step size.
repulsive_w_len : float
*Repulsive generator only.* Weight of the small inert length regularizer.
repulsive_stages : tuple of int
*Repulsive generator only.* Coarse-to-fine resolution schedule ``N = 64 →
128 → 256``. Must be non-empty, strictly increasing, and each a positive
multiple of 4. The last entry must equal ``num_points`` (the tail input
resolution) when ``generator == "repulsive"``.
repulsive_settle_iters : int
*Repulsive generator only.* Settle-phase iteration budget above the ratchet.
repulsive_resample_every : int
*Repulsive generator only.* Periodic arc-length reparameterization interval.
repulsive_stall_window : int
*Repulsive generator only.* Iterations between stall checks / early-exit
readbacks.
repulsive_stall_area_tol : float
*Repulsive generator only.* Freeze an env once it is past target length AND
its enclosed-area relative change over a stall window is below this
(reparameterization-invariant shape-convergence tolerance).
repulsive_deactivate_obstacles : bool
*Repulsive generator only.* When ``True`` (default), zero the inner-disc
obstacle weights once an env reaches its target length (the wall stays live),
closing the disc halos.
num_harmonics : int
*Fourier / experimental — not used by the runtime path.* Number of Fourier
harmonics (``K``). Retained for compatibility with
``track_gen._experimental.fourier`` and older sweeps.
decay_p : int
*Fourier / experimental — not used by the runtime path.* Fourier amplitude
decay exponent: amplitude ~ ``amp / k ** decay_p``.
amplitude : float
*Fourier / experimental — not used by the runtime path.* Fourier base
amplitude.
num_centerline_samples : int
*Fourier / experimental — not used by the runtime path.* Dense sample count
for the Fourier generator (``M_max``).
half_width : float
Track half-width in the same coordinate units as the centerline. Must be
> 0. All spacing, relaxation, and inflation distances couple to this value:
``spacing`` defaults to ``0.6 * half_width``; the XPBD separation target is
``2 * half_width * (1 + relax_margin)``; ``N_max`` headroom scales with
``1 / half_width`` at a given track scale.
relax_enable : bool
When ``True`` (default), the XPBD relaxation solve runs before inflation.
When ``False``, the constant-spacing centerline is inflated directly; useful
for ablation studies but typically produces lower-quality tracks.
relax_solver : str
Backend selector. Only ``"xpbd"`` is used by the runtime path
(``TrackGenerator``). ``"energy"`` (Adam) and ``"tp_sobolev"`` are
oracle-only backends in ``tests/_oracle/relaxation.py``; ``TrackGenerator``
rejects any solver other than ``"xpbd"`` at construction time.
relax_chunk_size : int or None
*Energy oracle backend only — not used by the runtime path.* Env-chunk
size for the dense ``[E, N, N]`` interaction term in the energy backend.
``None`` disables chunking.
relax_use_warp : bool or None
*Oracle-only — not used by the runtime path.* When ``False``, oracle tests
redirect relaxation to the pure-torch path instead of Warp. Ignored by
``TrackGenerator`` and ``warp_relax.py``.
relax_tol : float
Validity thickness tolerance. The minimum valid track half-width is
``(1 - relax_tol) * half_width``. Default 0.02 (2% tolerance).
relax_band : int or None
Bead-index exclusion band for the separation constraint. ``None`` (default)
auto-computes ``round(2 * half_width / L0)`` per track, where ``L0`` is the
rest edge length. Set explicitly to override the per-track auto-band.
relax_iters : int
Number of XPBD Jacobi sweeps. Default 50. Each sweep applies separation,
spacing, and bending corrections; with the default Chebyshev-accelerated
solve (``relax_accel="chebyshev"``), 50 sweeps are calibrated for ~0.999
end-to-end valid yield at the library defaults (matching what 150 plain
Jacobi sweeps used to need).
relax_accel : str
Iteration-acceleration scheme for the XPBD Jacobi sweeps. ``"chebyshev"``
(default) applies Chebyshev semi-iterative acceleration (Macklin et al.
2014): after ``relax_cheby_start`` plain warmup sweeps, each sweep blends
the Jacobi update with the current and previous iterates using a
host-precomputed omega recurrence, converging ~3x faster per sweep.
``"none"`` runs plain damped Jacobi (the legacy behaviour).
relax_cheby_rho : float
Chebyshev spectral-radius estimate of the underlying Jacobi iteration.
Must be in ``[0, 1)``. Default 0.98. Larger values accelerate the
long-wavelength modes harder but risk overshoot on the nonsmooth
separation contacts; values >= 0.99 were observed to reduce yield.
relax_cheby_gamma : float
Chebyshev under-relaxation factor applied to the Jacobi step inside the
blend. Must be in ``(0, 1]``. Default 0.9. Values below 1 damp the
contact-switching noise that acceleration would otherwise amplify.
relax_cheby_start : int
Sweep index at which Chebyshev acceleration engages; earlier sweeps run
plain Jacobi so the separation contact set settles first. Must be >= 1.
Default 8.
relax_smooth_passes : int
Number of shrink-free Taubin smoothing passes applied after the main sweep
loop. Must be >= 0. Default 5. Each pass is two Laplacian half-steps
(:math:`\\lambda > 0` then :math:`\\mu < -\\lambda`), which removes the
sub-``R_min`` curvature noise the bending guard's deadband cannot see (bending
only fires below the minimum radius; gentler wiggles are invisible to all three
constraints) without shrinking corners through the validity gate the way a plain
Laplacian would. 0 disables the smoothing tail entirely.
relax_smooth_spacing_iters : int
Number of spacing-only polish sweeps applied after the Taubin passes. Must be
>= 0. Default 10. Each is the ordinary edge-spacing correction with separation
and bending off, restoring bead uniformity that the Taubin passes perturb. 0
disables the polish.
relax_sep_every : int
Broadphase refresh interval in sweeps. Must be >= 1. At 1, the dense
``O(count²)`` separation scan (or cache rebuild) runs every sweep. At
``K > 1`` with ``relax_sep_cache_slots == 0``, the dense scan is skipped
between refreshes (fast but may miss transient contacts). With
``relax_sep_cache_slots > 0``, this is the cache-rebuild interval; the exact
narrowphase still runs every sweep on cached candidates. Default 20: the
broadphase is built at sweep 0 and refreshed only at multiples of this
interval, so runs shorter than ``relax_sep_every`` sweeps never refresh it
mid-solve — the cadence dropped from 40 alongside the ``relax_iters``
150 -> 50 default so the accelerated solve still refreshes mid-solve.
relax_sep_cache_slots : int
Broadphase candidate cache capacity per bead. 0 disables the cache (uses
the dense cadenced scan). When > 0, each bead stores up to this many
non-neighbour candidate indices; the exact separation push runs against
cached candidates every sweep. Larger values reduce missed contacts at the
cost of memory and narrowphase work. Default 16.
relax_sep_cache_skin : float
Broadphase skin as a fraction of the exact separation target. Must be >= 0.
The cache stores candidates within ``target * (1 + skin)``; the exact
narrowphase test still requires ``dist < target``. Skin 0 is fastest;
positive values retain candidates that may enter contact during a long cache
interval. Default 0.5.
relax_sep_relax : float
Scale applied to the separation-constraint correction. Default 1.0 (full
correction). Reduce toward 0 to soften separation enforcement.
relax_spc_relax : float
Scale applied to the edge-spacing (rest-length) correction. Default 1.0.
Reduce to allow more spacing deviation.
relax_bend_relax : float
Scale applied to the bending/radius-guard correction. Default 1.5 (slightly
over-corrects to quickly remove sharp corners introduced by separation
pushes). Reduce if the solver oscillates.
relax_margin : float
Fractional expansion of the separation target beyond the road diameter:
``target = 2 * half_width * (1 + relax_margin)``. Default 0.15 (15%
margin). Leaves room for the downstream thickness validity gate.
energy_steps : int
*Energy oracle backend — not used by the runtime path.* Adam optimiser
steps for the ``"energy"`` oracle backend in ``tests/_oracle/relaxation.py``.
energy_lr : float
*Energy oracle backend — not used by the runtime path.* Adam learning rate.
energy_w_sep : float
*Energy oracle backend — not used by the runtime path.* Separation loss
weight.
energy_w_len : float
*Energy oracle backend — not used by the runtime path.* Edge-length loss
weight.
energy_w_bend : float
*Energy oracle backend — not used by the runtime path.* Bending loss weight.
energy_w_anchor : float
*Energy oracle backend — not used by the runtime path.* Anchor loss weight.
tp_iters : int
*TP-Sobolev oracle backend — not used by the runtime path.* Flow steps for
the ``"tp_sobolev"`` oracle backend in ``tests/_oracle/relaxation.py``.
tp_tau : float
*TP-Sobolev oracle backend — not used by the runtime path.* Step size for
the TP-Sobolev flow.
tp_alpha : float
*TP-Sobolev oracle backend — not used by the runtime path.* Sobolev kernel
exponent alpha; shared with the ``smooth_finish`` finisher.
tp_beta : float
*TP-Sobolev oracle backend — not used by the runtime path.* Sobolev kernel
exponent beta; shared with the ``smooth_finish`` finisher.
smooth_finish : bool
*Oracle-only — not used by the runtime path.* When ``True``, a
tangent-point/Sobolev smoothing finisher runs after the primary relaxation
solve. ``TrackGenerator`` rejects ``smooth_finish=True`` at construction
time.
smooth_finish_iters : int
*Oracle-only — not used by the runtime path.* Flow steps for the smoothing
finisher.
smooth_finish_tau : float
*Oracle-only — not used by the runtime path.* Step size for the smoothing
finisher.
num_points : int
Intermediate dense-resample resolution (``N``) used before the
constant-spacing step. Default 256. The constant-spacing resampler then
derives ``count[e] = floor(perimeter / spacing) + 1`` real points per env.
output_mode : str
Output resampling mode. The only supported value is
``"constant_spacing"`` (enforced by ``__post_init__``). Constant arc
spacing produces smoother XPBD convergence and higher valid-track yield than
a fixed point count.
spacing : float or None
Constant arc-length step (m) between adjacent output points. ``None``
(default) auto-couples to ``0.6 * half_width`` — the relaxation-friendly
value; a fixed default would be wrong as ``half_width`` varies. Must
resolve to > 0 after auto-coupling.
N_max : int
Output buffer width: maximum constant-spacing points per track. Tracks
whose true ``count[e]`` exceeds ``N_max`` are truncated and fail validity.
Default 384: ample headroom at ``half_width = 0.1`` (spacing ≈ 0.06,
~141 max points), full coverage to ``half_width ≈ 0.05`` (~281 max), and
~98% of the ``half_width = 0.03`` regime (~468 max). Raise ``N_max`` or
coarsen ``spacing`` for finer/larger-scale regimes.
max_regen_iters : int
*Vestigial on the Warp runtime path.* Maximum regeneration iterations for
the torch oracle's accept-first-valid loop in
``tests/_oracle/generators.py``. Ignored by ``_run_pipeline``, which is
single-pass with no host-side retry loop.
turning_tol : float
Absolute angular tolerance in radians for the turning-number validity gate.
Tracks whose signed curvature integral deviates from the expected full turn by
more than this tolerance are marked invalid. Default 0.1.
w_floor : float
Width floor for validity. Every real track point must have computed
half-width > ``w_floor``. Default ``1e-3``. Guards against near-degenerate
geometry that passes the global thickness check.
validity_border_check : bool
When ``True``, an explicit border self-intersection check is added to the
validity gate. Default ``False``. Redundant with the thickness/separation
gate (a self-crossing fat band drives
``separation_min → 0 → thickness < half_width → invalid``), so the default
saves two ``O(N²)`` passes with no change to the valid mask.
z_profile : str
Per-point altitude (Z) profile family, applied along the resampled
constant-spacing centerline arc length. One of ``"flat"`` (default,
every real point at ``z_base``), ``"uniform"`` (i.i.d. uniform in
``[z_min, z_max]``), ``"random_walk"`` (Brownian-bridge walk clamped
to ``[z_min, z_max]`` with a per-step grade cap of ``z_max_step``), or
``"noise"`` (periodic harmonic noise around ``z_base``). Applied by
``warp_zprofile.apply_z_profile`` on the resampler's ``(cum, perim)``
plan-view arc tables.
z_base : float
Baseline altitude for the ``"flat"``, ``"random_walk"``, and
``"noise"`` profiles. Default 0.0.
z_min : float
Lower altitude clamp for ``"uniform"``, ``"random_walk"``, and
``"noise"``. Must be <= ``z_max``. Default 0.0.
z_max : float
Upper altitude clamp for ``"uniform"``, ``"random_walk"``, and
``"noise"``. Must be >= ``z_min``. Default 0.0.
z_max_step : float
*Random-walk profile only.* Maximum ``|dz|`` per unit plan-view arc
length (a grade cap), applied per random-walk step before the
closure bridge. Must be >= 0. Default 0.0.
z_noise_amplitude : float
*Noise profile only.* Amplitude of the harmonic altitude
oscillation around ``z_base``. Must be >= 0. Default 0.0.
z_noise_harmonics : int
*Noise profile only.* Number of summed harmonics in the periodic
noise field. Must be >= 1. Default 3.
z_control_points : int
Number of arc-length-spaced altitude control knots. Altitude is decided
at these knots and interpolated between them with a periodic monotone
cubic, so smoothness is independent of the resampled point count. Must
be >= 3. Applies to ``z_profile="uniform"`` and ``"random_walk"``;
inert for ``"flat"`` (constant) and ``"noise"`` (analytically smooth —
use ``z_noise_harmonics`` for its frequency). Default 10.
z_valid_grade : float
Maximum allowed ``|dz|``/ds grade for post-hoc course validity checking,
where ``ds`` is measured along the PLAN-VIEW (XY) arc length, not the
3D lifted arc length. 0 disables the check. Must be >= 0. Default 0.0.
"""
# --- Generator selection + batching ---
generator: str = "bezier"
device: str = "cpu"
num_envs: int = 1
# --- Bezier params ---
min_num_points: int = 9
max_num_points: int = 13
num_points_per_segment: int = 30
min_point_distance: float = 0.05
min_angle: float = (12.5 / 180) * math.pi
rad: float = 0.4
edgy: float = 0.0
scale: float = 1.0
handle_clamp_frac: float = 0.4
# --- Hull generator params (generator="hull") ---
hull_displacement: float = 0.15
# --- Checkpoint-steering generator params (generator="checkpoint", method #5) ---
checkpoint_count: int = 12
checkpoint_radius_min_frac: float = 0.33
checkpoint_angle_jitter: float = 0.55
checkpoint_turn_rate: float = 0.42
checkpoint_steer_gain: float = 0.65
checkpoint_lookahead_frac: float = 0.16
checkpoint_best_of_k: int = 4
checkpoint_clip_fallback: bool = False
# --- Per-env style sampling (method #1: "Per-env style randomization") ---
style_sampling: bool = False
rad_range: tuple[float, float] | None = None
scale_range: tuple[float, float] | None = None
handle_clamp_frac_range: tuple[float, float] | None = None
# --- Polar / Fourier params ---
polar_num_knots: int = 12
polar_radial_jitter: float = 0.60
polar_angular_jitter: float = 0.30
# --- Voronoi / graph-cycle generator params (generator="voronoi") ---
voronoi_num_sites: int = 256
voronoi_site_layout: str = "void_ring" # {"ring", "void_ring", "clustered", "mixed"}
voronoi_control_points: int = 18
voronoi_radial_variation: float = 0.62
voronoi_angular_jitter: float = 0.08
# --- Repulsive-growth generator params (generator="repulsive") ---
repulsive_grow_mult_min: float = 4.5
repulsive_grow_mult_max: float = 5.5
repulsive_domain_frac: float = 0.35
repulsive_domain_init_ratio: float = 4.0
repulsive_obstacle_count_min: int = 8
repulsive_obstacle_count_max: int = 12
repulsive_obstacle_radius_min_frac: float = 0.02
repulsive_obstacle_radius_max_frac: float = 0.045
repulsive_ratchet_rate: float = 0.012
repulsive_alpha: float = 3.0
repulsive_beta: float = 6.0
repulsive_tau: float = 0.4
repulsive_w_len: float = 30.0
repulsive_stages: tuple[int, ...] = (64, 128, 256)
repulsive_settle_iters: int = 40
repulsive_resample_every: int = 25
repulsive_stall_window: int = 16
repulsive_stall_area_tol: float = 0.05
repulsive_deactivate_obstacles: bool = True
# Experimental torch-only Fourier generator fields retained for compatibility with
# track_gen._experimental.fourier and older sweeps.
num_harmonics: int = 5 # K
decay_p: int = 2 # decay exponent: amplitude ~ amp / k**decay_p
amplitude: float = 1.0
num_centerline_samples: int = 256 # Fourier dense sample count (M_max)
# --- Width params ---
half_width: float = 0.1
# --- Relaxation: backend selection + scale ---
relax_enable: bool = True
relax_solver: str = "xpbd" # {"xpbd","energy","tp_sobolev"}
relax_chunk_size: int | None = None
relax_use_warp: bool | None = None
relax_tol: float = 0.02
relax_band: int | None = None
relax_iters: int = 50
relax_accel: str = "chebyshev" # {"none","chebyshev"}
relax_cheby_rho: float = 0.98
relax_cheby_gamma: float = 0.9
relax_cheby_start: int = 8
relax_smooth_passes: int = 5
relax_smooth_spacing_iters: int = 10
relax_sep_every: int = 20
relax_sep_cache_slots: int = 16
relax_sep_cache_skin: float = 0.5
relax_sep_relax: float = 1.0
relax_spc_relax: float = 1.0
relax_bend_relax: float = 1.5
relax_margin: float = 0.15
# energy (Adam) — oracle-only backend
energy_steps: int = 800
energy_lr: float = 3e-3
energy_w_sep: float = 80.0
energy_w_len: float = 8.0
energy_w_bend: float = 1.0
energy_w_anchor: float = 0.01
# tp_sobolev — oracle-only backend (standalone + finisher share tp_alpha/tp_beta)
tp_iters: int = 100
tp_tau: float = 0.7
tp_alpha: float = 2.0
tp_beta: float = 4.5
# optional tangent-point/Sobolev smoothing finisher — oracle-only
smooth_finish: bool = False
smooth_finish_iters: int = 8
smooth_finish_tau: float = 0.2
# --- Output params ---
num_points: int = 256 # N: intermediate dense->resample resolution before constant-spacing
output_mode: str = "constant_spacing" # the only supported mode (see __post_init__)
spacing: float | None = None
N_max: int = 384
# --- Robustness params ---
max_regen_iters: int = 10
turning_tol: float = 0.1
w_floor: float = 1e-3
validity_border_check: bool = False
# --- Z-profile (altitude) params, shared with GateGenConfig via _validate_z_fields ---
z_profile: str = "flat"
z_base: float = 0.0
z_min: float = 0.0
z_max: float = 0.0
z_max_step: float = 0.0
z_noise_amplitude: float = 0.0
z_noise_harmonics: int = 3
z_control_points: int = 10
z_valid_grade: float = 0.0
def __post_init__(self):
if int(self.num_envs) < 1:
raise ValueError(f"num_envs must be >= 1, got {self.num_envs!r}")
if float(self.half_width) <= 0.0:
raise ValueError(f"half_width must be > 0, got {self.half_width!r}")
# Point-family sampler inputs (shared bezier/hull corner sampler). min_point_distance
# is a divisor: num_cells = int(1/(min_point_distance*2)) in warp_generate.py, so
# values <= 0 or > 0.5 drive a divide-by-zero in the grid kernels; min/max_num_points
# feed wp.randi and must be a non-inverted range. GateGenConfig already guards these.
if int(self.min_num_points) < 2:
raise ValueError(f"min_num_points must be >= 2, got {self.min_num_points!r}")
if int(self.max_num_points) < int(self.min_num_points):
raise ValueError(
"max_num_points must be >= min_num_points, got "
f"{self.max_num_points!r} < {self.min_num_points!r}")
if not (0.0 < float(self.min_point_distance) <= 0.5):
raise ValueError(
f"min_point_distance must be in (0, 0.5], got {self.min_point_distance!r}")
if int(self.num_points_per_segment) < 2:
raise ValueError(
f"num_points_per_segment must be >= 2, got {self.num_points_per_segment!r}")
if int(self.voronoi_control_points) < 6:
raise ValueError(
f"voronoi_control_points must be >= 6, got {self.voronoi_control_points!r}")
if int(self.voronoi_num_sites) < int(self.voronoi_control_points):
raise ValueError(
"voronoi_num_sites must be >= voronoi_control_points, got "
f"{self.voronoi_num_sites!r} < {self.voronoi_control_points!r}")
if self.voronoi_site_layout not in {"ring", "void_ring", "clustered", "mixed"}:
raise ValueError(
"voronoi_site_layout must be one of "
"{'ring', 'void_ring', 'clustered', 'mixed'}, got "
f"{self.voronoi_site_layout!r}")
if float(self.voronoi_radial_variation) < 0.0:
raise ValueError(
f"voronoi_radial_variation must be >= 0, got {self.voronoi_radial_variation!r}")
if float(self.voronoi_angular_jitter) < 0.0:
raise ValueError(
f"voronoi_angular_jitter must be >= 0, got {self.voronoi_angular_jitter!r}")
# Repulsive-growth generator validation (Spec §3.1). Ordered/positive knobs;
# the stages schedule is coarse-to-fine (strictly increasing, multiple of 4) and
# its last entry is the tail input resolution, so it must equal num_points for
# the repulsive generator.
# grow_mult_min >= 1 and grow_mult_max > 1: a multiplier <= 1 shrinks (or freezes) the
# init circle so the ratchet never grows, which drives the iteration budget
# n_ratchet = ceil(log(grow_mult_max)/log1p(rate)) to <= 0 -> a zero-length growth loop
# that leaves the centerline mis-strided at the coarsest stage (silent garbage tracks).
if float(self.repulsive_grow_mult_min) < 1.0:
raise ValueError(
f"repulsive_grow_mult_min must be >= 1, got {self.repulsive_grow_mult_min!r}")
if float(self.repulsive_grow_mult_max) <= 1.0:
raise ValueError(
f"repulsive_grow_mult_max must be > 1, got {self.repulsive_grow_mult_max!r}")
if float(self.repulsive_grow_mult_max) < float(self.repulsive_grow_mult_min):
raise ValueError(
"repulsive_grow_mult_max must be >= repulsive_grow_mult_min, got "
f"{self.repulsive_grow_mult_max!r} < {self.repulsive_grow_mult_min!r}")
if float(self.repulsive_domain_frac) <= 0.0:
raise ValueError(
f"repulsive_domain_frac must be > 0, got {self.repulsive_domain_frac!r}")
if float(self.repulsive_domain_init_ratio) <= 1.0:
raise ValueError(
"repulsive_domain_init_ratio must be > 1, got "
f"{self.repulsive_domain_init_ratio!r}")
if int(self.repulsive_obstacle_count_min) < 1:
raise ValueError(
"repulsive_obstacle_count_min must be >= 1, got "
f"{self.repulsive_obstacle_count_min!r}")
if int(self.repulsive_obstacle_count_max) < int(self.repulsive_obstacle_count_min):
raise ValueError(
"repulsive_obstacle_count_max must be >= repulsive_obstacle_count_min, got "
f"{self.repulsive_obstacle_count_max!r} < {self.repulsive_obstacle_count_min!r}")
if float(self.repulsive_obstacle_radius_min_frac) <= 0.0:
raise ValueError(
"repulsive_obstacle_radius_min_frac must be > 0, got "
f"{self.repulsive_obstacle_radius_min_frac!r}")
if (float(self.repulsive_obstacle_radius_max_frac)
< float(self.repulsive_obstacle_radius_min_frac)):
raise ValueError(
"repulsive_obstacle_radius_max_frac must be >= "
"repulsive_obstacle_radius_min_frac, got "
f"{self.repulsive_obstacle_radius_max_frac!r} < "
f"{self.repulsive_obstacle_radius_min_frac!r}")
if float(self.repulsive_ratchet_rate) <= 0.0:
raise ValueError(
f"repulsive_ratchet_rate must be > 0, got {self.repulsive_ratchet_rate!r}")
if float(self.repulsive_alpha) <= 0.0:
raise ValueError(f"repulsive_alpha must be > 0, got {self.repulsive_alpha!r}")
if float(self.repulsive_beta) <= 0.0:
raise ValueError(f"repulsive_beta must be > 0, got {self.repulsive_beta!r}")
stages = tuple(self.repulsive_stages)
if len(stages) == 0:
raise ValueError("repulsive_stages must be non-empty")
if any(int(s) <= 0 or int(s) % 4 != 0 for s in stages):
raise ValueError(
f"repulsive_stages entries must be positive multiples of 4, got {stages!r}")
if any(int(stages[i]) <= int(stages[i - 1]) for i in range(1, len(stages))):
raise ValueError(
f"repulsive_stages must be strictly increasing, got {stages!r}")
if self.generator == "repulsive" and int(stages[-1]) != int(self.num_points):
raise ValueError(
"repulsive_stages[-1] must equal num_points for generator='repulsive', got "
f"{stages[-1]!r} != {self.num_points!r}")
# settle_iters >= 1 keeps the growth loop non-empty even at grow_mult_max just above 1
# (n_iters = ceil(n_ratchet*1.6) + settle_iters); resample_every / stall_window feed the
# `(it+1) % k` guards in the growth loop, so k=0 would be a ZeroDivisionError mid-generate.
if int(self.repulsive_settle_iters) < 1:
raise ValueError(
f"repulsive_settle_iters must be >= 1, got {self.repulsive_settle_iters!r}")
if int(self.repulsive_resample_every) < 1:
raise ValueError(
f"repulsive_resample_every must be >= 1, got {self.repulsive_resample_every!r}")
if int(self.repulsive_stall_window) < 1:
raise ValueError(
f"repulsive_stall_window must be >= 1, got {self.repulsive_stall_window!r}")
if int(self.relax_sep_every) < 1:
raise ValueError(f"relax_sep_every must be >= 1, got {self.relax_sep_every!r}")
if int(self.relax_sep_cache_slots) < 0:
raise ValueError(
f"relax_sep_cache_slots must be >= 0, got {self.relax_sep_cache_slots!r}")
if float(self.relax_sep_cache_skin) < 0.0:
raise ValueError(
f"relax_sep_cache_skin must be >= 0, got {self.relax_sep_cache_skin!r}")
# Chebyshev-acceleration validation: rho is a spectral-radius estimate (< 1 or
# the omega recurrence diverges), gamma in (0, 1] keeps the blended step a
# damped Jacobi step, and start >= 1 guarantees at least one plain warmup
# sweep before the previous-iterate blend engages.
if str(self.relax_accel) not in ("none", "chebyshev"):
raise ValueError(
f"relax_accel must be 'none' or 'chebyshev', got {self.relax_accel!r}")
if not (0.0 <= float(self.relax_cheby_rho) < 1.0):
raise ValueError(
f"relax_cheby_rho must be in [0, 1), got {self.relax_cheby_rho!r}")
if not (0.0 < float(self.relax_cheby_gamma) <= 1.0):
raise ValueError(
f"relax_cheby_gamma must be in (0, 1], got {self.relax_cheby_gamma!r}")
if int(self.relax_cheby_start) < 1:
raise ValueError(
f"relax_cheby_start must be >= 1, got {self.relax_cheby_start!r}")
# Post-solve smoothing tail: both counts are simple non-negative sweep budgets
# (0 disables). No cross-constraint between them — either may run alone.
if int(self.relax_smooth_passes) < 0:
raise ValueError(
f"relax_smooth_passes must be >= 0, got {self.relax_smooth_passes!r}")
if int(self.relax_smooth_spacing_iters) < 0:
raise ValueError(
"relax_smooth_spacing_iters must be >= 0, got "
f"{self.relax_smooth_spacing_iters!r}")
# Checkpoint-steering generator validation: a loop needs >= 3 checkpoints, the
# radius fraction must be a proper sub-unit inner radius, and best-of-K needs >= 1.
if int(self.checkpoint_count) < 3:
raise ValueError(
f"checkpoint_count must be >= 3, got {self.checkpoint_count!r}")
if int(self.checkpoint_best_of_k) < 1:
raise ValueError(
f"checkpoint_best_of_k must be >= 1, got {self.checkpoint_best_of_k!r}")
if not (0.0 <= float(self.checkpoint_radius_min_frac) < 1.0):
raise ValueError(
f"checkpoint_radius_min_frac must be in [0, 1), "
f"got {self.checkpoint_radius_min_frac!r}")
# Only constant_spacing is supported: a constant link SIZE (~0.6*half_width) relaxes
# to smoother, higher-yield tracks than a constant point COUNT, which over-resolves
# the centerline (jagged XPBD -> folded roads).
if self.output_mode != "constant_spacing":
raise ValueError(
f"output_mode must be 'constant_spacing' (the only supported mode), "
f"got {self.output_mode!r}")
# Auto-couple spacing to half_width (~0.6*half_width relaxes to smoother tracks);
# a fixed spacing default would be wrong as half_width varies (too coarse -> degenerate).
if self.spacing is None:
self.spacing = 0.6 * self.half_width
if float(self.spacing) <= 0.0:
raise ValueError(f"spacing must be > 0, got {self.spacing!r}")
_validate_z_fields(self)
if int(self.z_control_points) < 3:
raise ValueError(
"z_control_points must be >= 3, got "
f"{self.z_control_points!r}")
[docs]
@dataclass
class GateGenConfig:
"""Configuration for fixed-batch native gate sequence generation.
Used by ``GateGenerator`` to produce batched ``GateSequence`` results. The
gate path only samples corner anchors and emits them directly as gates; it never
runs Bezier or hull curve assembly. As a result, ``num_points_per_segment``,
``rad``, ``edgy``, ``handle_clamp_frac``, and ``hull_displacement`` are **inert
for gate output** (kept for config parity with ``TrackGenConfig``). The
effective knobs for anchor placement are ``min_num_points``,
``max_num_points``, ``min_point_distance``, and ``scale``.
Attributes
----------
generator : str
Registered generator name. One of ``"bezier"`` (default), ``"hull"``,
``"polar"``, ``"voronoi"``, or ``"checkpoint"``. Selects the anchor
sampling family; only the anchor positions are used as gates.
device : str
Warp device string: ``"cpu"`` or ``"cuda"``.
num_envs : int
Number of environments in the batch. Must be >= 1.
min_gates : int
Minimum real gate count per env. Must be >= 2.
max_gates : int
Maximum real gate count per env. Must be >= ``min_gates``.
gate_radius : float
Gate detection radius used for gate-constraint solve proximity checks.
Must be >= 0.
gate_solve_iters : int
Number of gate-constraint solve iterations. Must be >= 0.
gate_width : float
Physical gate half-width (visual/collision extent). Must be >= 0. Does
not affect anchor sampling geometry.
gate_ordering : str
Gate output ordering. One of ``"ccw"`` (counter-clockwise sort by angle
around the batch centroid; default), ``"raw"`` (generator order as sampled),
or ``"random_pairs"`` (random gate pairs for training objectives).
min_num_points : int
Minimum corner-anchor count per env for the bezier/hull point sampler.
Must be >= 2. Affects the number of sampled anchor positions.
max_num_points : int
Maximum corner-anchor count per env. Must be >= ``min_num_points``.
num_points_per_segment : int
*Inert for gate output.* Dense samples per segment for Bezier/hull curve
assembly; the gate path does not assemble curves. Kept for config parity
with ``TrackGenConfig``.
min_point_distance : float
Minimum grid-cell spacing for anchor-position sampling. Must be > 0.
Governs the cell grid as ``num_cells = int(1 / (min_point_distance * 2))``.
rad : float
*Inert for gate output.* Bezier handle-length fraction. Kept for config
parity with ``TrackGenConfig``.
edgy : float
*Inert for gate output.* Corner-tangent blend weight. Kept for config
parity with ``TrackGenConfig``.
scale : float
Isotropic scale multiplier for anchor positions. Does affect the sampled
anchor coordinates and hence gate positions.
handle_clamp_frac : float
*Inert for gate output.* Bezier handle clamp. Kept for config parity with
``TrackGenConfig``.
hull_displacement : float
*Inert for gate output.* Hull midpoint displacement fraction. Kept for
config parity with ``TrackGenConfig``.
polar_num_knots : int
*Polar generator only.* Number of periodic cubic-spline control knots.
Default 12.
polar_radial_jitter : float
*Polar generator only.* Radial jitter magnitude for polar control knots,
as a fraction of the base radius. Default 0.60.
polar_angular_jitter : float
*Polar generator only.* Angular jitter per polar knot, as a fraction of a
half angular cell. Default 0.30.
voronoi_num_sites : int
*Voronoi generator only.* Total cell sites in the fixed site field. Must
be >= ``voronoi_control_points``. Default 256.
voronoi_site_layout : str
*Voronoi generator only.* Site distribution layout. One of ``"ring"``,
``"void_ring"`` (default), ``"clustered"``, or ``"mixed"``.
voronoi_control_points : int
*Voronoi generator only.* Anchor sites selected from the site field. Must
be >= 3 (the gate-native Voronoi path emits anchors directly as gates, so
it needs only 3 for a non-degenerate ring — unlike ``TrackGenConfig``'s >= 6
floor for Chaikin/Catmull-Rom densification).
voronoi_radial_variation : float
*Voronoi generator only.* Radial modulation of anchor target positions.
Must be >= 0. Default 0.62.
voronoi_angular_jitter : float
*Voronoi generator only.* Angular jitter for anchor sector targets. Must
be >= 0. Default 0.08.
checkpoint_count : int
*Checkpoint generator only.* Number of radial checkpoints. Must be >= 3.
Default 12.
checkpoint_radius_min_frac : float
*Checkpoint generator only.* Inner-radius fraction for checkpoint
placement. Must be in ``[0, 1)``. Default 0.33.
checkpoint_angle_jitter : float
*Checkpoint generator only.* Angular jitter as a fraction of the
per-checkpoint angular slot. Default 0.55.
z_profile : str
Per-gate altitude (Z) profile family. One of ``"flat"`` (default,
every real gate at ``z_base``), ``"uniform"`` (i.i.d. uniform in
``[z_min, z_max]``), ``"random_walk"`` (Brownian-bridge walk clamped
to ``[z_min, z_max]`` with a per-step grade cap of ``z_max_step``), or
``"noise"`` (periodic harmonic noise around ``z_base``). Applied by
``warp_zprofile.apply_z_profile`` after XY ordering/relaxation and
before the 3D lift, wired into the generation pipeline by
``warp_gate._run_gate_pipeline``.
z_base : float
Baseline altitude for the ``"flat"``, ``"random_walk"``, and
``"noise"`` profiles. Default 0.0.
z_min : float
Lower altitude clamp for ``"uniform"``, ``"random_walk"``, and
``"noise"``. Must be <= ``z_max``. Default 0.0.
z_max : float
Upper altitude clamp for ``"uniform"``, ``"random_walk"``, and
``"noise"``. Must be >= ``z_min``. Default 0.0.
z_max_step : float
*Random-walk profile only.* Maximum ``|dz|`` per unit plan-view arc
length (a grade cap), applied per random-walk step before the
closure bridge. Must be >= 0. Default 0.0.
z_noise_amplitude : float
*Noise profile only.* Amplitude of the harmonic altitude
oscillation around ``z_base``. Must be >= 0. Default 0.0.
z_noise_harmonics : int
*Noise profile only.* Number of summed harmonics in the periodic
noise field. Must be >= 1. Default 3.
z_valid_grade : float
Maximum allowed ``|dz|``/ds grade for post-hoc course validity checking.
0 disables the check. Must be >= 0. Default 0.0. (Consumed
starting in Task 4; unused by ``warp_zprofile`` itself.)
gate_align : str
Gate-frame alignment mode. ``"yaw_only"`` (default) keeps gate posts
horizontal (the frame forward axis is the plan-view tangent projected
onto the XY plane, so left/right posts share the center's altitude);
``"full_tangent"`` aligns the frame forward axis with the full 3D
tangent, tilting posts to follow the course slope.
"""
generator: str = "bezier"
device: str = "cpu"
num_envs: int = 1
min_gates: int = 4
max_gates: int = 32
gate_radius: float = 0.025
gate_solve_iters: int = 8
gate_width: float = 0.0
gate_ordering: str = "ccw"
# Point-family (bezier/hull) sampler inputs. The gate path only samples corner
# anchors and emits them as gates; it never runs the Bezier/hull curve assembly,
# so num_points_per_segment, rad, edgy, handle_clamp_frac, and hull_displacement
# are inert for gate output (they remain for parity with TrackGenConfig and are
# hidden in the explorer UI). min_num_points, max_num_points, min_point_distance,
# and scale do affect the sampled anchors.
min_num_points: int = 9
max_num_points: int = 13
num_points_per_segment: int = 30
min_point_distance: float = 0.05
rad: float = 0.4
edgy: float = 0.0
scale: float = 1.0
handle_clamp_frac: float = 0.4
hull_displacement: float = 0.15
polar_num_knots: int = 12
polar_radial_jitter: float = 0.60
polar_angular_jitter: float = 0.30
voronoi_num_sites: int = 256
voronoi_site_layout: str = "void_ring"
voronoi_control_points: int = 18
voronoi_radial_variation: float = 0.62
voronoi_angular_jitter: float = 0.08
checkpoint_count: int = 12
checkpoint_radius_min_frac: float = 0.33
checkpoint_angle_jitter: float = 0.55
z_profile: str = "flat"
z_base: float = 0.0
z_min: float = 0.0
z_max: float = 0.0
z_max_step: float = 0.0
z_noise_amplitude: float = 0.0
z_noise_harmonics: int = 3
z_valid_grade: float = 0.0
gate_align: str = "yaw_only"
def __post_init__(self):
if int(self.num_envs) < 1:
raise ValueError(f"num_envs must be >= 1, got {self.num_envs!r}")
if int(self.min_gates) < 2:
raise ValueError(f"min_gates must be >= 2, got {self.min_gates!r}")
if int(self.max_gates) < int(self.min_gates):
raise ValueError(
f"max_gates must be >= min_gates, got "
f"{self.max_gates!r} < {self.min_gates!r}"
)
if float(self.gate_radius) < 0.0:
raise ValueError(f"gate_radius must be >= 0, got {self.gate_radius!r}")
if int(self.gate_solve_iters) < 0:
raise ValueError(
f"gate_solve_iters must be >= 0, got {self.gate_solve_iters!r}"
)
if float(self.gate_width) < 0.0:
raise ValueError(f"gate_width must be >= 0, got {self.gate_width!r}")
if self.gate_ordering not in {"ccw", "raw", "random_pairs"}:
raise ValueError(
"gate_ordering must be one of {'ccw', 'raw', 'random_pairs'}, "
f"got {self.gate_ordering!r}"
)
# Point-family (bezier/hull) sampler inputs. These feed the shared corner
# sampler, where min_point_distance divides a cell count and the
# [min_num_points, max_num_points] range feeds wp.randi; validate them here
# so direct API callers get a clean config-time error instead of an opaque
# ZeroDivisionError or inverted-range sample deep inside generation.
if float(self.min_point_distance) <= 0.0:
raise ValueError(
f"min_point_distance must be > 0, got {self.min_point_distance!r}"
)
if int(self.min_num_points) < 2:
raise ValueError(
f"min_num_points must be >= 2, got {self.min_num_points!r}"
)
if int(self.max_num_points) < int(self.min_num_points):
raise ValueError(
"max_num_points must be >= min_num_points, got "
f"{self.max_num_points!r} < {self.min_num_points!r}"
)
# Floor is 3 here, not 6 as in TrackGenConfig: the gate-native Voronoi
# generator selects anchor sites and emits them directly as gates, so it
# never runs the Chaikin/Catmull-Rom densification that TrackGenConfig's
# >= 6 floor protects. Three control points is the smallest non-degenerate
# gate ring, which lets callers request short gate sequences.
if int(self.voronoi_control_points) < 3:
raise ValueError(
f"voronoi_control_points must be >= 3, got {self.voronoi_control_points!r}"
)
if int(self.voronoi_num_sites) < int(self.voronoi_control_points):
raise ValueError(
"voronoi_num_sites must be >= voronoi_control_points, got "
f"{self.voronoi_num_sites!r} < {self.voronoi_control_points!r}"
)
if float(self.voronoi_radial_variation) < 0.0:
raise ValueError(
"voronoi_radial_variation must be >= 0, got "
f"{self.voronoi_radial_variation!r}"
)
if float(self.voronoi_angular_jitter) < 0.0:
raise ValueError(
"voronoi_angular_jitter must be >= 0, got "
f"{self.voronoi_angular_jitter!r}"
)
if self.voronoi_site_layout not in {"ring", "void_ring", "clustered", "mixed"}:
raise ValueError(
"voronoi_site_layout must be one of "
"{'ring', 'void_ring', 'clustered', 'mixed'}, got "
f"{self.voronoi_site_layout!r}"
)
if int(self.checkpoint_count) < 3:
raise ValueError(
f"checkpoint_count must be >= 3, got {self.checkpoint_count!r}"
)
if not (0.0 <= float(self.checkpoint_radius_min_frac) < 1.0):
raise ValueError(
"checkpoint_radius_min_frac must be in [0, 1), got "
f"{self.checkpoint_radius_min_frac!r}"
)
_validate_z_fields(self)
if self.gate_align not in {"yaw_only", "full_tangent"}:
raise ValueError(
"gate_align must be one of {'yaw_only', 'full_tangent'}, got "
f"{self.gate_align!r}"
)
[docs]
@dataclass
class GateSequence:
"""Batched fixed-stride gate result returned by ``GateGenerator``.
Gate pose arrays are flat ``[E * max_gates]`` ``vec3f`` buffers; reshape via
``wp.to_torch(...).view(E, max_gates, 3)``. ``count[e]`` gives the real gate
count for environment ``e`` and slots ``i >= count[e]`` are NaN-padded (any
NaN component marks a padded slot). Each real slot's ``z`` follows the
configured ``GateGenConfig.z_profile``; it is 0 only for the default
``"flat"`` profile at ``z_base=0``.
.. warning::
``GateGenerator.generate()`` returns the SAME ``GateSequence`` instance on
every call and overwrites its buffers in place. A reference held across two
``generate()`` calls will see mutated data. Call ``GateSequence.clone()`` to
obtain a fully-owned deep copy before the next call.
Attributes
----------
position : wp.array
Flat ``[E * max_gates]`` ``vec3f`` gate centres. Reshape via
``wp.to_torch(...).view(E, max_gates, 3)``. Slots ``i >= count[e]`` are
NaN-padded.
tangent : wp.array
Flat ``[E * max_gates]`` ``vec3f`` unit tangent vectors at each gate centre.
NaN-padded past ``count[e]``.
forward : wp.array
Flat ``[E * max_gates]`` ``vec3f`` unit pose forward (x-axis of
``orientation``): the physical gate-plane normal used for pass detection;
equals ``tangent`` for ``full_tangent`` and for planar ``yaw_only`` gates,
the horizontal projection of ``tangent`` for sloped ``yaw_only`` gates;
NaN-padded past ``count[e]``.
orientation : wp.array
Flat ``[E * max_gates]`` ``quatf`` gate poses (``wp.quatf(x, y, z, w)``):
the roll-free frame with local x = gate forward, y = left, z = up.
NaN-padded past ``count[e]``.
half_size : wp.array
Flat ``[E * max_gates]`` ``float32`` square-opening half-extents
(``0.5 * gate_width``). NaN-padded past ``count[e]``.
left : wp.array
Flat ``[E * max_gates]`` ``vec3f`` left gate endpoints
(``position + half_size * left_axis``). NaN-padded past ``count[e]``.
right : wp.array
Flat ``[E * max_gates]`` ``vec3f`` right gate endpoints
(``position - half_size * left_axis``). NaN-padded past ``count[e]``.
valid : wp.array
``[E]`` ``int32`` environment validity flags (0 or 1). Convert to a boolean
tensor via ``wp.to_torch(...).bool()``.
count : wp.array
``[E]`` ``int32`` real gate counts per env. All other arrays are NaN-padded
for slots ``i >= count[e]``.
"""
position: wp.array
tangent: wp.array
forward: wp.array
orientation: wp.array
half_size: wp.array
left: wp.array
right: wp.array
valid: wp.array
count: wp.array
[docs]
def clone(self) -> "GateSequence":
"""Return a deep copy whose Warp buffers do not alias this sequence."""
return GateSequence(
position=wp.clone(self.position),
tangent=wp.clone(self.tangent),
forward=wp.clone(self.forward),
orientation=wp.clone(self.orientation),
half_size=wp.clone(self.half_size),
left=wp.clone(self.left),
right=wp.clone(self.right),
valid=wp.clone(self.valid),
count=wp.clone(self.count),
)
[docs]
@dataclass
class Track:
"""Final batched result of the track generation pipeline.
All boundary arrays are index-aligned: ``outer[i]``, ``center[i]`` and
``inner[i]`` share a single cross-section normal. Half-width is not stored;
recover it as the outer-center norm along dim=-1.
Fields are ``wp.array``; convert at the boundary via the wp bridge.
.. warning::
``TrackGenerator.generate()`` returns the SAME ``Track`` instance on every
call and overwrites its buffers in place. A reference held across two
``generate()`` calls will see mutated data. Call ``Track.clone()`` to obtain
a fully-owned deep copy before the next call.
Attributes
----------
outer : wp.array
Flat ``[E * N_max]`` ``vec3f`` outer boundary points. Each real point's
``z`` follows the configured ``TrackGenConfig.z_profile`` — 0 only for
the default ``"flat"`` profile at ``z_base=0`` — and shares the
``center`` point's altitude at the same index (level cross-sections;
see :doc:`/tracks-25d`). Reshape via
``wp.to_torch(...).view(E, N_max, 3)``. Points at ``i >= count[e]`` are
NaN-padded (NaN xy; z carries the flat path's ``z_base``, 0 by
default).
center : wp.array
Flat ``[E * N_max]`` ``vec3f`` centerline points. ``z`` follows the
configured ``TrackGenConfig.z_profile`` (see ``outer``). Reshape via
``wp.to_torch(...).view(E, N_max, 3)``. Index-aligned with ``outer`` and
``inner``; ``‖outer[i] - center[i]‖`` gives the per-point half-width
(an XY-plane distance: level cross-sections share a Z, so the vertical
component cancels). Points at ``i >= count[e]`` are NaN-padded.
inner : wp.array
Flat ``[E * N_max]`` ``vec3f`` inner boundary points. ``z`` follows the
configured ``TrackGenConfig.z_profile`` (see ``outer``). Reshape
via ``wp.to_torch(...).view(E, N_max, 3)``. Points at ``i >= count[e]``
are NaN-padded.
tangent : wp.array
Flat ``[E * N_max]`` ``vec3f`` unit tangent vectors at each centerline
point. For the default ``"flat"`` profile this is the planar tangent
(``z = 0``) from central differences over the 2D centerline; for a
non-flat ``z_profile`` it is instead a true 3D central difference over
the LIFTED centerline (see :doc:`/tracks-25d`). NaN-padded past
``count[e]``.
normal : wp.array
Flat ``[E * N_max]`` ``vec3f`` unit left-normals at each centerline point —
still the planar left-normal, always ``z = 0``, computed as
``normal = (-tangent2d.y, tangent2d.x, 0)`` (the PLAN-VIEW tangent rotated
+90° about +z) BEFORE the elevation lift. For a non-flat ``z_profile`` this
predates the 3D recompute described under ``tangent``: the two fields are
no longer literally related by that rotation formula (``normal``'s XY
direction still matches ``tangent``'s XY projection, but ``tangent`` is
unit-length in 3D, so its XY projection alone is sub-unit and rotating it
will not reproduce a unit ``normal``). Which boundary it faces is
winding-dependent (see ``winding``): it points toward
``outer`` for clockwise loops and toward ``inner`` for counter-clockwise
loops. The ``outer``/``inner`` split itself is winding-agnostic (assigned by
signed-area magnitude), so use ``outer``/``inner`` — not the normal's sign —
to identify the boundaries. NaN-padded past ``count[e]``.
arclen : wp.array
Flat ``[E * N_max]`` ``float32`` cumulative arc length along the centerline
at each point. For the default ``"flat"`` profile this is the plan-view
(2D) arc length; for a non-flat ``z_profile`` it is the true 3D arc length
of the LIFTED centerline (strictly >= the plan-view length whenever the
profile is non-constant; see :doc:`/tracks-25d`). Reshape via
``wp.to_torch(...).view(E, N_max)``. NaN-padded past ``count[e]``.
length : wp.array
``[E]`` ``float32`` total arc length (perimeter) of each track's centerline
loop — plan-view for the default ``"flat"`` profile, true 3D length
otherwise (see ``arclen``).
valid : wp.array
``[E]`` ``int32`` environment validity flags (0 or 1). A track is valid
when: turning number ≈ 1, no NaN points, width floor exceeded everywhere,
minimum thickness ≥ ``(1 − relax_tol) * half_width``, and (when
``validity_border_check=True``) zero border self-intersections. Convert via
``wp.to_torch(...).bool()``.
count : wp.array
``[E]`` ``int32`` real point counts, derived from constant-spacing resampling
as ``count[e] = floor(perimeter / spacing) + 1``, capped at ``N_max``. All
other arrays are NaN-padded for indices ``i >= count[e]``.
winding : wp.array
``[E]`` ``float32`` signed loop winding: ``+1.0`` for a counter-clockwise
centerline, ``-1.0`` for clockwise (the sign of the loop's signed area), and
``0.0`` for degenerate/empty loops. Winding is generator-dependent (bezier
and hull wind clockwise; polar, voronoi and checkpoint wind counter-clockwise),
so this field lets a consumer orient itself: for a CCW loop ``outer`` lies to
the right of increasing arc length, for a CW loop to the left.
"""
outer: wp.array
center: wp.array
inner: wp.array
tangent: wp.array
normal: wp.array
arclen: wp.array
length: wp.array
valid: wp.array
count: wp.array
winding: wp.array
[docs]
def clone(self) -> "Track":
"""Return a fully-owned deep copy of this Track.
Each field is cloned via ``wp.clone`` (torch-free), so the returned Track owns
independent buffers unaffected by future ``generate()`` calls that overwrite
this instance in place.
"""
return Track(
outer=wp.clone(self.outer),
center=wp.clone(self.center),
inner=wp.clone(self.inner),
tangent=wp.clone(self.tangent),
normal=wp.clone(self.normal),
arclen=wp.clone(self.arclen),
length=wp.clone(self.length),
valid=wp.clone(self.valid),
count=wp.clone(self.count),
winding=wp.clone(self.winding),
)