Courses and runtime utilities

The objects that run alongside your simulator: the Course (one generator plus its components, see Courses), the component protocol and its adapters (Components), and the runtime utilities the adapters wrap (collision, props, checkpoints, progress, localization, heightfields). Each one reads a generator’s result and answers for every env at once; an invalid env reads as an env with no track (Invalid envs). import track_gen imports all of these modules.

Courses

The composition root: a generator (any Producer) plus an ordered list of components, with the presets Course.track / Course.gates and the 0.x Course(CourseConfig) (Course.from_config; config= is the 0.x keyword of the constructor). See Courses for the lifecycle and Composing a course for composing your own.

Public course facade: a generator and its runtime components in one object.

Course is the composition root: Course(generator, components) over any producer and any track_gen.tools components, the presets Course.track / Course.gates, and Course.from_config(CourseConfig) (the 0.x Course(CourseConfig), unchanged). See Course for the lifecycle (construct -> bind -> step/reset, with regenerate() / generate(seeds=...)). step(), reset(), regenerate() and generate() can be recorded into a caller’s CUDA graph as-is; StepResult gives the 0.x fields plus read-only access to every per-step component’s result by name; set_capturing is a deprecated no-op kept for compatibility.

class Course[source]

Bases: object

The composition root: one producer and its runtime components.

Three ways to build one (all run __init__, so a subclass initializes normally):

  • Course(generator, components=(), *, stream=None, prepare=True): generator is any producer (a TrackGenerator, a GateGenerator, a built track_gen.pipeline.Program, or an object with the track_gen.pipeline.Producer surface); components are track_gen.tools components (built-in adapters or your own), sorted by their after / source dependencies (stable: an ordered list is kept as given);

  • the presets track() / gates() (the 0.x component sets over a config, a PipelineSpec or a built generator, plus extra components);

  • from_config() (Course(cfg) and Course(config=cfg), the 0.x forms, are its aliases): the 0.x course, byte for byte and launch for launch.

Lifecycle: construct -> bind() (stable sim buffers; required for step() when a component declares a required input) -> per-step step() / per-env reset(), plus regenerate() / generate() for new courses. Construction attaches every component to the producer’s current batch (their auto-derivations may read it back on the host once), then, with prepare=True, runs one real warm-up regeneration; all of it must happen outside any CUDA graph capture. The 0.x sub-tool attributes are set from the components named progress, checkpoints, collision, heightfield, centerline and localizer (None when absent): progress, checkpoints (the checkpoint component’s result), checkpoint_sampler, collision, heightfield, course_line, localizer; with generator, rng, result, components, inputs, seeds.

Regeneration recomputes the whole batch (fixed-batch pipelines); per-env NEW courses come from regenerate()’s mask + advance (only the masked envs change and are reset), per-env respawns on the SAME course from reset()’s mask.

Execution order: attach, on_generate and step in the sorted order, reset in reverse order (a component is reset before the components it reads; resets read only their own state and the mask). on_generate and reset run in the masked regeneration body, under a CUDA conditional node where the capture allows one: they read no bound input and emit only kernels, fills and device-to-device copies on the call’s stream (track_gen.testing.check_component checks both).

One course per producer: building a Course over a producer that a live Course drives makes the older course raise RuntimeError on its next step / reset / regenerate / enqueue_generate / generate (its components would read a source the newer course regenerates). Regenerate through the course: calling the producer directly regenerates the source without refreshing the components.

Lifetime: the course holds the producer, every component and the StepResult; recording a call into a Warp-registered CUDA capture anchors the course on that graph, with the arrays bound when step() was recorded and the mask / seeds arrays the recorded call reads (dropping every reference, or rebinding, while the graph lives keeps every buffer it reads). A CPU graph (wp.ScopedCapture("cpu")) retains the arrays its recorded launches touch itself. Under a plain torch.cuda.graph, keep the course and those arrays alive (track_gen.interop.torch.retain(g, course, ...)).

Invalid envs (valid[e] == 0 on course.result): every built-in component answers as for a poisoned env (course.checkpoints.count = 0 with NaN slots, oob = 1 with NaN geometry, no post hit, frozen progress with dist_to_next = 0, NaN s / n, a NaN heightfield with the box (-1, -1)-(1, 1)). StepResult.valid carries the mask: set done |= 1 - valid and give those envs a NEW course with regenerate(mask=done, advance=True) (reset would respawn them on the same invalid course).

Parameters:
  • generator – The producer (see above), or a CourseConfig (the 0.x form, then components / stream / prepare=False raise TypeError).

  • components – The components (unattached instances; each belongs to one course).

  • stream – The course’s default stream: calls that pass no stream= (and run outside an ambient scope) run on it. Default: the producer’s (TrackGenerator(..., stream=s)), else resolved per call. Such a course refuses (CaptureError) a call without stream= while another stream is capturing: pass the capturing stream (or "torch") when recording.

  • prepare – Run one warm-up regeneration at construction (loads every module the regeneration body uses). False defers it: prepare() runs it, as does the first regeneration call outside a capture; recording one first raises CaptureError.

  • config – The 0.x keyword (Course(config=cfg)).

Raises:
  • TypeError – a config or spec given to the generic form (use the presets), a CourseConfig with components / stream / prepare=False, a producer missing a required member, a component missing a method.

  • ValueError – duplicate component names, a dependency that is not a component, a cycle, an attached component, an input declared with two specs or by a component and the generator.

  • track_gen.exec.CaptureError – a capture is open (before anything is allocated).

__init__(generator=None, components=(), *, stream=None, prepare=True, config=None)[source]
Parameters:
Return type:

None

classmethod from_config(config)[source]

The 0.x course of a CourseConfig (Course(config) is an alias): the generator and the components the config selects. For what changed since 0.1 (invalid envs, the default checkpoint capacity), see Course presets.

Raises:

TypeError – config is not a CourseConfig.

Parameters:

config (CourseConfig)

Return type:

Course

classmethod track(source, *, checkpoint_spacing, collision=None, max_checkpoints=None, extra=(), seeds=None, stream=None, prepare=True)[source]

A track course: [Checkpoints, Progress, collision] + extra.

Parameters:
  • source – A TrackGenConfig or a track PipelineSpec (the preset builds TrackGenerator(source, seeds=seeds, stream=stream)), or a built TrackGenerator / track Program (used as is: seeds= / stream= raise TypeError).

  • checkpoint_spacing (float) – Centerline checkpoint spacing (a number > 0).

  • collision (str | object | None) – None (default, as CourseConfig.collision), "segments" / "sdf" (a tools.BandCollision of that method), or a component named "collision" (it fills StepResult.contacts and course.collision; e.g. tools.BandCollision("sdf", sdf_resolution=256, max_boxes=2)).

  • max_checkpoints (int | None) – The checkpoint capacity per env (an int >= 3); None or "auto": sized for the longest track the generator can produce, else the readback of the construction batch (tools.Checkpoints(max_checkpoints="auto")), with a UserWarning when it clips a valid env of the first batch.

  • extra – More components, a sequence (sorted with the preset’s by their dependencies).

  • seeds – The generator’s seeds (anything TrackGenerator(seeds=) takes); a PerEnvSeededRNG becomes course.rng (generate(seeds=) then reseeds through it, as 0.x).

  • stream (Stream | None) – The default stream of the generator and the course.

  • prepare (bool) – See Course.

Raises:
  • ValueError – a bad spacing / collision / capacity, a collision= component not named "collision", an extra that clashes with the preset’s names or dependencies (all before anything is built).

  • TypeError – a source of the other family or of another type, seeds= / stream= with a built generator, a single component as extra.

Return type:

Course

classmethod gates(source, *, collision=None, samples_per_gate=8, localize_window=None, extra=(), seeds=None, stream=None, prepare=True)[source]

A gate course: [GateCheckpoints, CenterlineFromGates, Progress, collision, Localizer] + extra (the localizer on the gates’ centerline).

Parameters:
  • source – A GateGenConfig or a gate PipelineSpec (the preset builds GateGenerator(source, seeds=seeds, stream=stream)), or a built GateGenerator / gate Program (used as is: seeds= / stream= raise TypeError). Its construction gate_frames.gate_width must be > 0, or, when a randomizer draws the width (with_randomize({"gate_frames.gate_width": (lo, hi)})), that randomizer’s construction lo (checked before anything is built); the preset then refuses a later scalar set of a width <= 0 on the generator it uses (a given one included).

  • collision (float | object | None) – None, a post radius > 0 (a tools.DiscCollision), or a component named "collision" (e.g. tools.FrameCollision(radius, thickness, depth), tools.DiscCollision(r, max_boxes=2)).

  • samples_per_gate (int) – Centerline samples per gate (>= 2).

  • localize_window (int | None) – The localizer’s warm_window (None: full scans).

  • extra – More components, a sequence (sorted with the preset’s by their dependencies).

  • seeds – As track().

  • stream (Stream | None) – As track().

  • prepare (bool) – As track().

Raises:
  • ValueError – a bad radius, a collision= component not named "collision", gate_width <= 0 (or a width randomizer’s lo <=     0), an extra that clashes (all before anything is built).

  • TypeError – a source of the other family or of another type, seeds= / stream= with a built generator, a single component as extra.

Return type:

Course

property graph: Graph | None

The course’s own regeneration graph (producer + every component’s refresh as ONE graph) for a plain regenerate() / generate(): captured on the first such CUDA call made outside any capture, replayed by later ones. None on CPU, for a producer with HOST stages, and before that call. Each (advance, mask) variant of regenerate() caches one more graph.

property seeds

The producer’s SeedState (the per-env seeds, bound by pointer or owned, plus the per-env epoch); None for a producer without one.

property inputs: MappingProxyType

Persistent device inputs (stable pointers, allocated at construction):

  • "regen_mask": [E] int32, zero-initialised. Write the envs to regenerate from your own kernels (e.g. done flags inside your sim graph) and pass it as regenerate(mask=course.inputs["regen_mask"], advance=True). Its pointer never changes, so the course’s own CUDA graph for it is captured once.

  • "reset_mask": [E] int32, zero-initialised: the envs to respawn on their current course, passed as reset(course.inputs["reset_mask"]).

The generator’s own inputs stay at course.generator.inputs: write them in place (they are read by every regeneration and every replay).

property components: MappingProxyType

The components, name -> component, in execution order (read-only).

bind(position=<required on a CourseConfig course>, orientation=None, half_extents=None, box_position=None, **arrays)[source]

Bind stable sim buffers to the components that declare them (required before step() when one declares a required input).

Every argument is routed by name to every component whose inputs declare it (Progress, Localizer and your own all take position), or that names it as an input’s default (box_position defaults to position with one box per env; an explicitly given name wins over the default). The four 0.x names (position, orientation, half_extents, box_position) are accepted on every course and ignored when no component declares them.

position is the [E] vec3f agent-position buffer. With a box collision component, orientation ([E * max_boxes] quatf box poses) and half_extents ([E * max_boxes] vec2f, planar boxes) are required too; with max_boxes == 1 the same position buffer serves as the box positions, otherwise pass a separate box_position [E * max_boxes] vec3f buffer. May be called any time after construction (before or after a regeneration).

A bind is complete and atomic: every route is validated before anything is rebound, and a rebind replaces every binding; when a component refuses (on_bind raising part-way), the components already rebound by the call and the one that refused are bound again to their previous arrays, their rebind-after-capture probes are restored, and the error propagates (every previous binding stays in place, and a later rebind after a capture still warns).

Do NOT rebind after capturing step() into a sim graph: the captured graph replays against the buffer pointers live at capture time, so a later rebind leaves it reading the old buffers (silently divergent results) — keep writing into the SAME bound buffers and rebind only before (re)capturing. Such a rebind (other arrays, after step() ran while its stream or a CPU graph capture was capturing) warns once, RebindAfterCaptureWarning; rebinding the same arrays is silent. The recipe “rebind, then record again” warns once too: filter the category when you re-record on purpose. The course’s own regeneration graphs never read bound inputs, so they stay valid.

The generator’s inputs (course.generator.inputs, e.g. "centerline.xy" of generator="external") are not bound here: write them in place.

Raises:
  • TypeError – an unknown name, a generator input name (with the in-place recipe), a missing required input; on a config course, bind() without position.

  • ValueError – an array of the wrong shape, dtype or device (position=None on a config course: the 0.x message).

  • RuntimeError – a config course with a box collision checker bound without orientation / half_extents (or box_position with max_boxes     > 1).

Parameters:
  • position (array)

  • orientation (array | None)

  • half_extents (array | None)

  • box_position (array | None)

  • arrays (array)

Return type:

None

prepare()[source]

Load every module the regeneration bodies use, outside any capture, and wait for it: a no-op once prepared (construction with prepare=True included).

For a course built with prepare=False: one real masked regeneration on a private all-zero mask (an exact identity: no env’s course or runtime state changes), synchronized. The first regeneration call outside a capture runs it too; recording one before it raises CaptureError.

Raises:

track_gen.exec.CaptureError – a capture is open.

Return type:

None

regenerate(*, mask=None, advance=False, stream=None, sync=True)[source]

Regenerate courses plus a coherent refresh of every component; returns result.

mask=None (whole batch): the producer’s generation from the current seeds (after advancing EVERY env’s epoch when advance=True), then every component’s on_generate (checkpoint resample / sdf and heightfield bake / posts and line rebuild as applicable, your components’ per-generation work) and a FULL reset of every component (progress, localizer, …) — every course changed (or, without advance, was rebuilt identically).

mask given ([E] int32 on the course device, read on device): envs with mask[e] != 0 advance their epoch (when advance=True) and get the course of their live (seed, epoch), and their runtime state is reset (every component’s reset(ctx, mask), in reverse order); envs with mask[e] == 0 keep their course and runtime state. The pipeline is fixed-shape, so it still recomputes every env: unmasked envs keep the effective seed their current course was built from and are recomputed to identical values, even if their seed or epoch changed since (rng.set_seeds_warp, seeds.reset_epoch(), a write to the seed array): such a change reaches an env at its next masked regeneration, or every env at the next one without a mask. The generator’s commit also restores an unmasked env’s Track rows (on a gate course its GateSequence rows) bit for bit after the recompute (track_gen.stages.commit). Without advance a mask respawns the masked envs (as reset()) on the course of their current (seed, epoch): the same one unless their seed was rewritten.

Conditional skip: the masked body runs after a device-side any_set(mask) under track_gen.exec.LaunchContext.skip_unless(). Inside a Warp-registered capture (Warp ScopedCapture, the course’s own CUDA graph, capture_begin(external=True), or track_gen.interop.torch.registered(conditional=True) under torch.cuda.graph) with CUDA Toolkit and driver >= 12.4, it is skipped on device when no env is masked; the caller’s graph then holds a conditional node, so it can no longer be used as a CUDA child graph, and wp.capture_if ends and restarts the capture: join every side stream before this call (or wrap it in track_gen.exec.unconditional()). Every component’s on_generate and reset run inside that conditional body (kernels, fills and device-to-device copies only; a refused body raises CaptureError naming the components). Elsewhere it runs unconditionally, which is correct because an all-zero mask makes it a no-op: nothing advances, nothing is reset, and every env is recomputed from the effective seed its current course was built from (the producer’s commit restores every row; the built-in components recompute from the unchanged result).

Execution (stream resolved per track_gen.exec):

  • inside a capture on the resolved stream: RECORDED into the caller’s graph (e.g. next to step() / reset()); each replay regenerates from the seeds, epochs and mask at replay time (and, with advance, advances again);

  • outside a capture on CUDA: replay of the course’s own CUDA graph (producer + refresh as ONE graph, captured on the first call of each (advance, mask) variant; graph is the plain one);

  • on CPU, or with a producer that has HOST stages: eager (a Course does not segment).

A course built with prepare=False prepares first (see prepare()).

Parameters:
  • mask (array | None) – optional contiguous [E] int32 regeneration mask. Pass a STABLE buffer (e.g. inputs["regen_mask"]): outside a capture each distinct mask pointer captures, and caches, one more graph of the course’s own (up to a bound, past which new pointers run eagerly).

  • advance (bool) – advance the epoch (of every env, or of the masked envs) on device first, so those envs get new courses. Default False (rebuild from unchanged seeds and epochs).

  • stream – optional wp.Stream (or "torch") to run on.

  • sync (bool) – block the host until the regeneration finished on the resolved stream (default True; never while capturing, nor while torch is capturing in the calling thread).

Raises:
  • ValueError – mask is not a contiguous [E] int32 array on the course device (before any launch).

  • RuntimeError – a newer Course drives this course’s producer.

  • track_gen.exec.NotCapturableError – recording a course whose producer has HOST stages (before any launch).

  • track_gen.exec.CaptureError – recording a course that was never prepared; a stream= course called without stream= under another stream’s capture; a component’s on_generate / reset refused in a conditional body.

In track mode, also check course.checkpoint_sampler.truncated afterwards: a per-env [E] int32 flag (1 if max_checkpoints clipped that env’s checkpoint ring), mirroring PropSet.truncated for prop rings.

enqueue_generate(*, stream=None, mask=None, advance=False)[source]

Enqueue one regeneration (as regenerate(), same mask / advance) without building the course’s own CUDA graph or blocking: recorded into the caller’s capture when the resolved stream is capturing, otherwise eager on that stream. Returns result (stream-ordered).

Parameters:
  • mask (array | None)

  • advance (bool)

generate(seeds=None, *, stream=None, sync=True)[source]

Optional reseed, then regenerate(). Returns result.

seeds int k reseeds every env with k + e (the same expansion as constructing a fresh PerEnvSeededRNG(seeds=k)), expanded device-side into a persistent buffer: no allocation. A wp.array must be [E] int32 on the course device. The reseed and the regeneration run on the SAME resolved stream, so this whole call can be recorded into a caller’s capture; note that an int k is then BAKED into the caller’s graph (every replay reseeds with k), while a wp.array is read at replay time (write new seeds into it to vary replays).

Determinism contract: the generators are deterministic under an unchanged (seed, epoch). Calling generate() again WITHOUT seeds= reproduces the identical batch (plus a full reset); pass seeds= to reseed and get new courses. A reseed also zeroes every env’s epoch (on device, in the same unit), so generate(seeds=s) reproduces a fresh course built with seeds=s whatever was advanced before.

Who writes the seeds follows their owner’s rule: with rng (a config course, or a generator built with a PerEnvSeededRNG) the reseed goes through rng.set_seeds_warp, which overwrites rng.seeds_warp in place: when CourseConfig.seeds was a wp.array, that IS your array (the rng binds it without a copy), so generate(seeds=...) rewrites it. Without an rng, owned seeds are set with SeedState.set (a copy, epochs zeroed); seeds bound to your array are never written (RuntimeError: write the array, then regenerate()).

Parameters:
  • seeds (int | array | None) – None, an int, or an [E] int32 wp.array.

  • stream – optional wp.Stream (or "torch") to run on.

  • sync (bool) – block the host until done (default True; never while capturing).

Raises:
  • TypeError – the producer has no SeedState (before any launch).

  • RuntimeError – seeds bound to a caller array with no rng; a newer Course drives this course’s producer.

step(stream=None)[source]

Every component’s step (progress update, collision query, localization, your components), in execution order, on the bound buffers.

Returns the same StepResult instance every call (its entries: the per-step components’ results).

Parameters:

stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else the course’s default stream, else Warp’s current stream). Every component launch of this call goes to that one stream.

Raises:
  • RuntimeError – a component declares a required input and bind() was not called; a newer Course drives this course’s producer.

  • track_gen.exec.CaptureError – a stream= course called without stream= under another stream’s capture.

Return type:

StepResult

reset(mask=None, stream=None)[source]

Per-env respawn on the SAME course: every component’s reset (progress state, the localizer’s warm-start memory, your stateful components), in reverse execution order, where mask[e] != 0. Collision, checkpoints and bakes derive from the course geometry and are unaffected by respawns.

Parameters:
  • mask (array | None) – [E] int32 respawn mask (e.g. inputs["reset_mask"]); None: every env.

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else the course’s default stream, else Warp’s current stream). Every component launch of this call goes to that one stream.

Return type:

None

static set_capturing(flag)

Deprecated no-op, kept for compatibility. Removed in 1.0.

Utilities no longer host-synchronize, so query() / update() / reset() / sample() / bake() / refresh() and Course.step() / reset() can be recorded into a CUDA graph without any flag.

Parameters:

flag (bool)

Return type:

None

class CourseConfig[source]

Bases: object

Configuration for Course. Strict option applicability: inapplicable options raise instead of being silently ignored.

mode

"track" or "gates".

Type:

str

gen

Generator config; its type must match mode. num_envs and device are taken from here. Gates mode requires gen.gate_width > 0 (a width-0 gate can never be crossed). The width is per env (gate_frames.gate_width): a scalar set of a value <= 0 is refused, but an [E] array written after construction is not range-checked (a value <= 0 is used as 0): keep it > 0, or those envs’ gates can never be passed.

Type:

TrackGenConfig or GateGenConfig

seeds

Initial per-env RNG seeding (forwarded to PerEnvSeededRNG).

Type:

int or wp.array

collision

Track mode only: "segments", "sdf", or None (no out-of-bounds checking — progress-only bundles are legal).

Type:

str or None

sdf_resolution

Track mode with collision="sdf" only; None -> 128. The facade always uses CollisionChecker’s AUTO SDF padding (sdf_padding=None, i.e. 10% of each env’s larger AABB extent); sdf_padding itself is not exposed here.

Type:

int or None

heightfield_resolution

Track mode only; when set (>= 8), builds a HeightFieldBaker exposed as course.heightfield and re-baked on every generate() (nearest-edge road-surface grid for external physics solvers). Uses the baker’s AUTO padding (10% of each env’s larger AABB extent). None (default) means no heightfield.

Type:

int or None

post_radius

Gates mode only: > 0 enables DiscChecker gate-post collision.

Type:

float

frame_collision

Gates mode only: True enables FrameChecker sphere-vs-gate-frame collision (each square gate = four thin oriented frame members). Mutually exclusive with post_radius > 0 (choose discs OR frames); requires frame_thickness, frame_depth and agent_radius all > 0.

Type:

bool

frame_thickness

Gates mode with frame_collision only (required, > 0): thickness of the frame members (their extent across the opening plane).

Type:

float

frame_depth

Gates mode with frame_collision only (required, > 0): depth of the frame members along the gate-forward axis.

Type:

float

agent_radius

Gates mode with frame_collision only (required, > 0): the agent sphere radius for frame collision (posts are exclusive, so this cannot ride post_radius).

Type:

float

checkpoint_spacing

Track mode only (required there): centerline checkpoint spacing.

Type:

float or None

max_checkpoints

Track mode only: the checkpoint capacity per env (>= 3), forwarded to CheckpointSampler. None (default): the plan’s size-independent bound max(3, ceil(max_loop_length / checkpoint_spacing)) with max_loop_length = 1.5 * N_max * spacing + dZ (track_gen.tools.ComponentContext.for_producer(); 70 at the TrackGenConfig defaults and checkpoint_spacing=0.5), or, when the plan declares none (an elevation whose z_min / z_max declare no range, a variable width, a plugin geometry stage), the construction batch’s readback (ceil(1.5 * max valid centerline perimeter / checkpoint_spacing)). A defaulted capacity that clips a valid env of the first batch warns (UserWarning) at construction. To keep a fixed shape (the capacity an earlier release derived from the batch, for example), pass it explicitly (course.checkpoints.position.shape[0] // num_envs reads the current one).

Type:

int or None

max_boxes

Collision query stride (boxes per env). Must be >= 1.

Type:

int

samples_per_gate

Gates mode only: centerline samples per gate for the CourseLine spline (>= 2). Default 8.

Type:

int

localize_window

Gates mode only: forwarded as TrackLocalizer(warm_window=...); None (default) means full cold scans every query.

Type:

int or None

__init__(mode, gen, seeds=0, collision=None, sdf_resolution=None, heightfield_resolution=None, post_radius=0.0, frame_collision=False, frame_thickness=0.0, frame_depth=0.0, agent_radius=0.0, checkpoint_spacing=None, max_checkpoints=None, max_boxes=1, samples_per_gate=8, localize_window=None)
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
class StepResult[source]

Bases: object

Per-step bundle returned by Course.step().

Holds the components’ preallocated in-place results: the SAME StepResult (and the same underlying buffers) is returned on every step(); use clone() for snapshots.

It also gives read-only mapping access to the result of every per-step component (one that overrides Component.step), by component name, in execution order: res["progress"], res.get(name), keys(), values(), items(), name in res, dict(res). Per-generation components (checkpoints, the course line, bakes, props) are read through course.components[name].result. It is not a collections.abc.Mapping: it stays truthy, iter(res) raises TypeError and dataclass utilities see the four 0.x fields only (dataclasses.replace rebuilds the entries from them; the repr is the dataclass one). Assigning a 0.x field (res.events = x, as tree utilities that copy a dataclass and set its fields do) updates its entry too (None removes it), without touching the instance it was copied from.

events

Progress events for this step: res["progress"] (None on a course without a component named progress).

Type:

ProgressEvents

contacts

Collision result (a BoxContact, a DiscContact or a FrameContact): res["collision"] (None when the course has no component named collision).

Type:

object or None

frame

Localization frame: res["localizer"] (None without a component named localizer: the 0.x track mode).

Type:

TrackFrame or None

valid

[E] int32 validity of each env’s course: an alias of course.result.valid (None only on a hand-built instance, or for a producer whose result has no valid). The other fields of an env with valid[e] == 0 hold the invalid-env sentinels (see Course): gate rewards and observations on it with a select (where(valid, x, 0), never a multiply: NaN * 0 is NaN) and end such episodes. It is producer state and live: never write into it (exclude it from code that zeroes or poisons the step outputs; dataclasses.fields lists it, the mapping entries do not), and read it before a regenerate in the same iteration, which rewrites it for the regenerated envs (or clone() the result).

Type:

wp.array or None

__getitem__(name)[source]

The result of the per-step component name.

Raises:

KeyError – no per-step component has that name (the message lists them).

Parameters:

name (str)

get(name, default=None)[source]

The result of the per-step component name, else default.

Parameters:

name (str)

keys()[source]

The names of the per-step components, in execution order.

values()[source]

Their results, in execution order.

items()[source]

(name, result) pairs, in execution order.

clone()[source]

Deep-copy every entry (and valid): a wp.array with wp.clone, an object with a clone() method with it, a dataclass as a shallow copy whose wp.array fields are cloned (cold: allocates).

Raises:

TypeError – an entry is none of these (the message names its component).

Return type:

StepResult

__init__(events, contacts, frame, valid=None)
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
set_capturing(flag)[source]

Deprecated no-op, kept for compatibility. Removed in 1.0.

Utilities no longer host-synchronize, so query() / update() / reset() / sample() / bake() / refresh() and Course.step() / reset() can be recorded into a CUDA graph without any flag.

Parameters:

flag (bool)

Return type:

None

Components

The component protocol and the adapters of the runtime utilities; see Writing a Component to write your own.

Components: runtime consumers of a generator’s result.

A Component declares what it reads (requires / optional capability protocols of its read target, source, topology), what it is bound to (inputs: InputSpec), and whether it keeps per-env state (stateful); a ComponentContext checks the declaration and attaches it, with the producer’s static capacities (ComponentContext.for_producer()). After attach its hot methods (on_generate, step, reset) launch through the LaunchContext they are given, allocate nothing and never synchronize, so a Course (or your sim loop) can record them into a CUDA graph:

import warp as wp

from track_gen import TrackGenerator, tools
from track_gen.exec import LaunchContext
from track_gen.pipeline import presets

E, device = 64, "cuda:0"
gen = TrackGenerator(presets.track(num_envs=E, device=device))
pos = wp.zeros(E, dtype=wp.vec3f, device=device)       # your agents' positions

track = gen.generate()                                 # the producer's result
cc = tools.ComponentContext.for_producer(gen)          # layout + plan capacities
cps = cc.add(tools.Checkpoints(0.5), track)            # plan-bound capacity
prog = cc.add(tools.Progress(), track)                 # reads cps.result
prog.bind(position=pos)
ctx = LaunchContext.for_call(track.layout.device)
events = prog.step(ctx)

The built-in adapters each wrap one 0.x utility (adapter.utility; the legacy classes keep every name): BandCollision (CollisionChecker), DiscCollision (DiscChecker), FrameCollision (FrameChecker), Props (PropSampler), Checkpoints (CheckpointSampler), GateCheckpoints (CheckpointSet.from_gates), Progress (ProgressTracker), Localizer (TrackLocalizer), HeightFieldBake (HeightFieldBaker) and CenterlineFromGates (CourseLine: its result is the Centerline). track_gen.testing.check_component checks a component (yours or a built-in) against the contract.

class Component[source]

Bases: object

A runtime consumer of a producer’s result, driven by a Course (or by hand).

A plain base class with immutable defaults (subclass it; the library duck-types and never isinstance-checks a component). Declare, as class attributes or in __init__:

  • name: unique among the components attached to one ComponentContext;

  • source: the read target: None for the producer’s result given to ComponentContext.add(), else the name of the component whose result it reads;

  • requires: capability protocols (track_gen.results) of the read target whose members are all read (strict: a missing one raises TypeError);

  • optional: protocols whose members are read when present (valid, generation: checked like the required ones when present);

  • topology: "closed" (refuses a read target with layout.closed=False) or "any";

  • stateful: whether it keeps per-env runtime state that reset() clears;

  • inputs: name -> InputSpec of the arrays bind() takes;

  • after: names of other components it reads (attached first).

ComponentContext.add() sets layout (the read target’s layout) and calls attach(), which sets result (allocated once, never replaced). Override attach(), on_bind() and the hot methods; never bind(). (The built-in adapters also expose the legacy utility they wrap as utility; it is not part of the component contract.)

Contracts (checked by track_gen.testing.check_component()): attach is cold (once, outside any capture), allocates every buffer, launches through cc.ctx, ends with one on_generate(cc.ctx, None) and loads the module of every kernel its hot methods launch; the hot methods launch through the ctx they are given only, never allocate, synchronize, read back or change Python state that alters a later emission; on_generate(ctx, mask) leaves the rows and state of envs with mask[e] == 0 bitwise unchanged (a whole-batch recompute qualifies); step returns result; reset(ctx, mask) clears the per-env runtime state where mask[e] != 0 and nothing else (a stateless component emits nothing).

In a Course: on_generate and reset run in the masked regeneration body, under a CUDA conditional node where the capture allows one, so they read and write no bound input (step reads them; a respawn snapshot of an input belongs in step: reset arms a per-env flag that the next step consumes) and emit only kernels, ctx.fill and device-to-device ctx.copy on ctx’s stream (no copy to or from pageable host memory, no event, no other stream; CUDA accepts a copy from pinned, device-mapped host memory, but keep the body to device memory for portability); reset reads only the component’s own state and the mask (resets run in reverse order, before the components they read); a component that overrides step is an entry of the course’s StepResult (its result), one that keeps the base step (per-generation work) is read through course.components[name].result.

name: str = 'component'
source: str | None = None
requires: tuple = ()
optional: tuple = ()
topology: str = 'closed'
stateful: bool = False
inputs: Mapping = mappingproxy({})
after: tuple = ()
layout: Layout | None = None
result: object = None
attach(source, cc)[source]

Cold, once: allocate, set result, end with on_generate(cc.ctx, None) and load the hot kernels’ modules. source is the read target, already checked against requires / optional.

Parameters:

cc (ComponentContext)

Return type:

None

bind(**arrays)[source]

Bind the stable input arrays (cold, before recording step()).

Routes InputSpec.default (an input not given takes the array of its default name), refuses unknown names and missing required ones (TypeError), checks each array against its spec on layout (ValueError; a routed input’s message names the argument it was routed from), then calls on_bind() with every declared input (None for an absent optional one). A name that is only the default of a declared input is accepted (and routed). The built-in adapters warn once (RebindAfterCaptureWarning) when an array is replaced after a hot method that reads it was recorded into a capture (their wrapped utilities arm that probe); a component of your own warns only if its hot methods do the same: record the graph again after rebinding.

Parameters:

arrays (array)

Return type:

None

on_bind(**arrays)[source]

Hook: the checked input arrays (pin them; an absent optional input is None). Default: nothing.

Parameters:

arrays (array)

Return type:

None

on_generate(ctx, mask)[source]

Hot: after every generation of the source (mask: its [E] int32 mask, or None for the whole batch). Default: nothing.

Parameters:
Return type:

None

step(ctx)[source]

Hot: per sim step; returns result. Default: returns it.

Parameters:

ctx (LaunchContext)

reset(ctx, mask)[source]

Hot: clear per-env runtime state where mask[e] != 0. Default: nothing.

Parameters:
Return type:

None

class ComponentContext[source]

Bases: object

What Component.attach() is given: the producer’s layout, its static capacities, a cold launch context and the components attached before.

Parameters:
  • layout – The producer’s Layout.

  • capacity – Static capacities of the producer’s plan (name -> number; see for_producer()). Hand-built sources pass their own.

  • ctx – The launch context of the cold work of attach (default: LaunchContext.for_call(layout.device): an enclosing ctx.scope(), else Warp’s current stream). Attach on the stream where allocations happen (Warp’s current stream: attach allocates and zero-fills there, then launches its first on_generate on ctx; a context on another stream races its own allocations), after the producer’s work completed on it (or make that stream wait for the producer’s).

  • bounds – Declared bounds of the producer’s plan (name -> number; see for_producer()): size_hi, the largest course size the plan can draw. Read by BandCollision / HeightFieldBake with size_hi="auto". Hand-built sources pass their own (default: none).

__init__(layout, capacity=None, *, ctx=None, bounds=None)[source]
Parameters:
Return type:

None

property layout: Layout

The producer’s layout.

property capacity: Mapping

The producer’s static capacities (read-only mapping). max_loop_length: an upper bound, with measured headroom, on the 3D length of the centerline and of either boundary loop of every valid env (see for_producer()); a consumer sizes from it, clamps to its own capacity and reports truncation.

property bounds: Mapping

The producer’s declared bounds (read-only mapping; see for_producer()). size_hi: the largest course size the plan can draw (a size randomizer’s construction hi, else the generator’s construction size); absent when the plan declares no size.

property ctx: LaunchContext

The launch context of the cold work of attach.

property names: tuple[str, ...]

The names of the attached components, in attach order.

component(name)[source]

The attached component named name.

Raises:

KeyError – none is attached under that name (the message lists them).

Parameters:

name (str)

Return type:

Component

add(component, source)[source]

Check component’s declaration and read target, attach it and register it.

The read target is source (the producer’s result) when component.source is None, else the result of the attached component it names. The target is checked against component.requires (every member) and component.optional (the members it has; with optional alone and a target without a layout, against the layout its present members give, this context’s layout filling what they do not), with component.topology; component.layout is set (to this context’s layout when nothing is declared), then component.attach(target, self) runs. A producer-result target (component.source is None) must hold this context’s batch: the same num_envs and device.

Returns:

component.

Raises:
  • ValueError – a component named in after / source is not attached, the name is taken, an input name is declared by an attached component with a different (dtype, slots), the target does not match (the check_source messages), or a producer-result target holds another batch (num_envs or device) than this context’s layout.

  • TypeError – a malformed declaration, or a required member missing on the target.

Return type:

Component

classmethod for_producer(producer, *, ctx=None)[source]

The context of a producer’s components: its layout and the static capacities of its plan (no device readback).

producer is a TrackGenerator / GateGenerator (its program), a built Program, a Course (its generator), or any object with the track_gen.pipeline.Producer surface that declares capacity and / or bounds itself (read before the plan); its spec / params / layout are read. A result (gen.generate()’s Track) has no plan: build a ComponentContext(track.layout, capacity) for a hand-built source.

Bound size_hi (bounds): the construction hi of a randomize stage drawing size (generator.size / normalize.size; the largest of them), else the construction size of the generator (or of the normalize stage of a ring gate pipeline); omitted when the plan has no size (external, a legacy spec). It is the declared bound, never a current value: a later set("hi", ...) or set("size", ...) curriculum is not seen.

Capacity key max_loop_length (0.x): 1.5 * N_max * spacing + dZ from the ("resample", "constant_spacing") slot’s spacing and the batch’s n_max. A non-truncated env’s plan-view centerline is shorter than N_max * spacing before relax (else the resample truncates it and the env fails validity); the 1.5 covers relax growth and a constant-width boundary’s extra 2 pi * half_width (measured, not proven: at most 0.904 of N_max * spacing). dZ bounds the elevation’s total variation from the knobs’ DECLARED bounds (bounds(), never their current values): 0 for elevation.flat; K * (z_max_hi - z_min_lo) for uniform / random_walk (K = z_control_points); min(2 * H * (z_max_hi - z_min_lo), 4 * A_hi * H / H_H) for noise (H = z_noise_harmonics, H_H = sum_{k<=H} 1/k, A_hi the amplitude’s upper bound); g * 1.5 * N_max * spacing with validity.z_valid_grade = g > 0; the smallest finite term (only the grade of the built-in validity.default counts: a plugin check promises nothing about the final altitudes). The key is OMITTED (pass explicit capacities) when no term is finite (an elevation whose z_min / z_max declare no range: every elevated TrackGenConfig today; a randomizer’s range does not count: its lo / hi are dynamic), when the altitudes come from the generator (it provides z: lift.elevated with no elevation stage after the resample) and no grade is declared, when no width.constant stage follows the resample (another width stage, or a generator that provides half_width: an unbounded per-point width), with a plugin stage of kind relax / width / offset / elevation / lift / resample after the constant-spacing resample, and without that resample (gate pipelines).

Raises:

TypeError – producer has no plan and declares neither capacity nor bounds (a Track or another result, a minimal producer), or it has no Layout.

Parameters:

ctx (LaunchContext | None)

Return type:

ComponentContext

class InputSpec[source]

Bases: object

One bound input of a component: a [E * slots] array of dtype.

dtype

The Warp dtype (wp.vec3f, wp.quatf, wp.vec2f, wp.float32, …).

Type:

type

slots

Elements per env: the array is [E * slots].

Type:

int

required

Whether Component.bind() must be given it (after default routing). An absent optional input reaches Component.on_bind() as None (def on_bind(self, position, speed=None) style hooks work, and so does def on_bind(self, position, speed)).

Type:

bool

default

Another input name whose array is used when this one is not given (e.g. box_position defaults to position with one box per env).

Type:

str | None

doc

Free text (not part of the equality).

Type:

str

check(name, array, layout, *, consumer)[source]

Check array against this spec on layout: a contiguous 1-D [num_envs * slots] array of dtype on the layout’s device.

Raises:

ValueError – naming consumer and name.

Parameters:
Return type:

None

__init__(dtype, slots=1, required=True, default=None, doc='')
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)

Adapters

Each adapter wraps one runtime utility (.utility) and exposes its result (.result).

class BandCollision[source]

Bases: Component

Out-of-bounds boxes against the road band (wraps CollisionChecker).

Read target: the producer’s result (HasBand). Inputs: box_position (default="position" when max_boxes == 1), orientation, half_extents ([E * max_boxes]). on_generate: the SDF bake (method="sdf"); step: query -> BoxContact. With size_hi (the largest size a size curriculum will reach) an "sdf" grid below ceil(reference_resolution * size_hi) texels per side warns at attach. size_hi="auto" (the default) reads the producer’s declared bound (cc.bounds["size_hi"]: a size randomizer’s construction hi, else the construction size); None declares none.

__init__(method='segments', *, max_boxes=1, sdf_resolution=128, sdf_padding=None, size_hi='auto', reference_resolution=128, name='collision')[source]
Parameters:
  • method (str)

  • max_boxes (int)

  • sdf_resolution (int)

  • sdf_padding (float | None)

  • size_hi (float | str | None)

  • reference_resolution (int)

  • name (str)

Return type:

None

classmethod __new__(*args, **kwargs)
class DiscCollision[source]

Bases: Component

Boxes against discs (wraps DiscChecker).

Posts mode (discs=None): the read target is the producer’s gates (HasGateFrames): on_generate interleaves their left / right posts into the owned posts ([2 * E * max_gates]) and writes post_count (2 * count); the gates’ valid makes an invalid env hit nothing. Raw mode (discs= a [E * D] vec3f array, optional count / valid [E] int32): no read target (requires=(), topology="any"), the arrays are aliased. Inputs as BandCollision; step: query -> DiscContact.

__init__(radius, *, max_boxes=1, discs=None, count=None, valid=None, name='collision')[source]
Parameters:
  • radius (float)

  • max_boxes (int)

  • discs (array | None)

  • count (array | None)

  • valid (array | None)

  • name (str)

Return type:

None

classmethod __new__(*args, **kwargs)
class FrameCollision[source]

Bases: Component

A sphere against the gate frames around the current target (wraps FrameChecker; num_envs comes from the layout).

Read target: the producer’s gates (HasGateFrames); after=(progress,): the query windows on that component’s result.next_checkpoint. Input: position. step: query -> FrameContact.

__init__(radius, frame_thickness, frame_depth, *, window=4, progress='progress', name='collision')[source]
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
class Props[source]

Bases: Component

Boundary prop poses (wraps PropSampler).

Read target: the producer’s result (HasBand). Capacity: max_props, or max(3, ceil(max_loop_length / spacing)) from the producer’s plan (ValueError without it). on_generate: sample; step: the PropSet.

__init__(spacing, *, boundary='outer', mode='points', max_props=None, name='props')[source]
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
class Checkpoints[source]

Bases: Component

Track checkpoints (wraps CheckpointSampler).

Read target: the producer’s result (HasBand; generation read when present). Capacity: max_checkpoints (an int >= 3); None: max(3, ceil(max_loop_length / spacing)) from the producer’s plan (ValueError without it); "auto": that bound, else, when the plan declares none (an elevated config, a variable width, a plugin geometry stage), the readback of the batch it attaches to (ceil(1.5 * max valid centerline perimeter / spacing), one cold host read; the CheckpointSampler(max_checkpoints=None) default). defaulted says how a capacity it chose itself was found (None for an explicit int); the sampler’s max_checkpoints is the capacity used. on_generate: sample (the set’s valid / generation are written with its rows); step: the CheckpointSet.

__init__(spacing, *, max_checkpoints=None, name='checkpoints')[source]
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
class GateCheckpoints[source]

Bases: Component

The gates as checkpoints (CheckpointSet.from_gates: a zero-copy view; utility is None). Read target: the producer’s gates (HasGateFrames; generation read when present). Every array of its result aliases the gates.

__init__(*, name='checkpoints')[source]
Parameters:

name (str)

Return type:

None

classmethod __new__(*args, **kwargs)
class Progress[source]

Bases: Component

Ordered progress through checkpoints (wraps ProgressTracker; stateful).

Read target: the result of the component named source (default "checkpoints"; checkpoints= is an alias kept for 0.x) (HasCheckpoints; its valid / generation read when present: invalid envs frozen, self-reset on a new generation). Input: position. step: update -> ProgressEvents; reset: clears the masked envs’ state.

__init__(*, source=None, checkpoints=None, name='progress')[source]
Parameters:
  • source (str | None)

  • checkpoints (str | None)

  • name (str)

Return type:

None

classmethod __new__(*args, **kwargs)
class Localizer[source]

Bases: Component

Track-frame localization (wraps TrackLocalizer; stateful in both modes: the warm memory and the self-reset state are per env).

Read target: the producer’s result, or the result of the component named source (HasCenterline; valid / generation read when present). Input: position. step: query -> TrackFrame; reset: drops the masked envs’ warm memory.

__init__(*, warm_window=None, source=None, name='localizer')[source]
Parameters:
  • warm_window (int | None)

  • source (str | None)

  • name (str)

Return type:

None

classmethod __new__(*args, **kwargs)
class HeightFieldBake[source]

Bases: Component

A per-env road heightfield (wraps HeightFieldBaker).

Read target: the producer’s result (HasBand). on_generate: bake. With size_hi a grid below ceil(reference_resolution * size_hi) texels per side warns at attach; size_hi="auto" (the default) reads the producer’s declared bound (cc.bounds["size_hi"]), None declares none.

__init__(resolution, *, padding=None, size_hi='auto', reference_resolution=128, name='heightfield')[source]
Parameters:
Return type:

None

classmethod __new__(*args, **kwargs)
class CenterlineFromGates[source]

Bases: Component

A 3D centerline through the gates (wraps CourseLine): its result is the Centerline.

Read target: the producer’s gates (HasGates; generation read when present, copied into the line’s own). on_generate: refresh.

__init__(samples_per_gate=8, *, name='centerline')[source]
Parameters:
  • samples_per_gate (int)

  • name (str)

Return type:

None

classmethod __new__(*args, **kwargs)

Collision queries

Box-vs-track out-of-bounds checks with full contact info, and the same box query against disc obstacles (gate posts, physical cones, point props). Collision backends compares the backends (the exact segments scan and the baked sdf grid), their accuracy and their cost.

Cones and walls placed by track_gen.props, spacing comparison, and the collision SDF field with boxes classified by the exact backend.

The query and instancing utilities on one generated track: track_gen.props cones (points mode) and wall pieces (segments mode) on both boundaries, the effect of the spacing knob, and track_gen.collision’s baked SDF field with boxes classified by the exact segments backend (green inside, red out of bounds, dotted lines to the nearest boundary point). Regenerate with python -m viz.render_utility_assets.

Public collision-query API: box-vs-track out-of-bounds checks.

CollisionChecker answers, for batches of oriented boxes against a batch of generated tracks, whether each box has left the drivable band — with full contact info (BoxContact): OOB flag, signed clearance, nearest boundary point, inward normal, and boundary id. It has two Warp backends:

  • method="segments" (default): exact, zero precompute; reads the bound Track buffers directly (fresh after every generate()).

  • method="sdf": bakes per-env signed-distance grids; a query takes the exact minimum of the interpolated field over the box outline, at a cost that grows with the grid cells the outline crosses. Approximate within one grid cell near boundaries; requires bake() after each generate(). Memory ~ E * sdf_resolution**2 * 5 bytes.

DiscChecker is a sibling checker in this module, not a third backend of CollisionChecker: it answers the same oriented-box query against disc obstacles instead of a track (gate posts, physical cones, point props).

This module is the template for future query utilities: each gets its own public sibling module (flat namespace, no grab-bag utils).

class CollisionChecker[source]

Bases: object

Batched box-vs-track out-of-bounds checker bound to a Track.

Because TrackGenerator.generate() overwrites its Track buffers in place, the segments backend always reads the CURRENT track batch with no rebind step. The sdf backend requires bake() after each generate() call.

query() is allocation-free and host-sync-free (CUDA-graph capturable); its launches are stream-ordered, so a consumer on another stream must order itself explicitly. query() returns the same preallocated BoxContact on every call (result).

The source’s valid is read when present (checked at construction like the required members): an invalid env reads as out of bounds with NaN geometry (see the module docstring).

topology = 'closed'

The topologies this component supports (closed loops only in 0.x).

__init__(track, max_boxes, method='segments', sdf_resolution=128, sdf_padding=None, position=None, orientation=None, half_extents=None)[source]

Bind to a Track batch and allocate the result buffers.

Parameters:
  • track (HasBand) – the bound track batch; the segments backend reads its buffers directly on every query() (no rebind needed after generate()).

  • max_boxes (int) – query stride (boxes per env); must be >= 1.

  • method (str) – "segments" (exact, default) or "sdf" (baked, approximate near boundaries; see the module docstring).

  • sdf_resolution (int) – method="sdf" only — grid side length in texels per env (>= 8; default 128). Ignored for "segments".

  • sdf_padding (float | None) – method="sdf" only — per-side AABB padding in world units the grid is expanded by. None (default) selects AUTO padding: 10% of the larger AABB extent (max(width, height)) of each env’s track, computed at bake time. Pass a positive float to override with a fixed padding for every env.

  • position (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

  • orientation (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

  • half_extents (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

Return type:

None

property method: str

The backend, "segments" or "sdf" (read-only).

property result: BoxContact

The preallocated BoxContact query() writes and returns (the same instance for the checker’s lifetime).

bake(stream=None, *, ctx=None)[source]

(Re)bake the per-env SDF grids from the bound Track.

Required after every TrackGenerator.generate() call when method="sdf" (the segments backend needs no rebake). Pure kernel launches — CUDA-graph capturable.

Parameters:
  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the checker’s device to emit on (exclusive with stream=: TypeError).

Raises:

ValueError – if this checker was constructed with method="segments".

Return type:

None

bind_inputs(position, orientation, half_extents)[source]

Bind stable per-step input buffers (validated once, here).

After binding, query() takes no arguments and reads these arrays in place each call — the natural CUDA-graph pattern: the sim writes poses into its stable buffers and replays the captured query. The arrays must keep the same .ptr for the binding’s lifetime: replacing one after query() was recorded into a capture warns (RebindAfterCaptureWarning).

Parameters:
  • position (array)

  • orientation (array)

  • half_extents (array)

Return type:

None

query(position=None, orientation=None, half_extents=None, stream=None, *, ctx=None)[source]

Compute contact info for E * max_boxes oriented boxes.

Two modes:

  • Bound mode: after bind_inputs(), call query() with no arguments; it reads the bound arrays in place each call (the arrays must keep the same .ptr for the binding’s lifetime — write new poses into them rather than rebinding, e.g. under CUDA-graph capture).

  • Per-call mode: if not bound, pass position, orientation and half_extents explicitly on every call.

Parameters:
  • position (array | None) – [E*max_boxes] vec3f box centers (the query projects to xy; OOB semantics stay planar). NaN marks an inactive slot (NaN outputs, oob=0, boundary=-1). Required in per-call mode; omit in bound mode.

  • orientation (array | None) – [E*max_boxes] quatf box orientations (wp.quatf(x, y, z, w); the query uses the yaw about +z). Required in per-call mode; omit in bound mode.

  • half_extents (array | None) – [E*max_boxes] vec2f per-box half sizes (planar boxes). Required in per-call mode; omit in bound mode.

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the checker’s device to emit on (exclusive with stream=: TypeError).

Returns:

The checker’s preallocated BoxContact (same instance every call; buffers overwritten in place).

Raises:

ValueError – on shape/dtype/device mismatch, or on mode misuse (passing arguments while bound, or omitting them while not bound).

Return type:

BoxContact

class BoxContact[source]

Bases: object

Batched box-vs-track contact result, flat [E * max_boxes] per field.

Warning

CollisionChecker.query() returns the SAME BoxContact instance on every call and overwrites its buffers in place. Call clone() for a fully-owned snapshot.

oob

int32 — 1 if the box crosses a boundary or lies outside the band.

Type:

wp.array

distance

float32 signed clearance: positive = margin from the box to the nearest boundary; negative = deepest corner penetration when OOB (0.0 when the box only edge-crosses a boundary with all corners inside). With method="sdf" it is the minimum of the interpolated signed-distance field over the box outline and centre, so an edge crossing a side reads negative too (oob == (distance < 0)). NaN for inactive slots. Cf. DiscContact.depth, which reports an UNSIGNED penetration depth (0.0 = no hit) rather than a signed clearance.

Type:

wp.array

nearest

vec2f nearest point on the boundary polylines to the box.

Type:

wp.array

normal

vec2f boundary normal at nearest, pointing INTO the band.

Type:

wp.array

boundary

int32 — 0 = inner boundary, 1 = outer boundary, -1 = inactive slot, degenerate track (count < 3) or invalid env (valid[e] == 0); with method="sdf" also -1 for a box whose corners are not finite (NaN or inf half-extents, yaw or position; reported oob=1 with NaN geometry).

Type:

wp.array

clone()[source]

Return a deep copy whose Warp buffers do not alias this result.

Return type:

BoxContact

__init__(oob, distance, nearest, normal, boundary)
Parameters:
  • oob (array)

  • distance (array)

  • nearest (array)

  • normal (array)

  • boundary (array)

Return type:

None

classmethod __new__(*args, **kwargs)
class DiscChecker[source]

Bases: object

Batched oriented-box vs disc-obstacle checker (collision family).

See the module docstring. num_envs is required when count is not given (a flat disc array alone cannot determine the env split).

__init__(discs, radius, max_boxes, num_envs=None, count=None, position=None, orientation=None, half_extents=None, *, valid=None)[source]

Bind a flat disc array and allocate the result buffers.

Parameters:
  • discs (array) – [E * D] vec3f disc centers (ALIASED — regenerated buffers are seen automatically). NaN-marked slots are skipped.

  • radius (float) – shared disc radius (> 0); every disc uses the same radius.

  • max_boxes (int) – query stride (boxes per env); must be >= 1.

  • num_envs (int | None) – required when count is not given (a flat disc array alone cannot determine the env split); must match count.shape[0] when both are given.

  • count (array | None) – optional [E] int32 real-disc-count array (aliased, stable user buffer); omit to rely on NaN padding alone.

  • position (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

  • orientation (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

  • half_extents (array | None) – optional constructor binding (all-or-none); equivalent to calling bind_inputs() right after construction.

  • valid (array | None) – optional [E] int32 per-env validity (aliased, stable buffer): an env with valid[e] == 0 has no discs.

Return type:

None

bind_inputs(position, orientation, half_extents)[source]

Bind (or rebind) stable per-step input buffers (validated once).

After binding, query() takes no arguments and reads these arrays in place; same-.ptr rule applies under CUDA-graph capture (replacing one after query() was recorded warns: RebindAfterCaptureWarning).

Parameters:
  • position (array)

  • orientation (array)

  • half_extents (array)

Return type:

None

property result: DiscContact

The preallocated DiscContact query() writes and returns (the same instance for the checker’s lifetime).

query(position=None, orientation=None, half_extents=None, stream=None, *, ctx=None)[source]

Box-vs-disc contact for E * max_boxes boxes.

Bound mode (inputs bound at construction): call with no arguments. Per-call mode: pass all three arrays; under CUDA-graph capture the SAME arrays must be used at capture and every replay.

Parameters:
  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the checker’s device to emit on (exclusive with stream=: TypeError).

  • position (array | None)

  • orientation (array | None)

  • half_extents (array | None)

Return type:

DiscContact

class DiscContact[source]

Bases: object

Batched box-vs-disc result, flat [E * max_boxes] per field.

Warning

DiscChecker.query() returns the SAME instance every call and overwrites its buffers in place; clone() for snapshots.

hit

int32 — 1 iff any disc touches the box (distance <= radius).

Type:

wp.array

disc

int32 — index of the deepest-penetrating disc, -1 when none.

Type:

wp.array

depth

float32 — penetration depth of that disc (>= 0; 0 on graze or no hit). Saturates at radius once the disc center lies inside the box (ties broken by lowest disc index). Cf. BoxContact.distance, which reports a SIGNED clearance (positive margin / negative penetration) rather than an unsigned depth.

Type:

wp.array

nearest

vec2f — point on that disc’s boundary nearest the box (approximate when the disc center lies deep inside the box). NaN when no hit or the box slot is inactive.

Type:

wp.array

clone()[source]

Return a deep copy whose Warp buffers do not alias this result.

Return type:

DiscContact

__init__(hit, disc, depth, nearest)
Parameters:
  • hit (array)

  • disc (array)

  • depth (array)

  • nearest (array)

Return type:

None

classmethod __new__(*args, **kwargs)

Boundary props

Rendering-only instancing poses along track boundaries: cone lines (mode="points") and wall pieces (mode="segments"). Props never collide. An invalid env (Track.valid == 0) gets no props (count = 0), like an env with no track.

Public boundary prop-sampling API: instancing poses along track boundaries.

PropSampler resamples the inner or outer boundary at a set spacing into a PropSet of per-prop poses (position, tangent, yaw, length) for rendering-only instancing — cone lines (mode="points") or wall pieces (mode="segments"). The complement of track_gen.collision: these props never collide; use CollisionChecker for out-of-bounds queries.

class PropSampler[source]

Bases: object

Resample one track boundary into instancing poses at a set spacing.

One sampler binds one Track, one boundary curve, one mode, and one spacing, so all output shapes are fixed and sample() is CUDA-graph capturable (allocation-free and host-sync-free). Because TrackGenerator.generate() overwrites its Track buffers in place, every sample() reads the CURRENT batch — no rebind after regeneration. Props are rendering-only; they are not colliders. An env with valid[e] == 0 gets no props (count = 0, NaN slots: the sentinel of a poisoned env), whatever the source’s on_invalid.

PropSet.truncated/PropSet.step live on the RESULT because PropSampler is the ONLY producer of a PropSet — unlike CheckpointSet, which is also produced zero-copy by CheckpointSet.from_gates (no sampling, so no truncation/step to report); track_gen.checkpoints.CheckpointSampler keeps its analogous diagnostics on the sampler instead, to avoid meaningless fields on gate-derived checkpoint sets.

topology = 'closed'

The topologies this component supports (closed loops only in 0.x).

__init__(track, spacing, boundary='outer', mode='points', max_props=None)[source]

Bind to a Track boundary and allocate the result buffers.

Parameters:
  • track (HasBand) – the bound track batch; sample() reads its buffers directly on every call (no rebind needed after generate()).

  • spacing (float) – target arc-length spacing between props (> 0); snapped per env so each ring closes without a seam (see module docstring).

  • boundary (str) – "inner" or "outer" (default) — which track boundary curve to resample.

  • mode (str) – "points" (default, one pose per sample) or "segments" (one pose per chord between samples).

  • max_props (int | None) – buffer capacity per env; None (default) derives it from the CURRENT batch (see _derive_max_props: a construction-time readback, kept as a supported convenience). Must be >= 3 when given explicitly. The size-independent bound max(3, ceil(max_loop_length / spacing)) from the producer’s plan is the default of the component route: track_gen.tools.Props(spacing) attached through ComponentContext.for_producer(generator).

Return type:

None

property max_props: int

The per-env capacity (slots per env) of the PropSet (read-only).

property result: PropSet

The preallocated PropSet sample() refreshes and returns (the same instance for the sampler’s lifetime).

sample(stream=None, *, ctx=None)[source]

Resample the bound boundary; returns the preallocated PropSet.

Two kernel launches (scan + place); reads the Track buffers as they are NOW, so it reflects the latest generate() with no rebind.

Parameters:
  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the sampler’s device to emit on (exclusive with stream=: TypeError).

Return type:

PropSet

class PropSet[source]

Bases: object

Batched prop poses, flat [E * max_props] per pose field.

Warning

PropSampler.sample() returns the SAME PropSet instance on every call and overwrites its buffers in place. Call clone() for a fully-owned snapshot.

position

vec3f pose positions on the (possibly lifted) boundary: on-curve sample (points mode) or chord midpoint (segments mode); z tracks the boundary elevation (0 on flat tracks). NaN past count[e].

Type:

wp.array

tangent

vec2f unit directions: curve tangent at the sample (points) or chord direction (segments). NaN past count[e].

Type:

wp.array

yaw

float32 heading, atan2(tangent.y, tangent.x).

Type:

wp.array

length

float32 per-prop extent: effective arc step (points mode) or chord length (segments mode). Both are measured along the (possibly lifted) boundary polyline: on a flat track this is a planar distance, and on a non-flat z_profile track it is the true 3D distance (the 3D chord, in segments mode) between samples that may sit at different altitudes.

Type:

wp.array

count

[E] int32 real prop counts (0 for degenerate and invalid envs: valid[e] == 0 on the source).

Type:

wp.array

truncated

[E] int32 — 1 if max_props clipped this env’s ring (the ring still closes, at a coarser effective spacing).

Type:

wp.array

step

[E] float32 effective arc spacing perimeter / count.

Type:

wp.array

clone()[source]

Return a deep copy whose Warp buffers do not alias this set.

Return type:

PropSet

__init__(position, tangent, yaw, length, count, truncated, step)
Parameters:
  • position (array)

  • tangent (array)

  • yaw (array)

  • length (array)

  • count (array)

  • truncated (array)

  • step (array)

Return type:

None

classmethod __new__(*args, **kwargs)

Checkpoints

Ordered course goals from gates (zero-copy) or subsampled track centerlines.

Public checkpoint API: ordered course goals from gates or tracks.

CheckpointSet is the shared contract consumed by track_gen.progress.ProgressTracker. Build one zero-copy from a GateSequence (CheckpointSet.from_gates) or by subsampling a track’s centerline at a coarse spacing (CheckpointSampler — each checkpoint’s crossing segment is the road cross-section, a “virtual gate”).

class CheckpointSampler[source]

Bases: object

Resample a track’s centerline into coarse course checkpoints.

One sampler binds one Track and one spacing; sample() refreshes the owned CheckpointSet in place from the CURRENT track batch (no rebind after generate(); two kernel launches, allocation-free and host-sync-free, so CUDA-graph capturable as-is). Spacing is expected to be coarse — checkpoint counts similar to gate counts; nothing enforces this. Progress state bound to the sampled set must be reset after resampling a regenerated track.

Producer diagnostics live on the sampler (not the set): truncated ([E] int32, 1 if max_checkpoints clipped the ring) and step ([E] float32 effective arc spacing). An env with valid[e] == 0 reads like a poisoned one (count = 0, step NaN, truncated = 0, NaN slots, the set’s valid = 0). They live here rather than on CheckpointSet because the set has a SECOND, sampling-free producer (CheckpointSet.from_gates) that has no truncation or step concept — keeping the diagnostics on the sampler avoids meaningless fields on gate-derived sets (contrast track_gen.props.PropSet, whose only producer is PropSampler, so its diagnostics live on the result).

topology = 'closed'

The topologies this component supports (closed loops only in 0.x).

__init__(track, spacing, max_checkpoints=None)[source]

Bind to a Track centerline and allocate the result buffers.

Parameters:
  • track (HasBand) – the bound track batch; sample() reads its centerline buffer directly on every call (no rebind needed after generate()).

  • spacing (float) – target arc-length spacing between checkpoints (> 0); snapped per env so each ring closes without a seam (same snap rule as track_gen.props).

  • max_checkpoints (int | None) – buffer capacity per env; None (default) derives it from the CURRENT batch (see _derive_max_checkpoints: a construction-time readback, kept as a supported convenience). Must be >= 3 when given explicitly. The size-independent bound max(3, ceil(max_loop_length / spacing)) from the producer’s plan is the default of the component route (track_gen.tools.Checkpoints(spacing) attached through ComponentContext.for_producer(generator)) and of Course.

Return type:

None

The track’s generation is read when present (checked like the required members): sample() copies it (and valid) into the set.

property max_checkpoints: int

The per-env capacity (slots per env) of the CheckpointSet (read-only).

property result: CheckpointSet

The preallocated CheckpointSet sample() refreshes and returns (the same instance for the sampler’s lifetime).

sample(stream=None, *, ctx=None)[source]

Refresh the owned CheckpointSet from the bound Track; returns it.

Parameters:
  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the sampler’s device to emit on (exclusive with stream=: TypeError).

Return type:

CheckpointSet

class CheckpointSet[source]

Bases: object

Ordered per-env checkpoints, flat [E * M] per field.

The consumer contract for track_gen.progress.ProgressTracker. Sets produced by from_gates() ALIAS the gate buffers (mutations to the gates are visible here); sets produced by CheckpointSampler are owned by the sampler and refreshed in place by sample().

position

vec3f checkpoint centers. NaN past count[e].

Type:

wp.array

left

vec3f crossing-segment endpoints (gate left / track inner).

Type:

wp.array

right

vec3f crossing-segment endpoints (gate right / track outer).

Type:

wp.array

tangent

vec3f unit forward (travel) directions. For gate-derived sets this is the gate’s forward (the physical gate-plane normal), not the 3D spline tangent.

Type:

wp.array

up_half

[E * M] float32 vertical half-openings for the crossing test: _BIG (unbounded) for track cross-sections, and the gate’s half_size for gate-derived sets (aliases GateSequence.half_size). NaN past count[e].

Type:

wp.array

count

[E] int32 real checkpoint counts (a sampler’s set: 0 for an invalid env).

Type:

wp.array

valid

[E] int32 validity of the course the rows were built from: owned by CheckpointSampler (written by sample(), so count, the rows, valid and generation always describe the same ring), the gates’ own valid for from_gates(). None on a hand-built set (then every env is treated as valid). ProgressTracker freezes an env whose valid is 0.

Type:

wp.array or None

generation

[E] int32 generation (epoch) of the course the rows were built from: owned by CheckpointSampler (written by sample(), 0 when its track has none), the gates’ own generation for from_gates(). ProgressTracker resets an env’s state when it changes (self-reset). None: never.

Type:

wp.array or None

classmethod from_gates(seq)[source]

Zero-copy view of a GateSequence as checkpoints.

Aliases (does not copy) position, left, right, forward (as tangent: the physical gate-plane normal), half_size (as up_half), count and, when the source has them, valid and generation. seq is any HasGateFrames with those members, checked here (track_gen.results.check_source; valid / generation are checked when present). A ProgressTracker on the set resets an env’s state by itself when its generation changes (a regeneration with advance); reset it explicitly after a reseed (see ProgressTracker).

Parameters:

seq (HasGateFrames)

Return type:

CheckpointSet

clone()[source]

Return a deep copy whose Warp buffers do not alias this set (valid / generation too, when set).

Return type:

CheckpointSet

__init__(position, left, right, tangent, up_half, count, valid=None, generation=None)
Parameters:
  • position (array)

  • left (array)

  • right (array)

  • tangent (array)

  • up_half (array)

  • count (array)

  • valid (array | None)

  • generation (array | None)

Return type:

None

classmethod __new__(*args, **kwargs)

Progress tracking

Stateful per-env course progress over any CheckpointSet.

Public progress-tracking API: ordered course progress + reward signals.

ProgressTracker consumes any CheckpointSet (gate sequences via CheckpointSet.from_gates, or subsampled track centerlines via CheckpointSampler) and emits per-step ProgressEvents: pass events, laps, wrong-way / wrong-checkpoint flags, and dist_to_next — difference it across steps for the classic negative-delta-distance reward.

class ProgressTracker[source]

Bases: object

Track ordered progress of one agent per env through a CheckpointSet.

See the module docstring for semantics. Construct with position= (a stable [E] vec3f wp.array owned by your sim) for bound mode — update() then takes no arguments and reads the buffer in place; or leave unbound and pass positions per call. Mixing modes raises ValueError.

__init__(checkpoints, position=None)[source]

Bind to a CheckpointSet and allocate the tracker’s state.

Parameters:
  • checkpoints (CheckpointSet) – the ordered per-env checkpoint set to track against (from CheckpointSet.from_gates or CheckpointSampler.sample(); any HasCheckpoints). Aliased, not copied — later mutation (e.g. a resampled or regenerated set) is seen on the next update(). Its valid / generation are read when present ([E] int32 on the set’s device, checked here): invalid envs are frozen, and an env resets by itself when its generation changes. The state of that check starts at the set’s generation now (a device copy on the call’s stream: the ambient context or Warp’s current stream), so the first update() does not reset.

  • position (array | None) – optional stable [E] vec3f buffer to bind at construction (bound mode); equivalent to calling bind() right after construction.

Return type:

None

property result: ProgressEvents

The preallocated ProgressEvents update() writes and returns (the same instance for the tracker’s lifetime).

property next_checkpoint_state: array

Aliased read-only view of the [E] int32 current-target state.

Exposes the tracker’s live _next buffer (updated in place by every update()) for consumers that window their queries around the current target checkpoint (e.g. FrameChecker.bind_window). Do NOT mutate — the tracker owns this state.

bind(position)[source]

Bind (or rebind) a stable [E] vec3f position buffer.

After binding, update() takes no arguments and reads the buffer in place. Validation happens here, once; the array must keep the same .ptr for the binding’s lifetime (CUDA-graph contract): replacing it after update() was recorded into a capture warns (RebindAfterCaptureWarning).

Parameters:

position (array)

Return type:

None

update(position=None, stream=None, *, ctx=None)[source]

Advance one step; returns the tracker’s preallocated events.

Bound mode (constructed with position=): call with no arguments. Per-call mode: pass the [E] vec3f position array — the SAME array (identical .ptr) must be used across a CUDA-graph capture and its replays.

NaN-position semantics: a NaN position[e] PAUSES env e for this step rather than erroring — no crossing/wrong-way/wrong-checkpoint event can fire (the crossing test always misses against a NaN endpoint), dist_to_next[e] is NaN, and the NaN gets written into prev_pos[e], which re-arms the same sentinel reset() uses: the FIRST update after a finite position resumes also reports no event (it only re-primes prev_pos), and normal crossing detection resumes the update after that. Useful for envs mid-teleport or awaiting a fresh spawn without a full reset().

Parameters:
  • position (array | None) – [E] vec3f positions (per-call mode only).

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the tracker’s device to emit on (exclusive with stream=: TypeError).

Return type:

ProgressEvents

reset(mask, stream=None, *, ctx=None)[source]

Clear state where mask[e] == 1 ([E] int32); arms the NaN previous-position sentinel so the next update cannot emit a spurious crossing. Required after a reseed or an epoch roll-back of the bound course, and on respawns; after a regeneration that changes an env’s generation the tracker resets it by itself (byte-identical to this call).

Parameters:
  • mask (array) – [E] int32 reset mask.

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the tracker’s device to emit on (exclusive with stream=: TypeError).

Return type:

None

class ProgressEvents[source]

Bases: object

Per-env progress events, all [E]; overwritten in place per update.

Warning

ProgressTracker.update() returns the SAME instance every call. clone() for snapshots.

passed

int32 — 1 iff the target checkpoint was crossed forward this step.

Type:

wp.array

checkpoint_passed

int32 — index of the checkpoint passed this step, -1 otherwise.

Type:

wp.array

next_checkpoint

int32 — current target AFTER any advance this step.

Type:

wp.array

laps

int32 — completed laps.

Type:

wp.array

progress

int32 — total checkpoints passed since construction/reset.

Type:

wp.array

wrong_way

int32 — 1 iff the target was crossed BACKWARD this step.

Type:

wp.array

wrong_checkpoint

int32 — index of a non-target checkpoint crossed this step (either direction; first in index order), -1 otherwise.

Type:

wp.array

dist_to_next

float32 — |position - next checkpoint center| after any advance. NaN for a (valid) env with no checkpoints; 0 for an invalid env (frozen).

Type:

wp.array

clone()[source]

Return a deep copy whose Warp buffers do not alias this result.

Return type:

ProgressEvents

__init__(passed, checkpoint_passed, next_checkpoint, laps, progress, wrong_way, wrong_checkpoint, dist_to_next)
Parameters:
  • passed (array)

  • checkpoint_passed (array)

  • next_checkpoint (array)

  • laps (array)

  • progress (array)

  • wrong_way (array)

  • wrong_checkpoint (array)

  • dist_to_next (array)

Return type:

None

classmethod __new__(*args, **kwargs)

Track-frame localization

Per-env (s, n) centerline projection with an optional warm start, plus the per-generation curvature and speed-profile helpers.

Public track-frame localization API: (s, n) projection + speed hints.

TrackLocalizer projects one point per env onto the bound Track’s centerline and returns a TrackFrame: arc length s, signed lateral offset n (positive to the RIGHT of the direction of travel; which boundary that is depends on the loop’s generator-dependent winding), and the nearest segment index — the Frenet-style frame racing controllers and reward shapers consume every sim step. An optional warm start (warm_window=) narrows each scan to a window around the previous result; reset(mask) drops that memory after regenerations and teleports.

curvature() and speed_profile() are per-generation companions: per-point signed centerline curvature and the curvature-limited target speed (steady-state lateral limit, then forward-acceleration and backward-braking passes over the closed loop).

class TrackLocalizer[source]

Bases: object

Project one point per env onto the bound Track’s centerline.

Because TrackGenerator.generate() overwrites its Track buffers in place, query() always reads the CURRENT track batch — but a warm-started localizer remembers segment indices of the OLD geometry. When the source has generation (a Track, a CourseLine centerline) the localizer drops that memory by itself for every env whose generation changed; otherwise, and after a reseed or an epoch roll-back (which may leave generation unchanged), callers MUST reset() the affected envs (cold-scan localizers, warm_window=None, need nothing).

query() is allocation-free and host-sync-free (CUDA-graph capturable); its launches are stream-ordered, so a consumer on another stream must order itself explicitly. query() returns the same preallocated TrackFrame on every call.

Warm vs cold results are IDENTICAL whenever the nearest centerline segment stays within warm_window segments of the previous query’s result — the usual regime (per-step motion small against warm_window * spacing). Small motion alone does NOT guarantee it: where the loop pinches close to itself, the nearest segment can jump half a lap for arbitrarily small motion, and the warm result then stays on the traveled branch (often preferable for racing consumers — s stays continuous — but not the cold answer). Outside the contract (teleports, huge steps), a warm query may likewise lock onto a local minimum; call reset() on the affected envs to force a full rescan.

topology = 'closed'

The topologies this component supports (closed loops only in 0.x).

__init__(track, warm_window=None, position=None)[source]

Bind to a Track batch and allocate the result buffers.

Parameters:
  • track (HasCenterline) – the bound track batch; query() reads its centerline buffers directly on every call.

  • warm_window (int | None) – None (default) scans all segments every query; an int >= 1 scans only the 2 * warm_window + 1 segments centered on the previous result (per env; the first query after construction or reset() is always a full scan).

  • position (array | None) – optional stable [E] vec3f buffer to bind at construction (bound mode); equivalent to calling bind() right after construction.

Return type:

None

The source’s valid and generation are read when present (checked like the required members): invalid envs get the poisoned-env frame, and an env’s warm memory is dropped when its generation changes (the check starts from the source’s generation now: a device copy on the call’s stream, the ambient context or Warp’s current stream).

bind(position)[source]

Bind (or rebind) a stable [E] vec3f position buffer.

After binding, query() takes no arguments and reads the buffer in place. Validation happens here, once; the array must keep the same .ptr for the binding’s lifetime (CUDA-graph contract): replacing it after query() was recorded into a capture warns (RebindAfterCaptureWarning).

Parameters:

position (array)

Return type:

None

property result: TrackFrame

The preallocated TrackFrame query() writes and returns (the same instance for the localizer’s lifetime).

query(position=None, stream=None, *, ctx=None)[source]

Localize the batch; returns the localizer’s preallocated frame.

Bound mode (constructed with position= or after bind()): call with no arguments. Per-call mode: pass the [E] vec3f position array — the SAME array (identical .ptr) must be used across a CUDA-graph capture and its replays.

NaN position[e] yields NaN s/n and segment = -1 for that env, and drops its warm-start memory (the next finite query does a full scan) — safe for envs mid-teleport or awaiting respawn.

Parameters:
  • position (array | None) – [E] vec3f positions (per-call mode only).

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the localizer’s device to emit on (exclusive with stream=: TypeError).

Returns:

The localizer’s preallocated TrackFrame (same instance every call; buffers overwritten in place).

Raises:

ValueError – on shape/dtype/device mismatch, or on mode misuse (passing a position while bound, or omitting it while not bound).

Return type:

TrackFrame

reset(mask, stream=None, *, ctx=None)[source]

Drop warm-start memory where mask[e] is nonzero ([E] int32).

The next query for those envs performs a full centerline scan. Required, when warm_window is set, after regenerating the bound track without a generation change the localizer can see (a source without generation, a reseed, an epoch roll-back), and for any env that teleports further than the warm window covers. A no-op for cold-scan localizers (but valid to call, so regeneration code need not special-case).

Parameters:
  • mask (array) – [E] int32 reset mask.

  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the localizer’s device to emit on (exclusive with stream=: TypeError).

Return type:

None

class TrackFrame[source]

Bases: object

Per-env track-frame coordinates, all [E]; overwritten per query.

Warning

TrackLocalizer.query() returns the SAME instance every call. clone() for snapshots.

s

float32 — arc length along the centerline at the projected point, in [0, Track.length[e]), same units as the track coordinates. NaN for NaN positions, degenerate tracks (count < 3) and invalid envs (valid[e] == 0 on a source with valid).

Type:

wp.array

n

float32 — signed lateral offset from the centerline: positive to the RIGHT of the centerline’s direction of travel (the direction of increasing s), negative to the left. Whether the right side is the outer or the inner boundary depends on the loop’s winding, which is generator-dependent (CCW loops have outer on the right, CW loops on the left); |n| <= w means on the road either way, where w is Track.half_width interpolated at the foot point between segment and segment + 1 (at constant width, width.constant, the configured half_width). NaN for NaN positions, degenerate tracks and invalid envs.

Type:

wp.array

n_up

float32 — signed vertical offset in the roll-free frame at the foot point; equals position.z for planar tracks. NaN for NaN positions, degenerate tracks and invalid envs.

Type:

wp.array

segment

int32 — index i of the nearest centerline segment (from point i to i + 1, the closing segment being count[e] - 1 -> 0). -1 for NaN positions, degenerate tracks and invalid envs.

Type:

wp.array

clone()[source]

Return a deep copy whose Warp buffers do not alias this result.

Return type:

TrackFrame

__init__(s, n, n_up, segment)
Parameters:
  • s (array)

  • n (array)

  • n_up (array)

  • segment (array)

Return type:

None

classmethod __new__(*args, **kwargs)
curvature(track, window=2, ctx=None, *, out=None, scratch=None)[source]

Per-point signed centerline curvature, flat [E * N_max] float32.

Discrete curvature at point i: the turn angle between the incoming and outgoing centerline edges over the mean incident edge length, atan2(cross(v1, v2), dot(v1, v2)) / max(0.5 * (|v1| + |v2|), eps), then a wrap-aware moving average over 2 * window + 1 points. Units are 1 / track units. The sign follows the loop orientation: positive where the centerline turns counter-clockwise — a CCW circle of radius R gives kappa ~ +1/R everywhere. NaN past count[e], for degenerate envs (count[e] < 3) and, when the source has valid, for invalid envs (valid[e] == 0: the row of a poisoned env).

On a flat track (the default z_profile) v1/v2 are planar edge vectors and this is exactly the familiar 2D signed curvature. On a lifted track (a non-flat z_profile), center carries real per-point altitude, so v1/v2 are full 3D edge vectors, and the formula picks up grade in two ways: the numerator (cross(v1, v2)[2]) is still exactly the PLAN-VIEW cross product (unaffected by z on its own), but the denominator — both dot(v1, v2) inside the atan2 and the ds normalizer — gains a z contribution. Net effect: a genuine plan-view turn taken on a graded section reports a slightly SMALLER magnitude than the same turn taken flat (the added 3D edge length dilutes it); and a perfectly straight-in-plan-view section with a steep enough grade reversal (a crest or dip sharp enough to flip the sign of dot(v1, v2)) can register as if it were a sharp turn even though the centerline never bends in plan view. Mild grade changes (no sign flip in the dot product) still read as zero curvature, same as flat.

Without out this ALLOCATES its result (and a scratch when window > 0): call it after generate(), outside capture regions. With out it writes the caller’s arrays: allocation-free, host-sync-free and recordable into a capture (e.g. once per regeneration, next to the regeneration in a graph), with bytes identical to the allocating form.

Parameters:
  • track (HasCenterline) – the track batch to read the centerline from (any HasCenterline; its valid is read when present).

  • window (int) – moving-average half-width in points (>= 0); 0 disables smoothing. Default 2 (a 5-tap average).

  • ctx – optional track_gen.exec.LaunchContext; default resolves the device’s stream (see track_gen.exec).

  • out (array | None) – optional contiguous [E * N_max] float32 array on the track’s device to write (and return).

  • scratch (array | None) – with out and window > 0: a second contiguous [E * N_max] float32 array (the unsmoothed curvature), not overlapping out; ignored when window == 0.

Returns:

out, or a NEW flat [E * N_max] float32 wp.array of signed curvatures on the track’s device (owned by the caller; not refreshed by later generate() calls).

Raises:

ValueError – on window < 0, an invalid track batch layout (or a malformed valid), an out / scratch of the wrong shape, dtype, device or layout, a missing scratch (out with window > 0), or a scratch overlapping out.

Return type:

array

speed_profile(track, a_lat_max, a_accel, a_brake, v_cap, kappa=None, window=2, ctx=None, *, out=None)[source]

Curvature-limited target speed per centerline point, [E * N_max].

Three stages, batched per env (one Warp thread per env; the passes are sequential by nature): the steady-state limit min(sqrt(a_lat_max / |kappa|), v_cap), a forward pass capping acceleration out of corners (v[i+1]^2 <= v[i]^2 + 2 * a_accel * ds), and a backward pass capping braking into them (v[i]^2 <= v[i+1]^2 + 2 * a_brake * ds) — each run twice around the closed loop so the wrap converges. Speeds are in track units per second when the accelerations are in track units per second squared.

ds (from Track.arclen/Track.length) is the true 3D segment length on a lifted (non-flat z_profile) track, so the accel/brake ramps size their distance budget off the actual climbing/descending path length, not its plan-view projection. The corner-speed limit still comes from kappa (curvature()’s default, if not passed in), which on a lifted track can be perturbed by grade as documented there — this function adds no further elevation-specific handling of its own.

Without out this ALLOCATES its result (and the curvature when kappa is not given): call it after generate(), outside capture regions. With out (kappa required, e.g. from curvature(track, out=k, scratch=s)) it writes the caller’s array: allocation-free, host-sync-free and recordable, with bytes identical to the allocating form.

Parameters:
  • track (HasCenterline) – the track batch; segment lengths come from Track.arclen and Track.length (any HasCenterline; its valid is read when present).

  • a_lat_max (float) – maximum lateral acceleration (> 0).

  • a_accel (float) – maximum forward acceleration (>= 0; 0 pins the profile to each point’s corner speed going forward).

  • a_brake (float) – maximum braking deceleration (>= 0).

  • v_cap (float) – straight-line speed cap (> 0).

  • kappa (array | None) – optional precomputed flat [E * N_max] float32 curvature (e.g. from curvature(), possibly reused across several profiles). None (default) computes it internally (not with out).

  • window (int) – smoothing half-width forwarded to curvature() when kappa is None; ignored otherwise.

  • ctx – optional track_gen.exec.LaunchContext; default resolves the device’s stream (see track_gen.exec).

  • out (array | None) – optional contiguous [E * N_max] float32 array on the track’s device to write (and return); must not overlap kappa.

Returns:

out, or a NEW flat [E * N_max] float32 wp.array of target speeds on the track’s device, NaN past count[e], for degenerate envs (count[e] < 3) and, when the source has valid, for invalid envs.

Raises:

ValueError – on a non-positive a_lat_max / v_cap, a negative a_accel / a_brake, a kappa shape/dtype/device mismatch, an invalid track batch layout (or a malformed valid), an out of the wrong shape, dtype, device or layout, out without kappa, or out overlapping kappa.

Return type:

array

Heightfields

A per-env road-surface height grid, baked after every generation.

Public heightfield API: per-env road height grids for external physics solvers.

HeightFieldBaker bakes each env’s road surface (from a bound Track batch) into a square HeightField height grid, for consumers that want a grid rather than a polyline. Normally driven indirectly through track_gen.course.Course (set CourseConfig.heightfield_resolution, track mode only) which builds and re-bakes it on every generate() and exposes it as course.heightfield.

class HeightField[source]

Bases: object

Baked per-env height grid; overwritten in place by bake().

heightwp.array

[E * res * res] float32 road-surface heights (NaN for invalid envs). Reshape via .numpy().reshape(E, res, res) (row-major: [e, y, x]).

lo, hiwp.array

[E] vec2f world AABB corners; texel (x, y) center is at lo + ((x, y) + 0.5) / res * (hi - lo). (-1, -1) / (1, 1) for degenerate and invalid envs.

resint

Grid resolution per side.

height: array
lo: array
hi: array
res: int
clone()[source]

A deep copy: new height, lo and hi arrays with the same values (a snapshot that the next bake() does not overwrite).

Return type:

HeightField

__init__(height, lo, hi, res)
Parameters:
  • height (array)

  • lo (array)

  • hi (array)

  • res (int)

Return type:

None

class HeightFieldBaker[source]

Bases: object

Bake the bound track batch into a per-env heightfield.

Lifecycle mirrors the SDF baker: construct once (allocates), bake() after every regeneration (kernel launches only; the facade does this in its captured refresh when CourseConfig.heightfield_resolution is set). An env with valid[e] == 0 bakes NaN heights with the box (-1, -1)-(1, 1): the bake of a poisoned env.

topology = 'closed'

The topologies this component supports (closed loops only in 0.x).

__init__(track, resolution, padding=None)[source]
Parameters:
Return type:

None

property result: HeightField

The preallocated HeightField bake() refreshes and returns (the same instance for the baker’s lifetime).

bake(stream=None, *, ctx=None)[source]

Re-bake from the CURRENT track batch; returns the same instance.

Parameters:
  • stream – optional wp.Stream (or ‘torch’) to run on; by default the stream is resolved per track_gen.exec (an enclosing torch.cuda.graph capture is adopted, else Warp’s current stream).

  • ctx – optional LaunchContext on the baker’s device to emit on (exclusive with stream=: TypeError).

Return type:

HeightField