Choosing a Reporting Interval for Multi-Agent Releases

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.

Root Cause: Every Interval Destroys Something Specific

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.

Measurement fidelity against reporting interval The horizontal axis is the reporting interval in seconds, from one second up to two minutes. The vertical axis is each measurement expressed as a percentage of its value at one-second sampling, so a hundred percent means nothing was lost. Four curves are drawn from the same underlying trace, decimated to each interval rather than re-simulated. Path length declines gradually as straight-line hops cut the corners of a curving path. The ninety-fifth-percentile speed declines too, because differencing coarse samples averages away the peaks. Turn count falls much faster, since a turn taken over a few seconds is invisible once samples are further apart than the turn takes. Stop count falls fastest of all and collapses in steps as the interval crosses the typical stop duration — the stops do not blur, they disappear. The note underneath makes the point that these degrade at different rates, so no single interval is a compromise across them and no single correction factor restores them. Different measurements die at different intervals 1 30 60 90 120 0% 25% 50% 75% 100% reporting interval (s) fidelity vs 1 Hz (%) stop count path length path length speed p95 turn count stop count One hour of 1 Hz motion decimated to each interval — the same trace throughout, not four simulations. Because the four curves fall at different rates, no single interval is a compromise across them and no single correction factor restores them: a release corrected for path length is still missing two thirds of its stops.
Four measurements against reporting interval, each normalised to its 1 Hz value: they degrade at different rates and none of them degrades gracefully.

Prerequisite Check: Measure the Loss on Your Own Reference

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
def interval_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


def metrics(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.

Fix: Ship the Fine Series, Derive the Coarse Ones, Publish Both Contracts

1 — Generate at the finest interval the model is valid at

python
GENERATION_DT = 1.0     # not a release parameter — a property of the physics model


def generate(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.

2 — Decimate deterministically, with the phase recorded

python
def decimate(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.

3 — Publish the loss alongside the coarse artifact

python
def coarse_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.

Decimation against regeneration for a coarse release Two pipelines are shown side by side. On the left, the model generates at its own resolution, noise is injected at that resolution, and the coarse release is produced by keeping every nth sample at a declared phase. The fine series remains as ground truth, every coarse point is literally a fine point, and the measurement loss at the coarse interval can be computed exactly by comparing the two. On the right, the model is run directly at the coarse interval. The result is a plausible coarse trace, but it is a different realisation: no fine series exists to compare it against, the integrator has taken large steps and accumulated different error, noise was applied at the wrong resolution so its effective magnitude differs, and there is no way to reconcile the artifact with anything. Beneath each pipeline the preserved properties are listed. The note underneath states the rule the comparison supports: generate at model resolution regardless of publication interval, because the fine series is cheap to keep and impossible to recover. The coarse release should be a view of the fine one, not a separate run generate at model dt 1 Hz inject noise at model dt correct magnitude decimate every nth sample declared phase generate at release dt 30 s steps inject noise at release dt wrong magnitude publish no fine series nothing to compare decimation keeps the fine series as truth every coarse point exact the loss computable regeneration loses the reference entirely integrator step fidelity noise applied correctly Generate at model resolution regardless of the publication interval. The fine series costs storage that is small next to the compute already spent, and it is the only thing that makes the coarse artifact's bias measurable rather than assumed — once it does not exist, no later analysis can recover 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.

Verification Step: Assert the Coarse Release Is a Function of the Fine One

python
def test_coarse_is_a_subset_of_fine(fine, coarse, interval, phase=0):
    factor = int(interval / GENERATION_DT)
    for f_tr, c_tr in zip(fine, coarse):
        assert [p.signature() for p in f_tr[phase::factor]] == [p.signature() for p in c_tr]


def test_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]
        assert abs(actual - claimed) < tol, f"{key}: claimed {claimed:.2f}, actual {actual:.2f}"


def test_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 in zip(tr, tr[1:])}
        assert len(deltas) == 1, f"mixed intervals in one trace: {sorted(deltas)[:4]}"


def test_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.

The coarsest interval each downstream use tolerates A matrix with five downstream uses as rows. Stop and dwell analysis needs the finest data of all — an interval no coarser than a few seconds — because a stop shorter than the interval is not blurred but absent, and typical dwell times are short. Turn and manoeuvre analysis needs a similar resolution for the same reason. Map matching tolerates a moderate interval because the network constrains the path between samples, so the missing detail is partly recoverable. Origin-destination and flow analysis tolerates a coarse interval, since only the endpoints and the corridor matter. Coverage and presence analysis tolerates the coarsest interval of all, needing only that a sample exists somewhere in each period. A final column gives the relative storage cost, which is the reciprocal of the interval and is the reason coarse releases are chosen. The note underneath points out that the cost column is usually overweighted in this decision relative to the cost of regenerating a study when somebody later needs stops. Match the interval to the use, and publish more than one downstream use coarsest usable why storage stop & dwell analysis 1–5 s short stops vanish, not blur 1.0× turn & manoeuvre analysis 1–5 s a turn takes seconds 1.0× map matching 10–15 s the network constrains between 0.1× origin-destination flow 60 s endpoints and corridor only 0.02× coverage & presence 300 s one sample per period suffices 0.003× The storage column is what usually decides this, and it is usually overweighted. A thirtyfold size difference is real but small next to the cost of regenerating a study six months later because the release that shipped cannot answer a question about stops.
Which interval each downstream use actually needs, against what each one costs to store and transfer.

Edge Cases & Gotchas

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.

Publishing More Than One Interval

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.