"Each of my sensors swears it knows the time. The trouble is that no two of them agree, and none of them will admit it."
A Multi-Sensor AI Agent
Why a whole section on telling time
The moment you have more than one sensor, a new quantity appears that neither of them measures directly: the shared instant. A camera frame and a lidar sweep can each be flawless and still be useless together if you cannot say which frame and which sweep saw the world at the same time. Every fusion result, every state estimate, every "the accelerometer spiked when the microphone heard the impact" claim rests on a timebase you have to construct, because the hardware does not hand it to you for free. This section is about that construction: what a clock actually is, where a timestamp really comes from, how far apart independent clocks drift, and the concrete mechanisms (from Global Positioning System (GPS) pulses to IEEE 1588) that pull many sensors onto one timeline. Get this wrong and the errors are not noisy; they are systematic, and they masquerade as everything from bad calibration to a broken model.
Section 3.1 treated a single stream sampled on a single clock, and Section 3.2 contrasted clock-driven with event-driven acquisition. This section assumes the readings exist and asks the question fusion forces: how do we place samples from independent devices on a common axis? This presumes a little probability vocabulary for talking about timing error, which the Chapter 4 uncertainty primer supplies, and it feeds directly into Chapter 48, where misalignment becomes fusion error you can measure.
What a clock actually is: offset, skew, and drift
Get the clock model wrong and no downstream fix can rescue you: a rig of perfectly good sensors will smear its own data the moment one counter runs a hair fast, and the damage looks exactly like the bad calibration you will chase for weeks before suspecting time. A sensor's clock is a counter driven by an oscillator, usually a quartz crystal, that ticks at a nominal frequency. It is never exactly right. We model device \(i\)'s reported time \(C_i\) as an affine function (a straight-line map: a constant scaling of the input plus a constant shift) of true time \(t\):
$$C_i(t) = (1 + \epsilon_i)\,t + \theta_i.$$Here \(\theta_i\) is the offset (how far the clock is ahead or behind at \(t=0\)) and \(\epsilon_i\) is the skew (its fractional rate error, the reason it gains or loses time). Skew is quoted in parts per million (ppm): a garden-variety consumer crystal is rated at \(\pm 20\) to \(\pm 50\) ppm. That sounds negligible until you unpack the unit. One ppm is one microsecond of error per second, which is about \(86\ \mathrm{ms}\) per day. A 50 ppm crystal can therefore drift more than four seconds in a single day if left uncorrected. Two independent 50 ppm clocks can move apart at up to 100 ppm relative to each other. Drift is the slow change of \(\epsilon_i\) itself, driven mostly by temperature. The same crystal that reads 20 ppm on a bench can swing tens of ppm as a device warms in a pocket or a bearing housing.
The practical takeaway is that offset and skew are not one-time nuisances you subtract once. Offset must be re-estimated because clocks are set imperfectly; skew must be re-estimated because a fixed rate error accumulates without bound; and drift means both estimates go stale, so synchronization is a process you run continuously, not a calibration you do at the factory. In short: a shared timebase is never measured, only estimated and re-estimated, because every clock you own is quietly lying at its own rate.
Mental Model
Picture two cheap kitchen wall clocks. If you hang one reading 3:00 next to another reading 3:05, that constant five-minute gap is offset: subtract it once and they line up. But suppose one clock's motor runs slightly fast, gaining a minute a day; now no single subtraction keeps them matched, because the gap widens steadily, and that growing gap is skew, a rate error rather than a fixed one. And if the fast clock hangs above a radiator so it gains more on hot days than cold ones, the rate itself wanders from day to day: that is drift, and it is exactly why you cannot correct the pair once and walk away.
Alignment is a linear-map estimation problem
Putting sensor \(i\) onto a reference timeline means estimating two numbers, \(\theta_i\) and \(\epsilon_i\), that map \(C_i\) back to \(t\). That is the entire mathematical content of clock synchronization at this level: fit a line. Everything harder (PTP, GPS discipline, cross-correlation) is a better way to gather the paired timestamps you fit that line to, or a way to re-fit it as it drifts.
Where a timestamp really comes from
Fitting that line assumes the paired timestamps you feed it are trustworthy, which raises the prior question of where a timestamp actually comes from. A single sensor sample can carry several different timestamps, and confusing them is among the most common synchronization bugs in practice. A reading acquires its capture time when the physical event is transduced, a hardware timestamp if the sensor's own clock latches the sample, an arrival timestamp when the packet reaches the host, and a host timestamp when your software finally reads it off a queue. Between capture and host there is exposure time, bus transfer, driver buffering, and operating-system (OS) scheduling, and each adds a delay that is partly fixed and partly random. The fixed part is latency; the random part is jitter, treated as a failure mode in its own right in Section 3.4. Figure 3.3.1 illustrates the lifecycle of a single sample's timestamps: capture, hardware, arrival, and host times, and the latency and jitter accumulating between them.
Checkpoint
So far: one sample can carry several distinct timestamps (capture, hardware, arrival, host), and the delay between the physical event and the moment your software reads the sample splits into a fixed part (latency) and a random part (jitter).
The rule that saves projects is: timestamp as close to the physical event as the hardware allows, and record which clock did it. A camera that exposes a hardware timestamp at the middle of its integration window is worth far more than one you stamp when the Universal Serial Bus (USB) frame lands, because the arrival time smears the true capture instant by a variable few milliseconds. On the software side, always separate the monotonic clock (a counter that only ever increases, immune to being reset) from the wall clock (calendar time, which can jump backward when NTP (the Network Time Protocol, detailed below) corrects it or a leap second (an occasional one-second adjustment that keeps UTC in step with Earth's rotation) lands). Use the monotonic clock to measure durations and align streams; use the wall clock only to label data with human-readable dates. Mixing them produces negative time intervals and reversed sample orders that are maddening to debug.
Real-World Application: a 40-millisecond offset that bent the road
An autonomous-driving team projected lidar points into camera images to color the point cloud and to fuse detections. On straight highway their bounding boxes were crisp; on cloverleaf on-ramps they smeared by a full car width, and a validation engineer nearly filed it as a calibration defect. The cause was timing, not geometry. The lidar was hardware-timestamped from a GPS-disciplined clock, but the camera frames were stamped on arrival at the host, roughly 40 ms late and jittery. At highway speed a car covers about \(1.1\ \mathrm{m}\) in 40 ms, so on a curve the vehicle had rotated measurably between the true camera instant and the assumed one, shearing the projection. No amount of re-running the extrinsic calibration (the fixed geometric transform relating the two sensors' coordinate frames) could fix it, because the extrinsics were correct; the two sensors were simply describing different instants. The remedy lived entirely in the timing layer: a hardware trigger that exposed the camera and latched the lidar angle together, plus interpolation of vehicle pose to the exact stamp.
Mechanisms that pull sensors onto one clock
That road-bending failure was cured in the timing layer with a hardware trigger, but such heavy machinery is overkill when sensors need only loose agreement, so it helps to survey the full range of options before committing to one. There is a ladder of synchronization techniques, and the right rung depends on how tightly your sensors must agree. Figure 3.3.1 lays out that ladder from loosest to tightest, pairing each mechanism with the order of agreement it buys. From loosest to tightest:
Network Time Protocol (NTP) disciplines a host's wall clock over an ordinary network to within a few milliseconds on a local area network (LAN), tens of milliseconds over the internet. It is enough to correlate slow logs and timestamp events for humans, and nowhere near enough to fuse a 100 Hz inertial measurement unit (IMU) with a camera. Precision Time Protocol (PTP, IEEE 1588) uses hardware timestamping in the network interface to exchange delay-measurement messages and reach sub-microsecond agreement across devices on the same switched network; it is a backbone of many synchronized industrial and automotive sensor buses. GPS with a pulse-per-second (PPS) signal gives every receiver a physical electrical edge aligned to Coordinated Universal Time (UTC) to within tens of nanoseconds, which is why GPS-disciplined clocks anchor large outdoor multi-sensor rigs and are covered further in Chapter 25. A hardware trigger is the tightest of all: one electrical line fires multiple sensors at the same instant, so they share not just a clock but a common exposure event, eliminating offset by construction.
NTP deserves a closer look, because you inherit it whether or not you plan for it: it runs on essentially every networked machine, so its accuracy is the clock quality you start from. The protocol repeatedly asks a set of time servers for the current time, records four timestamps per exchange (send and receive at each end), and slews (gradually adjusts the rate rather than jumping) the local wall clock to minimize the estimated offset. Assuming a symmetric network path, it splits the round trip to recover one-way delay and offset. Reach for NTP when a few milliseconds over a routed network suffices (correlating logs, tagging coarse events); step up to PTP or a hardware trigger the moment fusion needs sub-millisecond alignment, because NTP's symmetry assumption breaks down exactly where tight fusion lives.
Step-Through: the NTP four-timestamp offset calculation
NTP estimates offset and delay from four timestamps recorded during one request-reply exchange: \(T_1\) when the client sends, \(T_2\) when the server receives, \(T_3\) when the server replies, and \(T_4\) when the client receives the reply. Suppose the client's clock is actually running behind the server, and one exchange yields (in seconds):
- \(T_1 = 100.000\) (client send)
- \(T_2 = 100.023\) (server receive)
- \(T_3 = 100.024\) (server send)
- \(T_4 = 100.031\) (client receive)
Round-trip delay: \(\delta = (T_4 - T_1) - (T_3 - T_2) = (100.031 - 100.000) - (100.024 - 100.023) = 0.031 - 0.001 = 0.030\), so 30 ms was spent on the wire and 1 ms inside the server. Clock offset, assuming the path is symmetric so each direction took \(\delta/2 = 15\) ms:
$$\theta = \frac{(T_2 - T_1) + (T_3 - T_4)}{2} = \frac{(0.023) + (-0.007)}{2} = \frac{0.016}{2} = 0.008.$$The client clock is 8 ms behind the server, so NTP slews it forward by 8 ms. Now watch the symmetry assumption break: if the reply path were secretly 10 ms slower than the request path, the same arithmetic still splits \(\delta\) evenly and reports the wrong offset by 5 ms, with nothing in the four numbers to reveal the asymmetry. That single hidden bias is exactly why NTP tops out at millisecond accuracy and why tight fusion reaches for PTP or a hardware trigger instead.
When you cannot rewire the hardware, you fall back to post-hoc software alignment: record the best timestamps you have, then estimate the residual offset from the data itself. If two sensors observe the same physical event (a shake felt by two IMUs, an audio-visual clap), the lag that maximizes their cross-correlation (a measure of how well two signals overlap as one is slid in time past the other) is a direct estimate of \(\theta_i - \theta_j\). This is cheap, requires no special hardware, and is the workhorse for aligning consumer devices that were never designed to be synchronized. It cannot fix skew on its own, though, so for long recordings you fit the full affine map, not just a constant shift.
Research Frontier
The two-device cross-correlation trick scales surprisingly far. The EgoExo4D dataset (Grauman et al., CVPR 2024) had to place hundreds of hours of simultaneously recorded egocentric and third-person cameras on one timeline without wiring them together, and it does so by cross-correlating the audio each device captured, turning a shared sound into the synchronizing event for dozens of independent clocks at once. This pushes past what the section teaches in two directions: the alignment is estimated at data-collection scale across many devices rather than a single pair, and it is validated to frame-level agreement so the recordings can train video models that assume tight temporal correspondence. The open frontier is doing this continuously and skew-aware for streams whose only common signal is noisy ambient audio, rather than as a one-shot offset per clip.
import numpy as np
# Paired timestamps: each row is (sensor_clock_reading, reference_clock_reading)
# gathered whenever a shared event let us observe both clocks at once.
sensor = np.array([0.001, 1.002, 2.004, 3.005, 4.007, 5.008]) # device clock (s)
ref = np.array([0.000, 1.000, 2.000, 3.000, 4.000, 5.000]) # reference clock (s)
# Fit ref = a * sensor + b (a undoes skew, b undoes offset)
A = np.vstack([sensor, np.ones_like(sensor)]).T
(a, b), *_ = np.linalg.lstsq(A, ref, rcond=None)
skew_ppm = (a - 1.0) * 1e6
print(f"skew = {skew_ppm:6.1f} ppm, offset = {b*1e3:6.2f} ms")
# Map any future device stamp onto the reference timeline:
def to_reference(device_stamp):
return a * device_stamp + b
a recovers skew (here about a 1500 ppm rate error) and the intercept b recovers offset, giving a single to_reference function that re-times every later sample.The code above is the whole idea in a dozen lines: gather pairs of readings taken at the same real instant, fit a line, and reuse it. In production you re-fit this map on a sliding window so it tracks drift rather than freezing a stale skew estimate.
Let a library do the nearest-timestamp join
Once every stream is on a common timeline, you still have to pair samples that never share an exact stamp: a 100 Hz IMU and a 30 Hz camera land on different grids. Writing a correct tolerance-bounded nearest-neighbor join by hand (two moving pointers, boundary handling, a maximum-gap guard) is easily 40 or more lines and a magnet for off-by-one bugs. pandas.merge_asof collapses it to one call:
import pandas as pd
# imu, cam: DataFrames sorted by a common-timeline column "t"
aligned = pd.merge_asof(cam, imu, on="t",
direction="nearest",
tolerance=0.005) # drop pairs > 5 ms apart
merge_asof attaches to each camera frame the temporally nearest IMU sample and refuses any match more than 5 ms away, replacing a hand-written two-pointer join.The library handles the sorted-merge, the direction choice (nearest, backward, or forward), and the tolerance gate that marks unmatched samples as missing rather than silently pairing distant ones. For robot stacks, the Robot Operating System (ROS) message_filters ApproximateTimeSynchronizer does the same job live on message streams.
The timestamp contract: what to record so alignment stays possible
Synchronization is only recoverable if you preserve the information it needs. This is a data-engineering discipline as much as a signal one, revisited under leakage-safe pipelines in Chapter 5. Every sample should carry: the timestamp value, the clock domain it came from (monotonic versus wall, and which device), and ideally the stamping stage (capture, hardware, or arrival). Store timestamps at nanosecond resolution in integer counts, not 32-bit floats, because a float32 second-count loses millisecond precision within an hour of uptime. Never overwrite a hardware timestamp with a host one to "clean up" the record; you are destroying the very evidence a later alignment step depends on. If you log the delay-measurement exchanges from PTP or the PPS edges from GPS, you can reconstruct and audit the timebase after the fact. That is the difference between a dataset that can be re-synchronized and one that is permanently smeared.
The clocks in your GPS receiver tick in a different flow of time
The atomic clocks aboard GPS satellites do not keep the same time as identical clocks on the ground, and not because of any oscillator flaw. Special relativity slows the orbiting clocks by about 7 microseconds per day because they move at roughly 14,000 km/h, while general relativity speeds them up by about 45 microseconds per day because they sit higher in Earth's weaker gravity. The two effects do not cancel: the net result is that each satellite clock runs about 38 microseconds per day fast. Left uncorrected, that error would corrupt position fixes by roughly 10 km within a single day, so every GPS satellite is deliberately built to tick at a slightly wrong frequency on the ground, tuned so that once in orbit it reads correctly. When you use a GPS PPS edge as the master timebase for a sensor rig, you are anchoring your whole timeline to hardware that only tells the right time because engineers pre-compensated it for Einstein.
Common Misconception
"The host receive time is good enough; the network is fast." The network may be fast on average, but arrival timestamps inherit every millisecond of bus contention, driver buffering, and OS scheduling jitter, and that jitter is exactly the quantity fusion is most sensitive to. A mean latency you could subtract is harmless; the variance around it is not, and no averaging removes it after the fact. The fix is to stamp earlier (in hardware), not to hope the arrival time is stable.
Exercise: budget the drift and choose a mechanism
You are building a wearable that fuses a 200 Hz IMU with a 25 Hz photoplethysmography (PPG) optical sensor, each on its own \(\pm 30\) ppm crystal, and your fusion step tolerates at most \(2\ \mathrm{ms}\) of relative misalignment. (1) If you synchronize the two clocks once at power-on and never again, how long until worst-case relative drift exceeds the 2 ms budget? (2) Would NTP-class (few-ms) synchronization ever satisfy this budget, and why or why not? (3) Propose a re-synchronization schedule or mechanism from the ladder in this section that keeps you inside 2 ms, and state what timestamp fields each sample must carry for your scheme to be auditable later.
Self-check
- Write the affine clock model and say, in one sentence each, what offset, skew, and drift are and why estimating one does not remove the need to re-estimate the others.
- Distinguish a hardware timestamp, an arrival timestamp, and a host timestamp, and explain why fusing on arrival timestamps was the root cause of the smeared lidar-camera projection in the worked example.
- You have two consumer phones that were never designed to be synchronized. Which mechanism from this section lets you align their recordings after the fact, what shared signal does it need, and what can it not correct on its own?
Try It: recover a hidden clock skew from paired timestamps
You can reproduce the whole affine-alignment idea on a laptop in a few minutes with only numpy and matplotlib.
- Build a reference timeline:
ref = np.arange(0, 60, 0.5)(two samples per second for a minute). - Manufacture a "device" clock with a skew and offset you pick, plus a little noise: choose
true_ppm = 120andtrue_offset = 0.030, then computedevice = (1 + true_ppm/1e6) * ref + true_offset + np.random.normal(0, 5e-4, ref.shape). - Pretend you only kept a handful of paired readings by slicing every eighth sample (
device[::8],ref[::8]), stack them withnp.vstack([dev_pairs, np.ones_like(dev_pairs)]).T, and solve for slope and intercept withnp.linalg.lstsq. - Convert the recovered slope to ppm with
(a - 1) * 1e6and print it next totrue_ppm; the two should match to a few ppm despite the noise. - Now break the symmetry assumption: add a fixed
0.010second latency to only the second half of the device samples before fitting, refit, and plot the residuals(a*device + b) - ref. Watch the residuals split into two levels, which is the fingerprint of an unmodeled step in the timing path.
Step 5 is the lesson that outlasts the code: a clean line-fit hides nothing, but the residual plot exposes the exact moment your affine model stops describing the clock.
Lab: align a real IMU and camera on the EuRoC MAV dataset
Goal (15 to 30 minutes): take two genuinely independent sensor streams recorded on a flying drone, place them on one timeline, and measure how alignment quality collapses as you widen the matching tolerance and inject artificial skew.
Tools needed: Python with pandas and numpy; one sequence from the ETH EuRoC MAV dataset (for example MH_01_easy), which ships hardware-synchronized imu0/data.csv (200 Hz) and cam0/data.csv (20 Hz) files whose first column is a nanosecond integer timestamp.
Steps:
- Load both comma-separated value (CSV) files, convert the nanosecond timestamp column to floating seconds, and confirm the nominal rates by differencing consecutive stamps (you should see roughly 5 ms for the IMU, 50 ms for the camera).
- Use
pandas.merge_asof(cam, imu, on="t", direction="nearest", tolerance=tol)to attach the nearest IMU sample to each camera frame. Record, for the matched rows, the actual timing gapabs(cam.t - matched_imu.t). - Vary the
toleranceacross0.0005, 0.001, 0.0025, 0.005seconds and observe both the fraction of camera frames that find a match and the distribution (mean and max) of the residual timing gap. - Inject skew: multiply the IMU timestamps by
1 + 150e-6(a 150 ppm rate error) before merging, so the two clocks now diverge over the sequence. Re-run the join and plot the residual gap against time.
What to observe: with true hardware sync the residual gap stays under about 2.5 ms and nearly every frame matches; a fixed tolerance cannot fix injected skew, because the residual grows linearly along the recording until late frames fall outside the tolerance and silently drop out. That growing, one-sided residual is the visual signature of an unmodeled skew, and it is why long recordings need the sliding-window affine re-fit from earlier in this section rather than a single nearest-neighbor join.
What's Next
Section 3.4 confronts what happens when the timeline you just built develops holes: samples that never arrive, timestamps that jitter around their true instant, and packets lost in transit. Clean synchronization assumes every sample is present and stamped; the next section drops that assumption and shows how to detect, mark, and reason about the gaps without letting them poison a downstream model.