6 · The Red Light: Reading a Time-Space Diagram
Eight cars meet a red light: see a queue grow backward, and the start-up wave after the green
Eight cars arrive at a traffic light, which turns red at 16 s and green at 46 s. Press Play, or drag the time slider yourself. The road is on the left; its time-space diagram is on the right. Colour shows speed: red is slow, green is fast.
// Advance the time slider while Play is on.
tsClock = {
while (true) {
const play = viewof tsPlay, slider = viewof tsT;
if (play.value) {
slider.value = slider.value >= 90 ? 0 : Math.round((slider.value + 0.2) * 10) / 10;
slider.dispatchEvent(new Event("input", {bubbles: true}));
}
yield Promises.delay(40, htl.html`<span></span>`);
}
}// Eight cars follow the Intelligent Driver Model; during the red phase the stop line acts as a stopped vehicle.
tsSim = {
const v0 = 15, T = 1.2, s0 = 2, a = 1.5, b = 2, lc = 5, dt = 0.1, stop = 300, red = [16, 46], n = 8;
const enter = Array.from({length: n}, (_, i) => 3 * i);
const x = new Array(n).fill(null), v = new Array(n).fill(v0), rows = [];
const idm = (vi, gap, dv) => a * (1 - (vi / v0) ** 4 - ((s0 + Math.max(0, vi * T + vi * dv / (2 * Math.sqrt(a * b)))) / Math.max(gap, 0.1)) ** 2);
for (let k = 0; k <= 900; k++) {
const t = k * dt, isRed = t >= red[0] && t < red[1];
for (let i = 0; i < n; i++) if (x[i] === null && t >= enter[i]) x[i] = 0;
const acc = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
if (x[i] === null) continue;
let gap = Infinity, dv = 0;
if (i > 0 && x[i - 1] !== null) { gap = x[i - 1] - x[i] - lc; dv = v[i] - v[i - 1]; }
if (isRed && x[i] < stop && stop - x[i] - 1 < gap) { gap = stop - x[i] - 1; dv = v[i]; }
acc[i] = Math.max(-8, gap === Infinity ? a * (1 - (v[i] / v0) ** 4) : idm(v[i], gap, dv));
}
for (let i = 0; i < n; i++) if (x[i] !== null) { v[i] = Math.max(0, v[i] + acc[i] * dt); x[i] += v[i] * dt; }
if (k % 2 === 0) for (let i = 0; i < n; i++) if (x[i] !== null && x[i] <= 500) rows.push({t: +t.toFixed(1), car: i + 1, x: x[i], v: v[i]});
}
// headway at x = 100 m and spacing at t = 10 s, between cars 1 and 2
const pass = (car, x0) => rows.find(r => r.car === car && r.x >= x0).t;
const at = (car, t0) => rows.find(r => r.car === car && r.t >= t0).x;
return {rows, stop, red, loop: 150,
headway: {x: 100, t1: pass(1, 100), t2: pass(2, 100)},
spacing: {t: 10, x1: at(1, 10), x2: at(2, 10)}};
}tsNow = {
const t = Math.round(tsT * 5) / 5;
const cars = tsSim.rows.filter(r => Math.abs(r.t - t) < 0.05);
const isRed = t >= tsSim.red[0] && t < tsSim.red[1];
const passed = new Set(tsSim.rows.filter(r => r.t <= t && r.x >= tsSim.loop).map(r => r.car)).size;
return {t, cars, isRed, passed, stopped: cars.filter(c => c.v < 0.5).length};
}Plot.plot({
width: plotW < 640 ? 64 : 110, height: !!document.querySelector(".reveal") ? 300 : 400, marginLeft: plotW < 640 ? 34 : 40, marginTop: 20, marginBottom: 35,
x: {axis: null, domain: [0, 1]},
y: {domain: [0, 500], label: "Position (m)"},
marks: [
Plot.rect([0], {x1: 0.25, x2: 0.75, y1: 0, y2: 500, fill: "#8883"}),
Plot.ruleY([tsSim.stop], {x1: 0.15, x2: 0.85, stroke: tsNow.isRed ? "#d7263d" : "#2e9e5b", strokeWidth: 5}),
Plot.rect(tsNow.cars, {x1: 0.32, x2: 0.68, y1: d => d.x - 5, y2: "x",
fill: d => d3.interpolateRdYlGn(Math.min(d.v / 15, 1)), stroke: "#333"})
]
})Plot.plot({
width: plotW < 640 ? plotW - 64 - 24 : 640, height: !!document.querySelector(".reveal") ? 300 : 400, marginTop: 20, marginBottom: 35,
x: {label: "Time (s)", domain: [0, 90]},
y: {label: null, domain: [0, 500]},
color: {type: "sequential", scheme: "RdYlGn", domain: [0, 15]},
marks: [
Plot.rect([0], {x1: tsSim.red[0], x2: tsSim.red[1], y1: tsSim.stop - 3, y2: tsSim.stop + 3, fill: "#d7263d"}),
Plot.ruleY([tsSim.loop], {stroke: "#c77700", strokeDasharray: "6 4"}),
Plot.text([0], {x: 89, y: tsSim.loop, text: () => "loop detector", dy: -7, textAnchor: "end", fill: "#c77700"}),
Plot.line(tsSim.rows, {x: "t", y: "x", z: "car", stroke: "v", strokeWidth: 2.2}),
Plot.arrow([tsSim.headway], {x1: "t1", x2: "t2", y1: "x", y2: "x", stroke: ink, headLength: 6, bend: false}),
Plot.text([tsSim.headway], {x: d => (d.t1 + d.t2) / 2, y: "x", text: () => "headway", dy: 12}),
Plot.arrow([tsSim.spacing], {x1: "t", x2: "t", y1: "x2", y2: "x1", stroke: ink, headLength: 6}),
Plot.text([tsSim.spacing], {x: "t", y: d => (d.x1 + d.x2) / 2, text: () => "spacing", dx: -6, textAnchor: "end"}),
Plot.ruleX([tsNow.t], {stroke: accent, strokeWidth: 2}),
Plot.text([tsNow.t], {x: d => d, y: 490, text: () => "drone snapshot", dx: 6, textAnchor: "start", fill: accent}),
Plot.dot(tsNow.cars, {x: "t", y: "x", r: 5, fill: ink})
]
})What it shows
Time runs across, position runs up. Flat lines are stopped cars. Watch where the queue’s back end goes during the red, and how the green reaches each car. Lecture 1 explains how to read it; Lecture 5 computes those speeds by hand.
theme = Generators.observe(notify => {
const read = () => notify(document.body.classList.contains("quarto-dark") ? "dark" : "light");
read();
const watcher = new MutationObserver(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);
})