A generated trace contains a forty-minute dwell and the detector finds nothing there, or finds three separate stops where there was one. The dwell is present, the coordinates are right, and the noise model has quietly made the stop invisible.
Part of Stop Detection & Dwell-Time Modelling: where that page frames the round trip, this one is about the single interaction that decides its outcome — the relationship between how far a stationary agent’s fixes wander and how far apart a detector will tolerate them being.
Every density-based detector asks the same question: are these consecutive fixes close enough together, for long enough, to be one place? “Close enough” is a radius, and the fixes of a stationary agent are not at one point — they are scattered by positional error.
With white noise the scatter is bounded: fixes are distributed around the true location with a standard deviation of sigma, and virtually all of them fall within about three sigma. A detector radius comfortably above three sigma finds the cluster every time.
With correlated drift — which is what a real receiver produces, and what the noise model should therefore emit — the scatter is not bounded in the same way. The error at each fix is close to the error at the last one, so the sequence wanders, and the further it wanders the more likely it is to leave the detector’s radius partway through the dwell. The cluster splits, and one forty-minute stop becomes two twenty-minute stops with a phantom movement between them.
The two regimes need different radii, and the difference grows with dwell duration rather than staying fixed.
Measured over 3,000 rendered dwells: white-noise scatter plateaus almost immediately, and correlated drift keeps growing with the square root of the dwell.
import math
defrender(rng, minutes:int, interval:float, sigma:float, rho:float)->list:"""rho = 0 gives white noise; rho near 1 gives correlated drift."""
fixes, ex, ey =[],0.0,0.0for _ inrange(int(minutes *60/ interval)):
ex = rho * ex + math.sqrt(1- rho **2)* rng.normal(0, sigma)
ey = rho * ey + math.sqrt(1- rho **2)* rng.normal(0, sigma)
fixes.append((ex, ey))return fixes
defmax_extent(fixes)->float:"""The radius a detector actually has to cover: the widest pairwise separation."""returnmax(math.dist(a, b)for a in fixes for b in fixes)
white = render(rng, minutes=40, interval=10, sigma=9.0, rho=0.0)
drift = render(rng, minutes=40, interval=10, sigma=9.0, rho=0.94)print(f"white noise extent: {max_extent(white):.0f} m")# ≈ 55 mprint(f"correlated extent: {max_extent(drift):.0f} m")# ≈ 130 m
Same sigma, same duration, same interval — and the correlated version needs a detector radius more than twice as large. A detector tuned on white-noise synthetic data and deployed on real traces will split most long dwells, which is the exact failure this reproduces in the other direction.
For a first-order autoregressive error with per-step standard deviation sigma and correlation rho, the stationary standard deviation of the error is sigma, but the expected range over k steps grows roughly with the square root of the effective number of independent samples:
python
defexpected_extent(sigma:float, rho:float, steps:int)->float:"""Approximate the widest separation a stationary agent's fixes will show."""# effective independent samples: correlated steps count for less
k_eff =max(1.0, steps *(1- rho)/(1+ rho))return2.6* sigma * math.sqrt(math.log(max(k_eff,2.0)))
The formula is an approximation and it is a far better starting point than a fixed multiple of sigma, because it carries the two things that actually matter: how long the dwell is, and how correlated the error is.
Sizing from the longest intended dwell rather than a typical one is the point. A radius that works for a twenty-minute stop will split an eight-hour one, and eight-hour stops are exactly the ones a trip-chaining consumer most needs.
At some point the radius needed to hold a long dwell together becomes large enough to merge genuinely separate nearby stops — two shops on the same street, a home and the parking space outside it. That is the signal that the noise model, not the detector, is the thing to change:
python
defcap_drift(contract:dict, max_radius_m:float=60.0)->float:"""The largest rho that keeps the required radius under a ceiling."""
steps = contract["longest_intended_dwell_s"]/ contract["reporting_interval_s"]for rho in[r /100for r inrange(99,0,-1)]:if1.3* expected_extent(contract["noise_sigma_m"], rho, steps)<= max_radius_m:return rho
return0.0
Real receivers do bound their drift, because they are not free-running random walks: they are corrected by fresh satellite geometry, and the correction acts as a restoring force. Modelling that as a mean-reverting process rather than a pure random walk is both more realistic and what keeps the required radius finite.
The detector radius that holds a dwell together against the radius that starts merging neighbouring stops — the gap between them is the operating range, and drift closes it.
import pytest
DWELL_BANDS =[(180,600),(600,1800),(1800,7200),(7200,28800)]@pytest.mark.parametrize("lo,hi", DWELL_BANDS)deftest_recall_by_dwell_length(traces, contract, lo, hi):"""Recall must hold across the whole range, not just on average."""
intended =[s for t in traces for s in t["intended"]if lo <= s["duration"]< hi]
found =[s for s in intended if was_recovered(s, detect_stops(s["trace"],**contract))]
recall =len(found)/max(len(intended),1)assert recall >=0.90,f"{lo}-{hi}s dwells: recall {recall:.1%}"deftest_no_dwell_is_split(traces, contract):"""One intended dwell must not become two recovered stops."""
splits =0for t in traces:for s in t["intended"]:
overlapping =[r for r in detect_stops(t["fixes"],**contract)if r["arrive"]< s["depart"]and r["depart"]> s["arrive"]]
splits +=max(0,len(overlapping)-1)assert splits ==0,f"{splits} dwells were split by drift"
Banding by dwell length is what catches this defect. Aggregate recall stays high while long dwells fail, because long dwells are a minority of the count and a majority of the time — so an average weighted by stop count hides exactly the failure that matters most to a consumer weighting by duration.
Once the operating range closes there are only two moves, and they are not equivalent.
Widening the detector radius is free to implement and it is not free in consequence. It merges
genuinely separate nearby stops, and the stops it merges are the ones a consumer most cares about
distinguishing — a home and the shop on the corner, two adjacent premises on a delivery round.
Worse, it changes the reference detector, which means the release is now validated against
different parameters from the ones it declared, and a consumer using the declared ones sees
different stops.
Narrowing the drift changes the noise model, which means the traces are no longer calibrated
against whatever positional-accuracy target they were fitted to. That is a real cost, but it is a
recoverable one: the accuracy target is a declared parameter, the change is visible in the
contract, and a consumer can see it. It also tends to be the more defensible move on realism
grounds, because a pure random walk was never the right model — real receivers are corrected, and
a mean-reverting error both bounds the extent and matches the physics.
The rule of thumb that falls out is to change the model when the required radius exceeds the
separation between features the release needs to keep distinct, and to change the radius only
within the range where nothing merges. Whichever is chosen, record it: a radius or a rho that
moved without a note is the reason a later release behaves differently from an earlier one for no
visible cause.
Indoor dwells drift much more. A receiver inside a building has degraded geometry and its error both grows and correlates more strongly. If the simulation places activity stops indoors — most of them are — the drift parameters during a dwell should not be the same as those in motion, and modelling them as identical understates the required radius substantially.
The choice between these three decides whether a detector radius capable of holding a long dwell exists at all.
The interval changes during a dwell. Many devices reduce their reporting rate when stationary to save power. That helps drift, because fewer fixes means less wandering, and it hurts detection, because a detector with a minimum sample count may no longer reach it. If the contract models a reduced dwell interval, the detector’s minimum-samples parameter has to be derived from that interval rather than from the moving one.
Two stops within the detector radius. A radius sized for an eight-hour dwell will merge a home stop and a stop at a shop fifty metres away. There is no parameter that fixes this; the resolution is a detector that considers time as well as space — a gap in the fixes between two clusters is evidence of two stops even when they overlap spatially.
The generator’s own dwell is not stationary. An agent parked in a vehicle that rolls, or a pedestrian dwelling while moving within a building, has genuine movement during the dwell. That is realistic and it adds to the extent. Decide whether the release models it and record the decision, because it changes the radius by more than the noise does.