Upgrading from 0.1 to 0.2¶
This page is for code written against TrackGen 0.1 (the v0.1.0 release). It answers
three questions in order: will my code still run, will my results change without an
error, and do I have to retrain or rebaseline anything.
Most 0.1 code runs unchanged. TrackGenConfig, GateGenConfig, TrackGenerator,
GateGenerator and Course(CourseConfig(...)) keep working through 0.x, and every
default generator except repulsive produces the same bytes as in 0.1. What needs your
attention is a short list of changes that alter results without raising, led by the way
invalid envs now read. The 0.1 documentation is in the
v0.1.0 documentation sources,
and the changelog
has the full details per release. Each item below is self-contained.
The examples on this page share these imports; E is the number of envs in a batch:
import numpy as np
import warp as wp
from track_gen import (GateGenConfig, GateGenerator, PerEnvSeededRNG,
TrackGenConfig, TrackGenerator)
from track_gen.course import Course, CourseConfig
from track_gen.pipeline import legacy, presets
E, device = 64, "cuda:0" # or "cpu"
How to upgrade¶
Raise your Warp pin to
warp-lang>=1.18,<2. On a GPU, Warp’s PyPI wheels need an NVIDIA driver R580 or newer and an sm_75 or newer GPU; thecpudevice runs anywhere. Skipping masked work on the device needs CUDA 12.4 or newer (driver and toolkit). Warp 1.18 or newer and Installation have the details.Run your tests and scripts with
python -W error::DeprecationWarning(or at least-W default::DeprecationWarning). The library attributes its deprecation warnings to the line of your code that triggers them, and Python hides aDeprecationWarningattributed to any module other than__main__, so a gym environment or a training package never shows them by default. Fix each one with the items below.Search your code for the patterns in the “Find it” column of the triage table, starting with the rows marked SILENT.
Gate every reward, observation and termination on the valid flag (The recipe on the host), and measure your invalid fraction right after construction:
(course.result.valid.numpy() == 0).mean(), or(track.valid.numpy() == 0).mean()on a generator’s result. Expect 0 to 3 % for tracks at the settings of the 0.1 examples, and up to 94 % for gates at the 0.1 quickstart’sgate_width=0.4.Rebaseline stored datasets and evaluation sets built with
repulsive, with thin tracks that 0.1 truncated, or with wide gate bars, and retrain policies whose rewards or terminations read thesdfcollision query.Measure memory and throughput again: construction now runs a generation, a course holds more checkpoint slots by default, and every generator keeps a copy of its result (Memory and construction cost).
Triage: what changed and who is affected¶
Rows are sorted by symptom: SILENT (results change and nothing raises or warns) first, then errors, warnings, and changes that need no action.
Symptom |
Change |
Who is affected |
Find it in your code |
What to do |
|---|---|---|---|---|
SILENT |
Gates at the 0.1 quickstart’s |
gate courses and gate datasets |
|
narrower bars or a larger |
SILENT |
Every runtime utility reads an invalid env as empty: |
every |
|
gate on |
SILENT |
Tracks truncated at |
track configs with |
|
raise |
SILENT |
The generator copies its config: changing the config object after construction is ignored |
config-mutation curricula |
|
set dynamic fields, rebuild for static ones: The generator copies its config |
SILENT |
The |
|
|
retrain: The sdf collision query |
SILENT |
|
|
|
regenerate datasets: The repulsive generator’s output |
SILENT |
The default checkpoint capacity comes from the longest possible track (70 at the defaults instead of 15 to 23; the same or fewer for thin roads), so checkpoint arrays change shape |
fixed-width observations |
|
pass |
SILENT |
No host sync after library calls |
consumers on another stream, benchmarks |
|
synchronize explicitly: No host sync after library calls |
SILENT |
Library calls inside |
torch CUDA graph users |
|
|
SILENT |
The list of available generators includes |
per-run generator sampling |
|
filter on |
SILENT |
|
direct |
|
|
SILENT |
More memory, and construction costs one generation (about two per |
large |
|
|
error |
Warp 1.18 or newer |
everyone |
|
|
error |
Constructing a generator or a course inside a CUDA graph capture raises; the rng must be on the generator’s device |
capture and multi-device users |
|
construct first: Construction and generate() |
error |
Newly rejected knob values ( |
tuned configs |
|
|
error |
New result fields (mappings, a |
converters and loggers |
|
|
error |
Utilities: |
direct utility users |
|
|
error / SILENT |
|
|
|
|
error |
Removed private kernels and |
0.1 plugin authors |
|
port to |
error / warning |
|
plugin authors |
|
|
warning |
|
capture users |
|
delete the calls: set_capturing |
warning |
|
|
|
|
warning |
Built-in generators are stages ( |
plugin authors |
|
|
none |
Flat config fields keep working; |
everyone |
|
optional: Flat config, Params and presets |
none |
|
|
|
drop that call: Course presets |
Silent changes¶
These change what your code computes without raising or warning. Read every one that matches your code.
Gate courses at wide bar widths¶
- Type:
Silent
- Affects:
gate courses and gate datasets built with wide bars, such as the 0.1 quickstart’s
gate_width=0.4- Find it:
gate_width=0.4,gate_width = 0.4, anygate_widthabove 0.1
Every gate pipeline checks each env’s bars for crossings, on 0.1 as on 0.2. With the 0.1
quickstart’s settings (bezier, gate_width=0.4, scale=1) about 94 % of a batch
fails that check: 60 of 64 envs, measured on both versions. The fraction is not new; what
happens next is. 0.1 kept tracking progress and post hits on those failed courses. 0.2
treats each one as an invalid env (next item): its
progress is frozen, its distance to the next gate reads 0 and it hits no post, so it is a
slot your agents cannot train on until it is regenerated.
Use bars of 0.1 or narrower, or a larger scale (the gates spread out, so wider bars
fit), and check the fraction after construction. Bar width and invalid envs lists
the measured fraction per generator, bar width and scale. That table uses the default
ccw ordering; with the 0.1 quickstart’s gate_ordering="random_pairs" the
fractions are higher (12.5 % at 0.1 for bezier, 94 % at 0.4, as measured below), so
use 0.05 there (no invalid env in the same batch) or measure your own setting.
The 0.1 quickstart still runs, with no warning:
config = GateGenConfig(generator="bezier", gate_ordering="random_pairs", num_envs=E,
max_gates=32, gate_width=0.4, gate_radius=0.025,
gate_solve_iters=8, device=device)
rng = PerEnvSeededRNG(seeds=0, num_envs=E, device=device)
gates = GateGenerator(config, rng).generate()
print((gates.valid.numpy() == 0).mean()) # 0.94: 60 of 64 envs are invalid
With narrower bars:
config = GateGenConfig(generator="bezier", gate_ordering="random_pairs", num_envs=E,
max_gates=32, gate_width=0.1, gate_radius=0.025,
gate_solve_iters=8, device=device)
gates = GateGenerator(config, rng).generate()
print((gates.valid.numpy() == 0).mean()) # 0.125: 8 of 64
Two clarifications, with the same bytes as 0.1: gate_width is the full bar width (the
0.1 docs called it a half-width; the code always used the full width), and the default
gate_ordering is now None, which resolves to "ccw" for every built-in
generator (a gate generator that accepts only "raw" now builds without naming it).
Invalid envs read as poisoned¶
- Type:
Silent
- Affects:
every
Courseuser, and every direct use of the runtime utilities on a result that has invalid envs- Find it:
.oob,dist_to_next,isnan(,res.contacts,res.events,course.reset(
An env is invalid when its result has valid[e] == 0: the track or the gates failed
one of the generator’s checks (truncated, self-crossing, too thin, crossing bars). Its
fail bits say which. On 0.1 every utility kept computing on the failed geometry.
On 0.2 every utility writes, for an invalid env, what it writes for an env with no track
at all. TrackGenConfig and GateGenConfig keep the failed geometry in the result
(as 0.1 did), so every config-built course, and every utility you run on a config-built
generator’s result, sees this change.
Field |
0.1 |
0.2 |
|---|---|---|
|
the answer on the failed geometry (usually |
|
The SDF bake ( |
the failed track’s field and box |
NaN texels, boundary id |
|
hits on the invalid gates’ posts |
|
|
progress tracked on the failed course, finite |
frozen: |
A |
the projection on the failed centerline |
NaN |
|
the failed track’s checkpoints |
|
|
NaN heights, the failed track’s box |
NaN heights, box |
|
the failed track’s props |
|
|
values |
NaN rows |
Unchanged from 0.1: frame contacts in gates mode (FrameContact: hit = 0,
depth = 0), the course line (count = 0, NaN), so res.frame in gates mode
(NaN s / n / n_up, segment = -1), and CheckpointSet.from_gates (a
view of the gates). Each row was measured on both versions with the same
seeds. When an env becomes valid again, its progress resumes without a
spurious checkpoint crossing.
valid is the signal: StepResult.valid and CheckpointSet.valid are new.
dist_to_next reads 0 for an invalid env (NaN only for a valid env of a
hand-built checkpoint set without checkpoints), so code that detected a broken env with
isnan(dist_to_next) must read valid instead.
What breaks in an RL loop: a loop that penalises oob, or ends episodes on it, without
gating on valid penalises or ends every invalid env at every step;
course.reset(done) respawns them on the same invalid course forever; and a NaN in one
env’s observation makes a batch mean NaN. Gate every reward, observation and termination
on valid with a select (never reward * valid: NaN * 0 is NaN), and give the
invalid envs a new course with a masked regeneration, new in 0.2
(The recipe on the host). A host version of the gating:
config = TrackGenConfig(num_envs=E, half_width=0.03, device=device) # the 0.1 README
course = Course(CourseConfig(mode="track", gen=config, seeds=0, checkpoint_spacing=0.6,
collision="segments"))
print((course.result.valid.numpy() == 0).mean()) # the invalid fraction
# your sim's car boxes, one per env
position = wp.zeros(E, dtype=wp.vec3f, device=device)
orientation = wp.full(E, wp.quatf(0.0, 0.0, 0.0, 1.0), dtype=wp.quatf, device=device)
half_extents = wp.full(E, wp.vec2f(0.02, 0.01), dtype=wp.vec2f, device=device)
course.bind(position, orientation, half_extents)
res = course.step()
valid = res.valid.numpy() != 0 # read it before any regeneration
oob = res.contacts.oob.numpy() != 0
reward = np.where(valid, np.where(oob, -1.0, 0.1), 0.0)
done = oob | ~valid
Combined with truncation, the settings of the 0.1 README
now leave about 3 % of the envs invalid, and each of them reads oob = 1 with frozen
progress at every step. Invalid envs explains what valid == 0 means and how to
lower the fraction; Invalid envs lists what every utility reads.
Truncated tracks are invalid¶
- Type:
Silent
- Affects:
track configs whose tracks need more than
N_maxpoints, typically ahalf_widthbelow the default 0.1 with the defaultN_max = 384- Find it:
half_width=below 0.1 (for examplehalf_width=0.03),N_max,n_max
A track has one point every spacing along its length (spacing defaults to
0.6 * half_width), up to N_max points. On 0.1 a track longer than that was cut
at N_max and still shipped with valid = 1. On 0.2 it fails its check:
track.truncated[e] is 1 and valid[e] is 0. The 0.1 README’s
TrackGenConfig(num_envs=E, half_width=0.03) gave 0 % invalid envs on 0.1 and gives
about 3 % on 0.2 (8 of 256), every one of them truncated. At the default
half_width=0.1 nothing changes.
Size N_max >= max(perimeter) / spacing + 1; at half_width=0.03, N_max=512
is enough. Size: the sizing rule has the sizing rule for larger courses.
rng = PerEnvSeededRNG(seeds=0, num_envs=E, device=device)
config = TrackGenConfig(num_envs=E, half_width=0.03, device=device) # the 0.1 README
track = TrackGenerator(config, rng).generate()
print(track.truncated.numpy().sum()) # 1 of 64 envs: invalid on 0.2
config = TrackGenConfig(num_envs=E, half_width=0.03, N_max=512, device=device)
track = TrackGenerator(config, rng).generate()
print(track.truncated.numpy().sum()) # 0
With a preset, the same setting is presets.track(..., half_width=0.03,
n_max=512).
The generator copies its config¶
- Type:
Silent
- Affects:
curricula and sweeps that change a config object after building the generator
- Find it:
cfg.<field> =orconfig.<field> =afterTrackGenerator(/GateGenerator(/Course(
TrackGenerator and GateGenerator copy their config at construction. On 0.1, where
the pipeline ran eagerly (on cpu), changing a field of the config object between two
generate() calls changed the next batch; on 0.2 it changes nothing, and no warning
says so:
config = TrackGenConfig(num_envs=E, half_width=0.05, device=device)
gen = TrackGenerator(config)
before = gen.generate().outer.numpy().copy()
config.half_width = 0.1 # 0.1: the next batch was wider
after = gen.generate().outer.numpy() # 0.2: the same batch
Change a dynamic field through gen.params (its value is read on the device at
every generation), draw it per env with randomize= (Domain randomization and curricula), and build a
new generator to change a static field such as half_width (Parameters):
gen.params["generator"].set("rad", 0.3) # dynamic: applies from the next generation
track = gen.generate()
import dataclasses
# static: build a new generator (spacing=None: derive it again from half_width)
gen = TrackGenerator(dataclasses.replace(config, half_width=0.1, spacing=None))
gen.config returns a copy of the config the generator was built with. spacing
defaults to 0.6 * half_width when the config is built, so a dataclasses.replace
keeps the old value unless you reset it to None: without spacing=None above, the
new generator would place points every 0.03 instead of 0.06, twice as many per track,
and truncate long tracks sooner (Truncated tracks are invalid).
The sdf collision query¶
- Type:
Silent
- Affects:
CollisionChecker(..., method="sdf")andCourseConfig(collision="sdf")users, and policies trained on them- Find it:
"sdf",method="sdf",collision="sdf"
The sdf box query now takes the exact minimum of the interpolated distance field over
the box outline. 0.1 took the minimum of five samples (the centre and the four corners),
so it missed an edge that crosses a box side while the four corners stay on the road,
typically the inner edge at a tight bend: a car box reaching 10 mm into an inner bend read
+16 mm. oob and distance therefore change near the edges (distance never
increases), and nearest and normal come from the point where the minimum is
reached. On the racing near-edge test set (100 worlds of 1024 car boxes, resolution 256),
agreement with the exact segments backend goes from 99.728 % to 99.949 %: misses drop
from 259 to 32, and the 20 false positives are unchanged. When the change was made, a
query cost two to three times as much per CUDA-graph replay; Cost has
the current measurements. A box whose corners are not finite now reads oob = 1 with
NaN distance (0.1 reported the grid’s first texel).
Retrain, or at least re-evaluate, policies whose rewards or terminations read oob or
distance from the sdf backend. The segments backend is unchanged.
Collision backends compares the two.
The repulsive generator’s output¶
- Type:
Silent
- Affects:
datasets, evaluation sets and fingerprints built with
generator="repulsive"- Find it:
"repulsive"
repulsive can now be recorded in a CUDA graph. Each env stops changing once it
converges, independently of the rest of the batch (0.1 ran a host-driven loop with one
early exit for the whole batch). Envs that converge before the loop ends keep the shape
they had at that point and no longer get the later periodic resamples, so their tracks
change; envs that never converge are byte-identical, and each env now depends only on
its own seed, not on the rest of the batch. Every other default generator gives the same
bytes as 0.1.
Cost: a generation on cuda outside the generator’s own CUDA graph runs the full
iteration budget; on cpu an eager generation stops at the first check that finds
every env converged (with the defaults, no earlier than iteration 144 of 269); the
generator’s own graph, and a capture with conditional nodes,
skip the finished windows on the device.
Regenerate stored repulsive datasets and evaluation sets.
The default checkpoint capacity¶
- Type:
Silent
- Affects:
track courses with the default
CourseConfig.max_checkpoints=None, and code that sizes observations from the checkpoint arrays- Find it:
max_checkpoints,course.checkpoints.,.shape[0] // E
0.1 sized a track course’s checkpoint arrays from the batch of the first generate():
ceil(1.5 * longest valid centerline / checkpoint_spacing) slots per env, 15 to 23 at
E = 64 and checkpoint_spacing=0.5 depending on the generator. 0.2 sizes them from
the longest track the generator can produce: max(3, ceil(1.5 * N_max * spacing /
checkpoint_spacing)) (plus the altitude range of an elevated track), which no longer
depends on the seeds or the batch. At the TrackGenConfig defaults that is 70 slots at
checkpoint_spacing=0.5, 58 at 0.6, 116 at 0.3 and 35 at 1.0.
Every
course.checkpointsarray changes to[E * M]: usually larger (70 slots instead of 21 for the defaultbezieratcheckpoint_spacing=0.5), but the same or smaller for thin roads with the defaultN_max(18 slots, where 0.1 gave 18 or 19 depending on the seeds, at the 0.1 README’shalf_width=0.03andcheckpoint_spacing=0.6). Each slot takes 52 bytes per env, plus 8 bytes per env for the newvalidandgeneration: 28.5 MiB atE = 8192,M = 70.An env whose checkpoints fit the old capacity gets the same rows, count, progress and laps. An env the old capacity cut short (
course.checkpoint_sampler.truncated == 1) now gets every checkpoint, so its progress and laps change.When the generator declares no bound (an elevated
TrackGenConfig, whosez_min/z_maxdeclare no range), the course reads the construction batch back, as 0.1 read the first batch.A defaulted capacity that cuts a valid env of the construction batch short emits one
UserWarningat construction. A course whose construction batch has no valid env no longer raises at flat elevation.CheckpointSampler(track, spacing)andPropSampler(track, spacing)built directly keep 0.1’s sizing (they read the bound batch once at construction).
To keep 0.1’s shape, pass the old capacity explicitly (print
course.checkpoints.position.shape[0] // E under 0.1):
config = TrackGenConfig(num_envs=E, device=device)
course = Course(CourseConfig(mode="track", gen=config, checkpoint_spacing=0.5))
print(course.checkpoint_sampler.max_checkpoints) # 70
course = Course(CourseConfig(mode="track", gen=config, checkpoint_spacing=0.5,
max_checkpoints=21)) # the shape 0.1 gave this batch
print(course.checkpoints.position.shape[0] // E) # 21
No host sync after library calls¶
- Type:
Silent
- Affects:
code that reads library outputs from another CUDA stream, and benchmarks
- Find it:
torch.cuda.Stream,wp.Stream,stream=,time.perf_counter()around library calls
On cuda, 0.1 called a device-wide wp.synchronize() after every utility call
(query(), update(), reset(), sample(), bake(), refresh()),
Course.step() / reset() and every generate(). 0.2 only enqueues the work on
a stream, and generate() waits only for the stream it ran on.
Unchanged: work on the same stream,
wp.array.numpy()(a blocking copy), and torch on its legacy default stream see finished results.A consumer on another stream (a torch side stream, a simulator’s own stream) must order itself after the library’s work:
wp.synchronize_device(), or a recordedwp.Eventit waits on.A benchmark must synchronize before reading its timer.
TRACKGEN_DEBUG_SYNC=1makes every library call block on its stream again, so an asynchronous CUDA error surfaces at the call that caused it (Environment variables).
Library calls inside a torch CUDA graph¶
- Type:
Silent
- Affects:
code that records TrackGen calls into a CUDA graph, with torch or with Warp
- Find it:
torch.cuda.graph,CUDAGraph,ScopedCapture,capture_begin,set_capturing
On 0.1 the utilities and Course.step() / reset() could be recorded into a
wp.ScopedCapture only after track_gen.set_capturing(True), which turned off their
host syncs. Inside torch.cuda.graph(g), every library call ran eagerly on Warp’s
stream at capture time and was missing from g. On 0.2 every generator, course and
utility call records into your graph with no flag: delete the set_capturing calls
(set_capturing). A 0.1 loop that recorded into a wp.ScopedCapture
under set_capturing(True) keeps working (the call only warns). What else changes:
Calls made inside
torch.cuda.graph(g)are recorded intog: a replay runs them, and nothing runs at capture time.Under torch’s capturing stream, the device-side skip of a masked regeneration (new in 0.2) is recorded unconditionally, so it costs a full generation at every replay, unless you opt in with
track_gen.interop.torch.registered(conditional=True)ortrack_gen.exec.conditional().A graph that contains conditional nodes (
repulsive, a masked regeneration) cannot be used as a CUDA child graph: record it insidetrack_gen.exec.unconditional().step()andreset()recorded through a Warp-registered capture keep the course alive as long as the graph; a plaintorch.cuda.graphneedstrack_gen.interop.torch.retain(g, course).course.generate(seeds=k)with anintrecords that value into the graph: every replay reseeds withk. Pass awp.arrayand rewrite it in place to vary the seeds between replays.
Rules for recording into your graph has the rules for recording into your graph, and Troubleshooting capture the errors and warnings and what to do about each.
available() lists external¶
- Type:
Silent
- Affects:
code that samples a generator name per run (domain randomization, sweeps)
- Find it:
available(,GENERATORS
registry.available("generator"), and the private
track_gen._src.generator_registry.available() that 0.1 code used, include
external, a generator that ingests centerlines you provide and draws nothing from the
seed: sampled from that list, it gives the same placeholder circles for every
seed. Keep the seed-dependent generators only:
from track_gen.plugin import registry
names = [n for n in registry.available("generator")
if registry.describe("generator", n).seeded]
print(names) # ['bezier', 'checkpoint', 'hull', 'polar', 'repulsive', 'voronoi']
Seeds and epochs¶
- Type:
Silent
- Affects:
code that reseeds a
PerEnvSeededRNGor samples from it directly- Find it:
set_seeds_warp,ids=,PerEnvSeededRNG(
rng.set_seeds_warp(seeds, None)followed by a sample with explicitids: on 0.1 the envs the sample did not select went back to their states from before the reseed; on 0.2 they follow the reseed, so their later draws differ. Generator outputs do not depend on these states and are unaffected.TrackGenerator(config)andGateGenerator(config)work without an rng: envethen gets seede, as withPerEnvSeededRNG(seeds=0, ...)(0.1 required one).The rng must be on the generator’s device (Construction and generate()).
New in 0.2:
generate(advance=True)gives each env a new course by advancing its epoch. A reseed through the rng keeps the epochs, so after an advance it does not reproduce a fresh generator: callgen.seeds.reset_epoch()as well.course.generate(seeds=...)does both. Seeds, epochs and the generation counter explains seeds and epochs.
config = TrackGenConfig(num_envs=E, device=device)
gen = TrackGenerator(config, PerEnvSeededRNG(seeds=0, num_envs=E, device=device))
gen.generate(advance=True) # every env moves to its next course
new_seeds = wp.array(np.arange(100, 100 + E, dtype=np.int32), device=device)
gen.rng.set_seeds_warp(new_seeds, None)
gen.seeds.reset_epoch() # without it: the seeds' epoch-1 courses
track = gen.generate() # what a fresh generator with new_seeds gives
Memory and construction cost¶
- Type:
Silent
- Affects:
large batches, memory budgets, start-up time
- Find it:
num_envs=,N_max=
Every generator holds a copy of its result, so it can restore the envs a masked regeneration leaves alone: 68 bytes per point plus 24 bytes per env, about 107 MB at
E = 4096,N_max = 384.The pipeline keeps some working buffers apart:
E * (2 * N_max + 1)floats for the final resample,E * (N_max + 1)more for an elevated track.Checkpoint arrays change shape with the default capacity, usually growing (The default checkpoint capacity).
Construction runs one generation of each generator (so its result holds a batch right away), a full iteration budget for
repulsive; aCourseruns about two at construction. 0.1 deferred that work to the firstgenerate().
Breaking changes¶
These raise an error, or change the result of code that relied on an internal detail.
Warp 1.18 or newer¶
- Type:
Breaking
- Affects:
everyone
- Find it:
warp-langinpyproject.toml,requirements*.txt, lock files and container images
0.2 requires warp-lang>=1.18,<2 (0.1 accepted warp-lang>=1.14), so a pin below
1.18 no longer resolves. Warp 1.18’s PyPI wheels need an NVIDIA driver R580 or newer and
an sm_75 or newer GPU for the cuda device; the cpu device runs anywhere. The
device-side skip of masked work needs CUDA 12.4 or newer, driver and toolkit. To check an
environment:
import warp as wp
import track_gen
print(track_gen.__version__, wp.__version__) # 0.2.x and 1.18 or newer
print(wp.is_cuda_available() and wp.is_conditional_graph_supported())
Construction and generate()¶
- Type:
Breaking
- Affects:
code that builds generators or courses inside a CUDA graph capture, that times construction, or that gives a generator an rng on another device
- Find it:
ScopedCapture,capture_begin,torch.cuda.graph,PerEnvSeededRNG(
On 0.1, construction was cheap and the first generate() ran three warm-ups, recorded
the generator’s own CUDA graph and blocked with a device-wide wp.synchronize(). On
0.2, construction runs one generation eagerly (it loads every kernel and leaves the batch
for the current seeds in the result) and blocks until that generation has finished on
the generator’s stream; the first generate() on cuda then records the
generator’s own graph and launches it. The same code still runs:
config = TrackGenConfig(num_envs=E, device=device)
rng = PerEnvSeededRNG(seeds=0, num_envs=E, device=device)
gen = TrackGenerator(config, rng) # 0.1: cheap; 0.2: one generation, then waits for it
track = gen.generate() # 0.1: warm-up, graph, device-wide sync
generate() now waits only for its own stream, and two calls never wait:
track = gen.generate() # waits for its own stream only
track = gen.generate(sync=False) # returns at once; results are stream-ordered
gen.enqueue() # never waits
Constructing a generator or a course inside a capture raises CaptureError before it
allocates anything (construction allocates and runs a real generation). Construct it
first, then record your graph:
with wp.ScopedCapture(device=device):
TrackGenerator(config, rng) # raises CaptureError: construct before capturing
The rng must be on the generator’s device: a PerEnvSeededRNG on another device
(a cuda rng for a cpu generator, which 0.1 accepted) raises ValueError at
construction. Calling a generator describes the calls and their streams.
Newly rejected values¶
- Type:
Breaking
- Affects:
tuned or swept configs with values outside the documented ranges
- Find it:
rad=,handle_clamp_frac=,hull_displacement=,polar_radial_jitter=,polar_angular_jitter=,voronoi_radial_variation=,voronoi_angular_jitter=,checkpoint_angle_jitter=,checkpoint_turn_rate=
Building a generator now raises ValueError for values 0.1 accepted:
radoutside[0, 1],handle_clamp_fracbelow 0,hull_displacementoutside[0, 1];polar_radial_jitteroutside[0, 0.85]andpolar_angular_jitteroutside[0, 0.45]. 0.1 clamped them to these ranges, so set the bound itself for exactly the old output (polar_radial_jitter=0.85for1.0); the error says so;voronoi_radial_variationabove 0.85 (no effect beyond it),voronoi_angular_jitterabove pi,checkpoint_angle_jitteroutside[0, 1],checkpoint_turn_rateoutside[0, pi];a
boolfor any of these knobs (rad=Truewas accepted).
TrackGenerator(TrackGenConfig(generator="polar", polar_radial_jitter=1.0,
num_envs=E, device=device))
# ValueError: polar_radial_jitter must be in [0, 0.85], got 1.0 (...: set
# polar_radial_jitter=0.85 for the same output)
gen = TrackGenerator(TrackGenConfig(generator="polar", polar_radial_jitter=0.85,
num_envs=E, device=device)) # 0.1's output for 1.0
A value that converts to a float is still accepted (a numeric string from YAML, a 0-d array, a torch scalar). Built-in generator parameters lists every knob’s range.
New result fields¶
- Type:
Breaking
- Affects:
converters, loggers and serializers that iterate over the fields of a result
- Find it:
dataclasses.fields(,asdict(,astuple(,vars(res),**res
The result dataclasses gain fields, after the 0.1 ones (positional construction is unchanged):
Class |
New fields |
|---|---|
|
|
|
|
|
|
|
|
Code that converts every field with .numpy(), recurses into every StepResult
field as a dataclass, or reshapes every CheckpointSet field to (E, M, ...) now
fails or mixes shapes. Keep the arrays, skip None, and reshape by length:
import dataclasses
track = TrackGenerator(TrackGenConfig(num_envs=E, device=device)).generate()
arrays = {f.name: getattr(track, f.name) for f in dataclasses.fields(track)
if isinstance(getattr(track, f.name), wp.array)}
per_env = {k: a.numpy() for k, a in arrays.items() if a.shape[0] == E}
per_point = {k: a.numpy().reshape(E, -1, *a.numpy().shape[1:])
for k, a in arrays.items() if a.shape[0] != E}
StepResult.validis the generator’s state, not a step output: never write into it, and leave it out of code that zeroes or poisons step outputs. It is live: after a regeneration in the same iteration it describes the new courses, so read it before regenerating, orclone()the result.StepResultalso gains read-only access by component name (res["progress"] is res.events). Its dataclass behaviour is unchanged (fields,asdict,==,repr,iter(res)still raises), but code that probes for a mapping now finds one:**resanddict.update(res)succeed,hasattr(res, "__iter__")isTrueandvars(res)gains_items, so a serializer that sniffs these takes another path.track_gen.testing.capture.result_arrays(res)listsvalid.
Results: Track, GateSequence, Centerline describes every field of the results.
Runtime utilities¶
- Type:
Breaking
- Affects:
code that uses
CollisionChecker,TrackLocalizer,CheckpointSampler,PropSampler,ProgressTracker,HeightFieldBakeror the course line directly, and code that clones results- Find it:
.clone(,except ValueError,.track =,.center =,course_line.track,bind_inputs(
clone()of aTrackorGateSequencechecks the result (a malformed hand-built one raisesValueError) and blocks the host until the copy is complete. Inside a capture it raisesCaptureError: allocate a result once withTrack.alloc(track.layout)andcopy_frominto it instead (below).A utility binds its source’s arrays at construction. Assigning a new array to a member of the source later (
track.center = other) is not seen, where 0.1 read the member again at every call: write into the arrays, or build a new utility.Construction errors changed: a missing member raises
TypeErrornaming it (CollisionChecker needs a HasBand source (...): Centerline has no 'outer', 'inner'), a mismatched one aValueErrorprefixed by the utility and the member, where 0.1 inferred the layout from one field and raisedValueError("track batch layout invalid: ..."). Code that catches construction errors should catch both types.A
validorgenerationmember of the wrong shape, dtype or device on a hand-built source raises at construction (0.1 ignored it).course.course_line.trackis aCenterlineand emits aDeprecationWarning: readcourse.course_line.centerline. Itsouter,inner,normalandwinding(NaN or zero placeholders on 0.1) raiseAttributeError.Replacing a bound array after a call was recorded into a graph emits
track_gen.exec.RebindAfterCaptureWarning(aUserWarning): the graph keeps reading the old arrays, so record it again. A test suite run with-W errorsees it.When you adopt
generate(advance=True)(new in 0.2),ProgressTrackerandTrackLocalizerreset an env’s state by themselves once its course changes.
Copying a result inside a graph:
from track_gen import Track
gen = TrackGenerator(TrackGenConfig(num_envs=E, device=device))
track = gen.generate()
snapshot = Track.alloc(track.layout) # allocate once, outside the capture
with wp.ScopedCapture(device=device) as capture:
gen.generate(advance=True)
snapshot.copy_from(track) # track.clone() would raise CaptureError here
wp.capture_launch(capture.graph)
If you subclass or wrap Course, a utility or a generator¶
- Type:
Breaking, and Silent for overridden hooks
- Affects:
subclasses, wrappers and monkeypatches of
Course,TrackGenerator,GateGeneratoror a utility, and code that reads their private attributes- Find it:
class .*(Course),_build_subtools,_apply_bind,_bind_args,_step_result,self.result =,self.rng =,self.components =
Course._build_subtools(which 0.1’s firstgenerate()called) is gone, and_apply_bindis no longer called bybind(): an override or a monkeypatch of either silently stops running.course._bind_argsholds only the arrays given to the last successfulbind. On 0.1 it always had the four names, withNonefor an absent one, socourse._bind_args["box_position"](or["orientation"]on a course without collision) now raisesKeyError: use.get(name).course._step_resultexists from construction (0.1 created it at the firststep()). The other 0.1 private names (_cfg,_posts,_device,_E,_needs_boxes(),_refresh,_fill_posts,_validate_seed_array,_check_arr) keep their meaning.New read-only properties raise
AttributeErrorwhen assigned:.resulton every utility,CollisionChecker.method,CheckpointSampler.max_checkpoints,PropSampler.max_props,TrackGenerator.rngandGateGenerator.rng,course.components. A subclass that storesself.result,self.rngorself.componentsmust rename them.New public names on
Course, which a subclass attribute of the same name shadows:components,prepare,from_config,track,gates,regenerate,enqueue_generate,inputs,seeds,graph. New private names:_producer,_needs_bind,_superseded,_reset_mask,_gen_inputs,_components_view,_zero_mask,_setup,_init_from_config,_set_legacy_attributes,_ctx,_ready,_check_owner,_check_mask.The
Courseconstructor isCourse(generator=None, components=(), *, stream=None, prepare=True, config=None). A subclass whose__init__(self, config)keeps the 0.1 signature still works forMyCourse(cfg)andMyCourse.from_config(cfg); the presets (MyCourse.track(...)) callcls(generator, components, stream=..., prepare=...)and need the new one.gen._graphis a read-only alias ofgen.graph;gen._generator_specof a built-in is no longer aGeneratorSpec(Plugins: built-in generators are Stages).
What to do. The course is complete at construction, so move the body of a
_build_subtools override to your __init__, after super().__init__(...). For
work that must follow every regeneration, write a component (its
on_generate runs at each regeneration, Writing a Component) and pass it with
extra=[...] to Course.track / Course.gates, or in the components of
Course(generator, components). Course(CourseConfig) takes no components, and
the presets build the course with cls(generator, components, stream=...,
prepare=...), so MyCourse.track(..., extra=[...]) needs a subclass with the new
constructor signature (one that forwards *args, **kwargs to super().__init__).
Replace an _apply_bind override with a bind() override that calls
super().bind(...) first (it works with either signature), or with a component that
declares the inputs, passed the same way (so not on a course built from a
CourseConfig). A 0.1 subclass that added a sampler:
from track_gen.props import PropSampler
class MyCourse(Course):
def _build_subtools(self): # 0.1: run by the first generate(); 0.2: never
super()._build_subtools()
self.cones = PropSampler(self.result, spacing=0.3)
On 0.2:
from track_gen.props import PropSampler
class MyCourse(Course):
def __init__(self, config):
super().__init__(config) # the course is complete here
self.cones = PropSampler(self.result, spacing=0.3)
course = MyCourse(CourseConfig(mode="track", gen=TrackGenConfig(num_envs=E, device=device),
checkpoint_spacing=0.6))
cones = course.cones.sample()
Removed private names¶
- Type:
Breaking
- Affects:
0.1 generator plugins that copied a built-in module and import its private kernels, and code that reads the generators’ private scratch
- Find it:
from track_gen._src,_pipe,_k(,_k,,_scratch
The built-in generators are now built from the public device ops in track_gen.ops,
and these private names are gone (their arguments also differ: explicit salts, strides
and masks):
Removed |
Use instead |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
every generator module’s |
|
|
|
Building blocks: track_gen.ops describes each op. Also:
Importing a built-in generator module no longer registers it. A plugin registers itself with a
register(registry)function (Packaging a Plugin).The pipeline’s working buffers no longer fall back to a generator scratch field of the same name, so a scratch field named
w,kappa,z,area_aorarea_bcan no longer shadow them.gen._scratchis a view of the pipeline’s buffers under the 0.1 field names (an absent stage’s slot isNone), andGateGenerator._scratch(0.1’s 6-tuple) is gone. Read intermediate results withgen.program.tap(...)(Taps).
Deprecations¶
These still work. Most emit a DeprecationWarning (each item says which), and each is
removed at 1.0 (Removed in 1.0, and what did not change).
set_capturing¶
- Type:
Deprecated
- Affects:
code that switched the utilities into capture mode
- Find it:
set_capturing
track_gen.set_capturing() (also track_gen.course.set_capturing and
Course.set_capturing) has no effect: the utilities no longer synchronize the host, so
they can be recorded into a graph without it (Library calls inside a torch CUDA graph).
import track_gen
track_gen.set_capturing(True) # DeprecationWarning: no effect, removed in 1.0
Delete the calls.
style_sampling¶
- Type:
Deprecated
- Affects:
bezierconfigs withstyle_sampling=Trueand its ranges- Find it:
style_sampling,scale_range,rad_range,handle_clamp_frac_range
Building a bezier generator with style_sampling=True warns once per build.
Its per-env draws become randomizers, which draw each env’s value from a uniform range
at every generation (Domain randomization and curricula):
config = TrackGenConfig(num_envs=E, device=device, style_sampling=True,
scale_range=(0.8, 1.2), rad_range=(0.1, 0.35))
gen = TrackGenerator(config) # DeprecationWarning
# a preset: the same uniform distributions
gen = TrackGenerator(presets.track(num_envs=E, device=device,
randomize={"generator.size": (0.8, 1.2),
"generator.rad": (0.1, 0.35)}))
# from a config: style_sampling off, then randomize the spec the config builds
config = TrackGenConfig(num_envs=E, device=device, style_sampling=False)
spec = legacy.to_pipeline_spec(config).with_randomize({"generator.size": (0.8, 1.2)})
gen = TrackGenerator(spec)
# a Course
course = Course.track(spec, checkpoint_spacing=0.5)
scale_range=(a, b)maps to{"generator.size": (a / scale, b / scale)}(it must lie in[0.1, 10]),rad_rangeto"generator.rad", andhandle_clamp_frac_rangeto"generator.handle_clamp_frac".The distributions are the same, but the randomizers draw from another random stream, so each seed gives another track: rebaseline fixed evaluation sets built with
style_sampling.A course built from a randomized
sizechecks its SDF and height-field resolution against the largest size it can draw (size_hi="auto", Grid resolution and course size). At flat elevation the checkpoint capacity already holds every track that is not truncated, at any size (A course curriculum).
The _src registries¶
- Type:
Deprecated (no warning yet), and Breaking for direct writes
- Affects:
generator plugins written for 0.1
- Find it:
track_gen._src.generator_registry,gate_generator_registry,register(,GENERATORS[,GATE_GENERATORS[,capturable=False
A 0.1 plugin wrote generate(seeds_wp, config, out_centerline, out_valid_wp, scratch)
and registered it in the private registry. Here is one, a wobbly circle:
from types import SimpleNamespace
import warp as wp
@wp.kernel
def _wobbly_k(seeds: wp.array(dtype=wp.int32), n: int, out: wp.array(dtype=wp.vec2f)):
t = wp.tid() # one thread per point
e = t // n
theta = 6.2831853 * float(t - e * n) / float(n)
r = 0.8 + 0.1 * wp.sin(3.0 * theta + float(seeds[e]))
out[t] = wp.vec2f(r * wp.cos(theta), r * wp.sin(theta))
@wp.kernel
def _all_valid_k(valid: wp.array(dtype=wp.int32)):
valid[wp.tid()] = 1
def alloc_scratch(config):
return SimpleNamespace() # this generator needs no buffers
def generate(seeds_wp, config, out_centerline, out_valid_wp, scratch):
E, N = int(config.num_envs), int(config.num_points)
wp.launch(_wobbly_k, dim=E * N, inputs=[seeds_wp, N, out_centerline],
device=out_centerline.device)
wp.launch(_all_valid_k, dim=E, inputs=[out_valid_wp], device=out_valid_wp.device)
Its 0.1 registration still works, with no warning:
from track_gen._src.generator_registry import GeneratorSpec, register
register(GeneratorSpec(name="wobbly", alloc_scratch=alloc_scratch, generate=generate))
track_gen._src.generator_registry and gate_generator_registry are now views of
the public registry, track_gen.plugin.registry, and they go at 1.0. Writing into
GENERATORS or GATE_GENERATORS raises TypeError: in tests, use
registry.scoped() instead of resetting the dictionaries. The old register(spec)
still replaces a track generator of the same name, now with a DeprecationWarning,
while registry.add refuses a name that is already registered
(DuplicateSpecError) unless you pass replace=True. Two ports:
# the minimal port: the same generate(), the public registry
from track_gen.plugin import GeneratorSpec, registry
registry.add(GeneratorSpec(name="wobbly", alloc_scratch=alloc_scratch,
generate=generate)) # UserWarning: not a dotted name
# the full port: every launch goes through ctx (or track_gen.ops), under a dotted name
from track_gen import ops
from track_gen.exec import resolve
from track_gen.plugin import GeneratorSpec, registry
def generate_ctx(seeds_wp, config, out_centerline, out_valid_wp, scratch, ctx=None):
ctx = resolve(ctx, out_centerline.device) # the facades always pass ctx
E, N = int(config.num_envs), int(config.num_points)
ctx.launch(_wobbly_k, E * N, [seeds_wp, N, out_centerline])
ops.fill(ctx, out_valid_wp, 1)
registry.add(GeneratorSpec(name="acme.wobbly", alloc_scratch=alloc_scratch,
generate=generate_ctx, accepts_ctx=True))
To ship it, declare the entry point in your package’s pyproject.toml
(acme_tracks.register(registry) calls registry.add):
[project.entry-points."track_gen.plugins"]
acme = "acme_tracks:register"
The minimal port runs eagerly and in the generator’s own CUDA graph. Recording it into your graph on another stream needs the full port: the minimal one raises
LegacyPluginStreamErrorthere (Troubleshooting capture).A generator registered with
capturable=FalseraisesNotCapturableErrorbefore any launch when you call it while your graph is recording (0.1 ran it eagerly at capture time). Call it outside the capture, or build from aPipelineSpecwithrequire_caller_capturable=True, so that construction refuses such a generator (PlanError) instead of the capture (Troubleshooting capture).A dotted name (
acme.wobbly) changes thegenerator=string in your users’ configs and saved specs; a name without a dot only warns.GeneratorSpecgains two trailing fields,accepts_ctx(defaultFalse) andparams(defaultNone), so positional construction is unchanged.
Packaging a Plugin covers registration, entry points and names;
GeneratorSpec and the 0.1 plugin ABIs the GeneratorSpec contract and its status.
Plugins: built-in generators are Stages¶
- Type:
Deprecated, and Breaking for code that calls the built-ins’ specs
- Affects:
plugin authors, and code that reads the built-in generators from the registry
- Find it:
.params,.generate(,.alloc_scratch(,.capturable,GateGeneratorSpec,warp_gate.,GenScratch,TrackGenConfig)subclasses
The built-in generators are stages now (Stage classes), not
GeneratorSpec objects: registry.get("generator", "bezier") (and the 0.1
generator_registry.get, GENERATORS[...], the gate equivalents) returns a class.
Its
.generateand.alloc_scratchare gone (the module functionsgenerate_*_warp/*_alloc_scratchremain until 1.0). Read capture support and the rest fromregistry.describe(kind, name)(.capturable,.accepts_ctx,.seeded), and a gate generator’s orders fromregistry.describe("gate_generator", name).supported_orderings..paramswarns: use.Params, the generator’s Params class.
from track_gen.plugin import registry
params_cls = registry.get("generator", "bezier").params # DeprecationWarning
params_cls = registry.get("generator", "bezier").Params
print(registry.describe("generator", "bezier").capturable) # True
For your own plugins:
Write new generators as a
Stage(Writing a Generator). AGeneratorSpeckeeps working (The _src registries); one withcapturable=Falsenow runs as a host step between recorded segments instead of making the whole pipeline eager.GateGeneratorSpecwarns when a generator binds it (GateGenerator,Course, the conformance kit), not when it is registered, and is removed at 1.0: write aStage(kind="gate_generator", output="anchors")(Writing a Gate Generator).The
warp_gatehelpers of 0.1 gate plugins (alloc_order_scratch,order_points,finish_ordered_gates) warn; thewarp_pipeline,warp_generateandwarp_generate_hullhelpers (resample_uniform,self_intersections_inplace,ccw_sort_inplace, …) are deprecated without a warning yet. Legacy _src helpers maps each one to its op intrack_gen.ops.warp_pipeline.GenScratchmoved towarp_generate.GenScratch(the old name warns).Run
track_gen.testing.check_generatorin your CI (Testing a Plugin).
Your plugin’s knobs. A 0.1 plugin read its knobs from a TrackGenConfig subclass.
That still works, but a frozen Params dataclass passed as params= supersedes
it: users set the knobs with generator_params, the values are validated at
construction, and registry.describe reports them. Override Params.from_legacy so
the subclass configs your users already have keep their meaning:
# 0.1: a config subclass, read with getattr
@dataclasses.dataclass
class AcmeConfig(TrackGenConfig):
acme_lobes: int = 3
def generate(seeds_wp, config, out_centerline, out_valid_wp, scratch):
lobes = getattr(config, "acme_lobes", 3)
...
import dataclasses
from track_gen import ops
from track_gen.exec import resolve
from track_gen.plugin import GeneratorSpec, Params, registry, static
@dataclasses.dataclass
class AcmeConfig(TrackGenConfig): # your users' existing configs
acme_lobes: int = 3
@dataclasses.dataclass(frozen=True, kw_only=True)
class AcmeParams(Params):
lobes: int = static(3, lo=1, hi=12, doc="lobes of the wobble")
@classmethod
def from_legacy(cls, cfg): # a config without generator_params
return cls(lobes=getattr(cfg, "acme_lobes", 3))
@wp.kernel
def _lobes_k(seeds: wp.array(dtype=wp.int32), n: int, lobes: int,
out: wp.array(dtype=wp.vec2f)):
t = wp.tid()
e = t // n
theta = 6.2831853 * float(t - e * n) / float(n)
r = 0.8 + 0.1 * wp.sin(float(lobes) * theta + float(seeds[e]))
out[t] = wp.vec2f(r * wp.cos(theta), r * wp.sin(theta))
def generate(seeds_wp, config, out_centerline, out_valid_wp, scratch, ctx=None):
ctx = resolve(ctx, out_centerline.device)
E, N = int(config.num_envs), int(config.num_points)
lobes = config.generator_params.lobes # the resolved, validated instance
ctx.launch(_lobes_k, E * N, [seeds_wp, N, lobes, out_centerline])
ops.fill(ctx, out_valid_wp, 1)
registry.add(GeneratorSpec(name="acme.lobes", alloc_scratch=lambda config: None,
generate=generate, accepts_ctx=True, params=AcmeParams))
old = TrackGenerator(AcmeConfig(generator="acme.lobes", acme_lobes=5,
num_envs=E, device=device))
new = TrackGenerator(TrackGenConfig(generator="acme.lobes",
generator_params={"lobes": 5},
num_envs=E, device=device))
Optional: new ways, no action needed¶
The 0.1 forms below keep working through 0.x with the same outputs. Move when it suits you.
Flat config, Params and presets¶
- Type:
Optional: no action needed
- Affects:
everyone
- Find it:
TrackGenConfig(,GateGenConfig(
Flat TrackGenConfig and GateGenConfig fields keep working through 0.x, with
byte-identical outputs. 0.2 adds two forms that give the same bytes: each generator’s
knobs as a typed Params class (generator_params), and presets, functions that
build a spec (a PipelineSpec, the list of stages a generator runs).
Generating tracks and gates describes them. Only these force a change:
Fields that the generator never reads warn (
DeprecationWarningat config construction, removed at 1.0):energy_steps,energy_lr,energy_w_sep,energy_w_len,energy_w_bend,energy_w_anchor,tp_iters,tp_tau,tp_alpha,tp_beta,smooth_finish,smooth_finish_iters,smooth_finish_tau,relax_chunk_size,relax_use_warp,max_regen_iters,min_angle,num_harmonics,decay_p,amplitude,num_centerline_samples. Setting one leaves every output unchanged: delete it.TrackGenConfigandGateGenConfiggain a public field namedsize(each track’s size, a multiplier ofscale): a config subclass that declares its ownsizefield must rename it.The values of Newly rejected values.
The checks of each generator’s own knobs run when the generator is built, not when the config is: a bad knob of the selected generator raises from
TrackGenerator(...)(orCourse(...)) instead ofTrackGenConfig(...), with the same message, and a bad knob of a generator you did not select is ignored.
TrackGenConfig(num_envs=E, device=device, energy_steps=200) # DeprecationWarning
flat = TrackGenConfig(num_envs=E, half_width=0.05, rad=0.3, edgy=0.1, device=device)
typed = TrackGenConfig(num_envs=E, half_width=0.05, device=device,
generator_params={"rad": 0.3, "edgy": 0.1})
spec = presets.track(num_envs=E, device=device, half_width=0.05,
generator_params={"rad": 0.3, "edgy": 0.1})
tracks = [TrackGenerator(c).generate().center.numpy().copy()
for c in (flat, typed, spec)]
Course presets¶
- Type:
Optional: no action needed, except one call
- Affects:
Courseusers- Find it:
CourseConfig(,course.generate()
Course(CourseConfig(...)) keeps working through 0.x with the same outputs. One call
changes meaning: on 0.1 a course was empty until its first generate()
(step() raised before it); on 0.2 it is ready at construction, so that first
generate() only computes the same batch again. Drop it:
# 0.1, still runs
config = TrackGenConfig(num_envs=E, half_width=0.05, device=device)
course = Course(CourseConfig(mode="track", gen=config, seeds=42, checkpoint_spacing=0.6,
collision="segments"))
course.bind(position, orientation, half_extents)
course.generate() # 0.1: required; 0.2: the same batch again
res = course.step()
With a preset (the same components and outputs):
course = Course.track(config, checkpoint_spacing=0.6, collision="segments", seeds=42)
course.bind(position, orientation, half_extents)
res = course.step() # ready at construction
res["progress"] is res.events # each component's result by name
course.reset() # every env; reset(mask) as before
New on every course: StepResult access by component name; bind(**arrays) for
the inputs of your own components; reset(mask=None) resets every
env; course.inputs["reset_mask"], a stable [E] array your kernels can write the
reset mask into; masked regeneration (RL auto-reset); Course(generator,
components) over a generator you built (one course per generator: a newer course built
on the same generator takes it over, and the older one raises at its next call). A
preset’s collision="sdf" and tools.HeightFieldBake warn at construction when
their resolution does not cover the largest track size the generator can draw. Only a
Course subclass may need changes (If you subclass or wrap Course, a utility or a generator). Courses and
Composing a course describe the presets.
Migrating from CourseConfig¶
Course(CourseConfig) keeps working through 0.x with no warning. To move to a preset:
|
|
What changes |
|---|---|---|
|
the preset |
|
|
|
|
|
|
|
|
the same names ( |
|
|
|
|
|
|
an SDF resolution too coarse for the largest track size warns |
|
|
as above |
|
|
|
|
|
|
|
the same names ( |
|
|
the same call; names no component declares are ignored, and |
the errors are the components’: a missing |
Course.gates(cfg, collision=r, seeds=PerEnvSeededRNG(seeds=s, num_envs=E,
device=d)) gives the bytes of Course(CourseConfig(mode="gates", gen=cfg, seeds=s,
post_radius=r)).
Smaller changes¶
Each generator and each
Courseis single-writer: never call the same one from two threads at once. Independent generators on different threads no longer serialize on a process-wide lock (0.1 plugins withoutaccepts_ctx=Truestill take it).track_gen._experimentalis not in the wheel any more (it stays in the source tree).TrackGeneratorraisesValueErrorinstead ofAssertionErrorfor an unsupportedrelax_solverorsmooth_finish=True, and checks that the rng has exactlynum_envsseeds, asGateGeneratordoes.relax_solver,z_profileandgate_orderingname any registered stage and are checked when the generator is built:TrackGenConfig(z_profile="bogus")andGateGenConfig(gate_ordering="spiral")construct, and the generator raises aValueErrorthat lists the available names.An unknown
z_profilepassed to the privateapply_z_profileraisesValueError(0.1 fell back to"noise").In the legacy plugin helpers, a
count[e]above the row length is clamped to it (0.1 read past the env’s row); valid counts give the same results.
If you used a 0.2.0.devN preview¶
The v0.2.0.devN tags were previews of 0.2. Everything above applies to code written
for them as well; the changelog remains the exhaustive record of each release. This
table lists the changes between previews that alter an error, a message, a default, a
stream or an output, with what to do.
Since |
Change |
What to do |
|---|---|---|
0.2.0.dev2 |
With |
nothing (a debugging aid: No host sync after library calls) |
0.2.0.dev3, 0.2.0.dev5 |
|
look stages up by id; never assume |
0.2.0.dev4 |
The width stage runs before |
|
0.2.0.dev4 |
Plugin width stages run before |
rebaseline outputs of pipelines whose width is not |
0.2.0.dev4 |
|
pass one value to every stage (the presets do) |
0.2.0.dev4 |
|
a hand-built spec that places it before any |
0.2.0.dev4 |
Built-in gate generators are |
Plugins: built-in generators are Stages; read intermediates with |
0.2.0.dev4 |
Under torch’s capturing stream, |
|
0.2.0.dev4 |
A |
construct before capturing; keep |
0.2.0.dev4 |
|
code that tested |
0.2.0.dev5 |
|
|
0.2.0.dev5 |
|
pass Python |
0.2.0.dev5 |
The generator is the first stage that is not a randomizer (it was
|
use |
0.2.0.dev5 |
|
nothing for the built-ins (the same stage); |
0.2.0.dev5 |
The |
|
0.2.0.dev5 |
|
drop them from hand-built specs |
0.2.0.dev5 |
|
pass ints; keep bar widths positive |
0.2.0.dev5 |
Spec dicts built from a built-in |
regenerate stored dicts once |
0.2.0.dev5 |
New |
use |
0.2.0.dev5 |
|
a dynamic field’s kernel reads |
0.2.0.dev5 |
|
|
0.2.0.dev6 |
Invalid envs read as poisoned in every utility; |
Invalid envs read as poisoned, New result fields, The default checkpoint capacity |
0.2.0.dev6 |
Rebinding after a capture warns; malformed optional members raise; gates-mode post
collision scans each env’s real posts; |
|
0.2.0.dev6 |
|
stop resetting by hand after an advancing regeneration ( |
0.2.0.dev6 |
|
code that expects one key sees two |
0.2.0.dev6 |
|
|
0.2.0.dev6 |
The |
filters on the category or the first sentence are unaffected |
0.2.0.dev6 |
A refused conditional body is re-raised as |
record inside |
0.2.0.dev6 |
On CUDA, a |
nothing (same outputs; only the streams of construction launches change) |
Specs written for 0.2.0.dev3¶
The width stage moved and three stages lost a field, so a spec built in Python against
0.2.0.dev3 may need an edit; a saved dict migrates on its own (Saved specs: format /1
and /2), with one DeprecationWarning. TrackGenConfig
users, presets.track(half_width=...) users and every Track consumer are
unaffected.
A
half_widthkey in the params ofrelax.xpbd,offset.defaultorvalidity.defaultraises, before the params are coerced:stage 'relax' (relax.xpbd): half_width is no longer a relax.xpbd param (0.2.0.dev4): the half-width is cl.half_width, written by the width stage (presets.track(half_width=...) / width.constant).A spec in the old order (
relax,uniform,width) hasrelax.xpbdreadcl.half_widthbefore any writer: the read error names the new order (the example under build() and the planner). A hand-builtrelaxed_stationsgate spec written before the width stage moved, which had no width stage, gets the same message.An old-order spec without relaxation (
relax.none,uniform,width, …) still plans and gives the same bytes, with one launch fewer than in 0.2.0.dev3: its width stage now writescl.half_widthafter the uniform resample, which has no version of it to fill.
Removed in 1.0, and what did not change¶
What 0.2 deprecates goes at 1.0:
Removed at 1.0 |
Warns in 0.2 |
Replacement |
|---|---|---|
|
yes |
nothing (set_capturing) |
|
yes |
|
|
yes |
|
|
yes |
|
|
yes, when bound |
a gate generator |
the |
yes |
the order stages and |
|
yes |
|
the |
no |
|
the |
only when a registration replaces another |
|
the |
yes, when set |
nothing (Flat config, Params and presets) |
|
no (inert) |
nothing |
loading |
yes |
save them again as |
What did not change:
For valid envs, every default generator’s output is byte-identical to 0.1, except
repulsive(The repulsive generator’s output); only thevalidflag of tracks 0.1 truncated changes (Truncated tracks are invalid).TrackGenConfig,GateGenConfigandCourse(CourseConfig)keep working through 0.x, with no warning beyond the items above.The runtime utilities give 0.1’s outputs for valid envs, apart from the
sdfquery (The sdf collision query) and the checkpoint capacity (The default checkpoint capacity).