The release ships at 30-second intervals because that is what the reference feed used. Stop
detection on it finds a third of the stops, turn counts are wrong by half, and the distance
totals are eight percent short — none of which is a modelling defect.
Part of Temporal Synchronization for Moving Objects: alignment is about making timestamps from different agents comparable, and this is the prior question of how often they should exist at all.
Down-sampling a trajectory is not a uniform loss of detail. It removes particular features
completely while leaving others intact, and the boundary is sharp enough to predict.
Distance is systematically under-measured. Summing straight-line hops between samples cuts
every corner, so measured path length falls as the interval grows. The loss is roughly
proportional to the curvature of the path, so it is small on motorways and large in a city
centre — which means a single correction factor cannot fix it.
Features shorter than the interval vanish entirely. A forty-second stop is invisible at
sixty-second sampling, not blurred. Stop counts therefore fall in steps as the interval crosses
the modal dwell duration, and the step is large.
Speed and acceleration statistics are aliased, not merely smoothed. Differencing coarse
samples produces a speed series with lower variance and a truncated tail, so the same trace
appears to have gentler driving at a coarser interval. This is the effect most often mistaken for
a real difference between datasets.
Four measurements against reporting interval, each normalised to its 1 Hz value: they degrade at different rates and none of them degrades gracefully.
The general shape above is universal; the numbers are not, because they depend on the network and
the behaviour being modelled. Measure them once, on a fine reference, and the choice becomes a
lookup rather than an argument.
python
definterval_sensitivity(fine_traces, intervals, dt_fine:float=1.0)->dict:"""Recompute headline metrics at each candidate interval from the same source traces."""
ref = metrics(fine_traces, dt_fine)
out ={}for iv in intervals:
coarse =[decimate(t,int(iv / dt_fine))for t in fine_traces]
got = metrics(coarse, iv)
out[iv]={k: got[k]/ ref[k]for k in ref}return out
defmetrics(traces, dt)->dict:return{"path_length":sum(polyline_length(t)for t in traces),"stop_count":sum(len(detect_stops(t, dt))for t in traces),"turn_count":sum(count_turns(t, threshold_deg=35)for t in traces),"speed_p95": quantiles([s for t in traces for s in speeds(t, dt)],[0.95])[0],}
Running this before choosing is what turns “30 seconds seems reasonable” into “30 seconds costs
us sixty percent of stops and eight percent of distance”, which is a decision somebody can
actually make.
GENERATION_DT =1.0# not a release parameter — a property of the physics modeldefgenerate(agent, route, dt:float= GENERATION_DT):"""Simulate at model resolution regardless of what will be published."""return integrate(agent, route, dt)
Generating coarse and publishing coarse means the coarse artifact is all that ever existed, and
the information is not recoverable. Generating fine costs storage that is cheap relative to the
compute already spent.
defdecimate(trace, factor:int, phase:int=0):"""Keep every nth sample from a fixed phase — not a re-simulation."""return trace[phase::factor]
The phase parameter matters more than it looks. Decimating from a different offset produces a
different coarse series from the same fine one, and two consumers who decimated independently
will get different stop counts and disagree about which is right. Fixing and publishing the phase
makes the coarse release a function of the fine one rather than one sample of it.
defcoarse_release(fine, interval:float, sensitivity:dict)->dict:return{"trajectories":[decimate(t,int(interval / GENERATION_DT))for t in fine],"interval_s": interval,"generation_interval_s": GENERATION_DT,"known_bias": sensitivity[interval],# e.g. path_length 0.92, stop_count 0.38}
A consumer who knows that stop counts in this artifact are 0.38 of the true value can correct for
it or choose a different artifact. A consumer who does not know will report the number as
observed, and nothing downstream will ever question it.
Two ways to produce a coarse release: decimation keeps the fine series as ground truth, regeneration produces an artifact that cannot be reconciled with it.
deftest_coarse_is_a_subset_of_fine(fine, coarse, interval, phase=0):
factor =int(interval / GENERATION_DT)for f_tr, c_tr inzip(fine, coarse):assert[p.signature()for p in f_tr[phase::factor]]==[p.signature()for p in c_tr]deftest_known_bias_is_recorded(release, tol=0.02):
got = metrics(release["trajectories"], release["interval_s"])
ref = metrics(release["fine_reference"], release["generation_interval_s"])for key, claimed in release["known_bias"].items():
actual = got[key]/ ref[key]assertabs(actual - claimed)< tol,f"{key}: claimed {claimed:.2f}, actual {actual:.2f}"deftest_all_agents_share_one_interval(release, tol=1e-6):"""A mixed-rate release is a different artifact and must say so."""for tr in release["trajectories"]:
deltas ={round(b.t - a.t,6)for a, b inzip(tr, tr[1:])}assertlen(deltas)==1,f"mixed intervals in one trace: {sorted(deltas)[:4]}"deftest_phase_is_declared(release):assert"decimation_phase"in release,"the coarse series is not reproducible without it"
The second test is the one that keeps the documentation honest over time. A bias table copied
from an earlier release and never re-measured is worse than no table, because it is trusted; this
test recomputes it every build and fails when the generator’s behaviour has moved.
Which interval each downstream use actually needs, against what each one costs to store and transfer.
Event-driven reporting is not a fixed interval. Many real fleets report on distance travelled
or heading change rather than on a clock, which produces dense samples in cities and sparse ones
on motorways. Reproducing that requires a rule rather than a rate, and a release claiming to
match such a feed with a fixed interval will be wrong in a way no interval choice fixes.
Mixed intervals across agents. A realistic multi-agent release often has agents on different
reporting rates, which is fine and should be declared per agent. What is not fine is a release
where the interval varies for reasons nobody recorded — that is indistinguishable from
timestamp misalignment and will be diagnosed as such.
Storage is rarely the binding constraint people assume. A 1 Hz release is thirty times the
size of a 30-second one, which sounds decisive until it is priced. For most release sizes the
difference is small relative to the cost of regenerating the study when somebody needs stops.
Interval interacts with the noise model. Position noise partly averages out at coarse
intervals, so a release decimated after noise injection has different effective accuracy than one
generated coarse and noised. Noise belongs at generation resolution, before decimation, for the
same reason everything else does.
Once the fine series exists, shipping several intervals is nearly free, and it removes the
argument entirely.
The derived artifacts cost only storage. Decimation is a slice. A release family at 1, 10 and
60 seconds is three views of one generation run, produced in seconds, and each consumer takes the
one that matches what they are measuring instead of arguing for a global change.
They share an identity, which is what makes them comparable. Because all three come from the
same fine series with declared phases, a consumer can check that their coarse analysis agrees
with a colleague’s fine one, and any disagreement is a real methodological difference rather than
two different realisations. Two independently generated releases at different rates give no such
guarantee.
One of them should be the default, and it should be the fine one. A consumer who has not
thought about the interval will take whatever is listed first, and the failure mode of taking too
fine a release is a larger download, while the failure mode of taking too coarse a release is a
wrong answer that looks right.
The bias table belongs with the family, not with each artifact. Published once, showing every
metric at every interval as a fraction of its value at generation resolution, it lets a consumer
see immediately whether the interval they picked can answer their question — and it is the thing
that turns this from an internal engineering decision into something the release documents about
itself.