A loop in the road and a drone overhead watch the same cars and report different average speeds
A loop detector is buried in the road. A drone hovers overhead. Both watch exactly the same cars, and both report the average speed. They disagree, and neither is broken.
NoteBefore you watch: predict
Which reports the higher average speed: the loop, or the drone? Or do they agree?
The showdown
Two lanes: fast cars spaced out in the top lane, slow cars bunched together in the bottom lane. The orange bar is the loop detector; it flashes each time a car crosses it. The dashed frame is what the drone sees.
// Two lanes with no lane changing; a loop at x0 averages the speeds of the cars that cross it,// and a drone averages the speeds of all cars in view at each instant.showdown = {const Lr =360, x0 =180, lanes = [{v:30,s:60}, {v:15,s:40}];const cars = []; lanes.forEach((ln, li) => { for (let x =0; x < Lr; x += ln.s) cars.push({x,lane: li,v: ln.v}); });const dt =1/60;let t =0, loopN =0, loopSum =0, droneSum =0, droneN =0;const flash = [0,0];while (true) {for (const c of cars) {const xn = c.x+ c.v* dt;if (c.x< x0 && xn >= x0) { loopN++; loopSum += c.v; flash[c.lane] =10; } c.x= xn >= Lr ? xn - Lr : xn; } droneSum += cars.reduce((s, c) => s + c.v,0) / cars.length; droneN++; flash[0] =Math.max(0, flash[0] -1); flash[1] =Math.max(0, flash[1] -1); t += dt;yield {cars: cars.map(c => ({...c})), t, Lr, x0,flash: [...flash], loopN,loop: loopN ? loopSum / loopN :NaN,drone: droneSum / droneN}; }}
{const narrow = plotW <640, W = narrow ? plotW :720, left =20, right = W -20, sc = (right - left) / showdown.Lr;const laneY = [52,92], H =28;const colors = ["#2a9d8f","#e76f51"];return htl.svg`<svg width="100%" viewBox="0 0 ${W}${narrow ?184:150}" role="img" aria-label="Two lanes of cars passing a loop detector under a drone"> <rect x=${left -8} y="22" width=${right - left +16} height="112" fill="none" stroke=${accent} stroke-width="2" stroke-dasharray="7 5" rx="6"/> <text x=${left} y="16" font-size="13" fill=${accent} font-weight="bold">${narrow ?"Drone's view: whole road, one instant":"Drone's view: the whole road, at one instant"}</text>${laneY.map(y => htl.svg`<rect x=${left} y=${y - H /2} width=${right - left} height=${H} fill="#8882"/>`)}${laneY.map((y, li) => htl.svg`<rect x=${left + showdown.x0* sc -4} y=${y - H /2} width="8" height=${H} fill=${showdown.flash[li] ?"#ff9f1c":"#ff9f1c55"} />`)}${showdown.cars.map(c => htl.svg`<rect x=${left + c.x* sc} y=${laneY[c.lane] -8} width=${Math.max(4,4.5* sc)} height="16" rx="3" fill=${colors[c.lane]} />`)} <text x=${left + showdown.x0* sc} y="146" font-size="12" text-anchor="middle" fill="#c77700" font-weight="bold">▲ loop detector</text>${narrow? htl.svg`<text x=${left} y="166" font-size="11" fill="gray">top: 108 km/h, 60 m apart</text> <text x=${left} y="180" font-size="11" fill="gray">bottom: 54 km/h, 40 m apart</text>`: htl.svg`<text x=${right} y="16" font-size="11" text-anchor="end" fill="gray">top: 108 km/h, 60 m apart · bottom: 54 km/h, 40 m apart</text>`} </svg>`;}
md`<div class="grid"><div class="g-col-12 g-col-md-6 card-lite"><b>Loop detector</b> (one point, over time)<br><span style="font-size:2rem;font-weight:800;color:#c77700">${showdown.loopN? (showdown.loop*3.6).toFixed(0) +" km/h":"waiting…"}</span><br><small>${showdown.loopN} cars counted so far</small></div><div class="g-col-12 g-col-md-6 card-lite"><b>Drone</b> (the whole road, at an instant)<br><span style="font-size:2rem;font-weight:800;color:${accent}">${(showdown.drone*3.6).toFixed(0)} km/h</span><br><small>average of every car in view</small></div></div>`
Build your own road
Two lanes, with no lane changing. Set each lane’s speed and spacing, and see what each sensor reports.
viewof vA = Inputs.range([5,40], {value:30,step:1,label:"Lane A speed (m/s)"})viewof sA = Inputs.range([10,120], {value:60,step:5,label:"Lane A spacing (m)"})viewof vB = Inputs.range([5,40], {value:15,step:1,label:"Lane B speed (m/s)"})viewof sB = Inputs.range([10,120], {value:40,step:5,label:"Lane B spacing (m)"})
lanes = {const qA = vA / sA, qB = vB / sB;// flow = speed / spacing (veh/s)const kA =1/ sA, kB =1/ sB;// density = 1 / spacing (veh/m)const vt = (qA * vA + qB * vB) / (qA + qB);// flow-weighted: what a point sensor seesconst vs = (kA * vA + kB * vB) / (kA + kB);// density-weighted: what a snapshot seesconst varS = (kA * (vA - vs) **2+ kB * (vB - vs) **2) / (kA + kB);return {vt, vs, varS};}
{const Lr =500;const cars = [];for (let x =0; x < Lr; x += sA) cars.push({x,lane:"Lane A"});for (let x =0; x < Lr; x += sB) cars.push({x,lane:"Lane B"});return Plot.plot({width: plotW,height:!!document.querySelector(".reveal") ?110:140,marginLeft:60,x: {label:"A drone's view: 500 m of road",domain: [0, Lr]},y: {label:null},marks: [ Plot.ruleY(["Lane A","Lane B"], {stroke:"#eee",strokeWidth:16}), Plot.dot(cars, {x:"x",y:"lane",r:5,fill: d => d.lane==="Lane A"?"#2a9d8f":"#e76f51"}) ] });}
The loop counts fast cars more often, because fast cars pass it more often: it reports the time-mean speed. The drone counts slow cars more often, because slow cars spend longer on the road: it reports the space-mean speed. The two differ by the spread of speeds: \bar v_t = \bar v_s + \sigma_s^2/\bar v_s. Lecture 2 proves it, and shows why it matters for every travel-time sign.
theme = Generators.observe(notify => {const read = () =>notify(document.body.classList.contains("quarto-dark") ?"dark":"light");read();const watcher =newMutationObserver(read); watcher.observe(document.body, {attributes:true,attributeFilter: ["class"]});return () => watcher.disconnect();})accent = theme ==="dark"?"#c9a0ff":"#57068c"accentRange = theme ==="dark"? ["#4b3a5c","#ecdcff"] : ["#dccbee","#3a0463"]ink = theme ==="dark"?"#f2f2f2":"black"// Plot width: Plot's default 640 px, except on a phone, where a plot is drawn to fit its card// instead of being shrunk (and its text with it). Changes only when the width actually changes.plotW = Generators.observe(notify => {let last;const read = () => {const main =document.querySelector("main"), fit = main ? main.clientWidth-50:640;const w =!document.querySelector(".reveal") && fit <640?Math.max(240,Math.floor(fit)) :640;if (w !== last) notify(last = w); };read();window.addEventListener("resize", read);return () =>window.removeEventListener("resize", read);})