Between two trusted compass fixes, Wheria's heading runs on gyroscope alone. Indoors the magnetometer gets vetoed a lot: a steel column 2 m away can rotate apparent north by 40–90°, which is why the compass post treats that sensor as guilty until proven innocent. While the veto lasts, the filter integrates angular rate and nothing else. So the load-bearing question for the whole heading pipeline is how many seconds that integral stays inside the error the UI can absorb.
Inertial navigation people answer that with Allan variance, a tool that needs nothing more exotic than a phone lying still for a long time. In July we gave it 5 phones and a week of nights: stationary gyro logs, 7–9 h each, on a granite tile in the studio. What follows is the bench notes — the three noise types, the computation, the log-log plot, our numbers, and which knobs they pin down in Wheria's filter.
Three noises live in a resting gyro
A phone gyro on a table should read zero. It actually reads Earth's rotation, 15 deg/h, plus everything the silicon adds on top. The best of our five phones has a bias floor of 16 deg/h, so Earth loses by a nose and phone gyrocompassing remains cancelled.
The first contribution is white noise on the rate signal: thermal and electronic noise in the MEMS sense loop, uncorrelated from sample to sample. Integrate white rate noise and the angle performs a random walk whose standard deviation grows as √t. Datasheets call it angle random walk, N, quoted in deg/√h. It dominates over seconds.
The second is uglier. The zero offset itself wanders, slowly, with 1/f flicker character. Calibrate the bias perfectly at 21:00 and by 21:05 it is a slightly different bias. This is bias instability, B, quoted in deg/h, and no amount of averaging removes it, because averaging longer just averages a moving target. It dominates over minutes. A garage walk is minutes.
Third, rate random walk: a random walk on the rate itself, K, quoted in deg/h per √h. Under it the angle error grows as t^1.5. On consumer parts a clean K is rare, because temperature ramps print the same signature on the curve.
The Allan deviation recipe
The computation dates to 1966 and is pleasantly mechanical. Chop the log into adjacent clusters of duration τ. Average the rate inside each cluster to get Ω̄_1, Ω̄_2, … Then measure how much successive averages disagree: σ²(τ) = ½·⟨(Ω̄_{k+1} − Ω̄_k)²⟩. Repeat for many values of τ and plot σ(τ) against τ on log-log axes.
The factor ½ is what makes the slopes meaningful. For white rate noise with variance σ_w² per sample, a cluster mean over m samples has variance σ_w²/m. Two adjacent means are independent, so their difference carries 2σ_w²/m, and halving recovers σ_w²/m. With m = τ·f_s that gives σ(τ) = N/√τ, a straight line of slope −1/2 on the plot. Each noise type plays the same game with a different exponent, which is the entire reason the curve is readable.
Cluster times run log-spaced from 2 samples up to about a tenth of the record, 60 points or so. The tenth matters. At τ = 1,000 s an 8 h log holds only 28 non-overlapping clusters, and the relative error of an Allan point is roughly 1/√K for K cluster pairs, so the right-hand end of every published Allan curve is mush. Overlapping clusters, stride 1 sample instead of stride m, squeeze extra confidence out of the same record for nothing but CPU. This is also why the logs run overnight. A 10 min log supports τ up to about 60 s and says nothing about the floor.
Two platform traps. Log the raw stream (on Android the uncalibrated gyro sensor, on iOS the plain gyro data rather than the fused device-motion rate), because runtime bias compensation quietly edits the signal mid-record and stamps steps into the curve. And resample to a uniform grid first; both OSes deliver jittered timestamps, and the fixed 100 Hz resampling stage from step detection serves here unchanged. Charging during a run is banned. A warming battery is a bias ramp.
The computation in 16 lines
With prefix sums every cluster mean costs O(1), and the full curve computes in seconds on a laptop, overlapped version included:
// w[0..N-1]: gyro rate samples (deg/s), fs: sample rate (Hz)
S[0] = 0
for i in 0..N-1:
S[i+1] = S[i] + w[i] // prefix sums
for tau in logspace(2/fs, (N/fs)/10, 60):
m = round(tau * fs) // samples per cluster
if m < 2 or 2*m >= N: continue
acc = 0; K = 0
for k in 0 .. N - 2*m: // overlapping clusters
a = (S[k+m] - S[k]) / m // mean of cluster at k
b = (S[k+2*m] - S[k+m]) / m // mean of the next one
acc = acc + (b - a)²
K = K + 1
adev[tau] = sqrt(acc / (2·K)) // Allan deviation at tau
Feed it deg/s. The point at τ = 1 s, multiplied by 60, is N in deg/√h. The flat minimum, times 3,600 and divided by 0.664, is B in deg/h; the 0.664 is √(2·ln2/π) from the flicker-noise derivation, one of those constants you verify once and then trust forever.
Reading the slopes
The finished plot reads by pattern. Left side, slope −1/2: the white-noise region, ARW, with N read directly at τ = 1 s. Middle: the curve flattens where flicker takes over, and the minimum marks bias instability. Right side, slope +1/2: rate random walk, following σ(τ) = K·√(τ/3). A slope of −1 at the very shortest τ is quantization noise from the sensor's output resolution and is harmless.
Bumps are the informative part. A periodic disturbance with period T0 prints an oscillation on the curve whose first hump lands near τ ≈ T0/2. On our first night we left the aircon running, and all three phones logged that evening grew a hump at τ ≈ 500 s; the compressor cycle measured about 17 min. The gyros were faithfully reporting the building. Aircon off every night after.
One week, five phones
Bench setup, for the record: 5 phones (two iPhones, one Pixel-class Android, two mid-range Androids, model years 2019–2025), each logged 7–9 h overnight at 100 Hz on a 30 mm granite tile — airplane mode, screen off, unplugged, studio door locked. 8 h at 100 Hz is 2.9 million samples per axis, about 35 MB of float32 per phone.
Z-axis numbers, since yaw is what heading uses. ARW ran from 0.15 deg/√h on the newest flagship to 0.9 deg/√h on the 2019 mid-ranger, with the other three between 0.2 and 0.6. Bias instability came out at 16, 19, 24, 31 and 44 deg/h respectively, floors sitting between τ = 80 s and τ = 400 s. Only the two oldest units showed a credible +1/2 tail before the error bars went to mush, and on one of them the tail shrank when we re-ran the log inside a foam box; part of it was thermal, since the studio cools by about 2 °C overnight. Everything lands in the class that MEMS datasheets admit to when read carefully.
Now translate to angle. N = 0.3 deg/√h is 0.005 deg/√s; after 60 s of open-loop integration that is 0.04° of heading error, and after a 4 min garage walk, 0.08°. White noise is irrelevant at our timescales. The floor is what bites. B = 20 deg/h is 0.33 deg/min, and it is a floor in the strict sense: the drift that survives any bias calibration, however careful, because flicker is precisely the part left over once averaging stops helping.
What Wheria does with the numbers
The Kalman post describes the state vector: position x and y, heading θ, and a slowly drifting gyro bias term. The Allan curve is the datasheet for that fourth state. Its process noise is set from the measured floor and tail, so the bias covariance grows at roughly the instability rate; after about 5 min without a correction the filter has honestly forgotten the old bias, back down to the floor of 0.3–0.7 deg/min depending on the phone. Corrections come from moments when the compass is trusted and from near-still stretches, the same low-turn-rate windows the compass post uses for magnetometer calibration.
The curve also sets the gyro-only clock. Stationary, our mid-pack phones drift 0.3–0.7 deg/min once the bias state has converged. Walking is worse, for reasons in the next section, and the 1–3 deg/min quoted in the compass post is the honest in-pocket figure. Find mode drops from arrow to distance-first guidance once heading standard deviation crosses 25°. At 3 deg/min that limit is 8 min away; the filter flips much earlier, because its heading process noise (about 2° per step under compass trust, nearer 8° in steel-heavy zones) is deliberately fatter than the measured drift. The covariance is an admission of what the filter cannot verify, not a measurement of what the gyro just did.
The practical outcome: between magnetometer corrections a mid-2020s phone holds heading to 1–2° across the 30–90 s the filter typically has to bridge in a steel-heavy aisle, and the oldest phone in our test drawer holds 3–5°. Standing 20 m from the car, a 5° heading error displaces the arrow's target about 1.7 m sideways, still narrower than one parking space. That margin is why Wheria survives minutes of vetoed compass without drawing a confident lie.
Where the stationary curve stops being honest
An overnight log on a stone tile is the gyro on its best behavior. A trouser pocket degrades it three ways. Linear-acceleration sensitivity first: consumer gyros register 0.01–0.1 deg/s per g of applied acceleration, and walking applies a ±0.4 g bounce at roughly 2 Hz, part of which rectifies into an apparent bias. Then temperature: leaving 22 °C garage air for a 32 °C pocket drives the bias tempco through a ramp the Allan curve never saw. And the phone rotates relative to the walker, fabric and soft tissue conspiring, motion that must never be booked as heading. The stationary curve is a physical lower bound. Every tuning number in Wheria sits above it on purpose.
To run the experiment yourself, the internal Phyzix build we are testing adds an overnight Allan-log mode to the lab screens, curve and fitted slopes included; the resampler and the logging plumbing already existed from step counting. Until that ships, the 16 lines above plus one boring night are genuinely all it takes.
Giữa hai lần la bàn được tin, heading của Wheria chạy bằng mỗi con quay hồi chuyển. Trong hầm, từ kế bị phủ quyết thường xuyên: một cột thép cách 2 m đủ xoay hướng bắc biểu kiến 40–90°, chuyện đã kể kỹ trong bài la bàn. Suốt lúc bị phủ quyết, bộ lọc chỉ còn cách tích phân tốc độ góc. Câu hỏi gánh cả pipeline nằm ngay đó: tích phân ấy trụ được bao nhiêu giây trước khi sai số vượt định mức mà UI chịu nổi.
Dân định vị quán tính trả lời bằng phương sai Allan. Dụng cụ cần đúng một chiếc điện thoại nằm im thật lâu. Tháng 7 vừa rồi mình cho 5 máy thay nhau nằm: log gyro qua đêm, mỗi máy 7–9 tiếng, đặt trên tấm đá granite trong studio. Dưới đây là bench notes: ba loại nhiễu, cách tính, đồ thị log-log, số đo thực tế và mấy nút vặn chúng chốt lại trong bộ lọc của Wheria.
Ba loại nhiễu trong một con quay nằm im
Gyro đặt trên bàn lẽ ra phải đọc 0. Thực tế nó đọc chuyển động quay của Trái Đất, 15 độ/giờ, cộng mọi thứ silicon bồi thêm. Máy tốt nhất trong 5 máy có sàn bias 16 độ/giờ, tức Trái Đất thua sát nút. Trò gyrocompass bằng điện thoại vẫn bị hủy.
Loại thứ nhất: white noise trên tín hiệu rate. Nguồn là nhiễu nhiệt với nhiễu điện tử trong mạch cảm biến MEMS, sample này chẳng dính gì sample kia. Tích phân white noise thì góc đi random walk, độ lệch chuẩn phình theo √t. Datasheet gọi là angle random walk, ký hiệu N, đơn vị độ/√h. Nó thống trị thang vài giây.
Loại thứ hai khó chịu hơn. Bản thân điểm 0 cũng trôi, chậm rãi, theo kiểu flicker 1/f. Calibrate bias thật chuẩn lúc 21:00 thì tới 21:05 đã là một bias hơi khác. Đây là bias instability, ký hiệu B, đơn vị độ/giờ. Lấy trung bình lâu hơn cũng vô ích, vì mục tiêu tự bò đi mất. Nó thống trị thang vài phút. Một cuốc tìm xe trong hầm cũng dài cỡ vài phút.
Loại thứ ba: rate random walk, tức random walk trên chính rate, ký hiệu K, đơn vị độ/giờ trên √h. Dưới nó sai số góc phình theo t^1.5. Trên chip phổ thông hiếm khi đo K cho sạch, vì dốc nhiệt độ in lên đường cong đúng chữ ký đó.
Công thức Allan deviation
Phép tính có từ 1966, thuần cơ học. Cắt log thành các cluster liền nhau dài τ. Lấy trung bình rate trong từng cluster, được Ω̄_1, Ω̄_2, … Rồi đo mức lệch giữa hai trung bình kế nhau: σ²(τ) = ½·⟨(Ω̄_{k+1} − Ω̄_k)²⟩. Lặp cho nhiều giá trị τ, vẽ σ(τ) theo τ trên trục log-log.
Hệ số ½ chính là chỗ khiến slope có nghĩa. White noise phương sai σ_w² mỗi sample, trung bình qua m sample còn σ_w²/m. Hai trung bình kề nhau độc lập, hiệu của chúng mang 2σ_w²/m, chia đôi trả về đúng σ_w²/m. Với m = τ·f_s, ra σ(τ) = N/√τ, một đường thẳng slope −1/2 trên log-log. Mỗi loại nhiễu chơi đúng trò này với số mũ khác nhau, nhờ vậy đường cong mới đọc được.
τ chạy log-spaced từ 2 sample đến chừng 1/10 độ dài bản ghi, khoảng 60 điểm. Con số 1/10 quan trọng. Ở τ = 1.000 s, log 8 tiếng chỉ chứa 28 cluster không chồng nhau, mà sai số tương đối của một điểm Allan cỡ 1/√K với K cặp cluster, nên đuôi phải của mọi đường Allan từng công bố đều nhão. Dùng cluster chồng nhau, bước nhảy 1 sample thay vì m, vắt thêm độ tin cậy từ cùng bản ghi mà chỉ tốn CPU. Cũng vì thế phải log qua đêm. Log 10 phút chỉ đỡ nổi τ tới chừng 60 s, chưa với tới sàn.
Hai cái bẫy nền tảng. Một, ghi stream thô (Android là sensor gyro uncalibrated, iOS là dữ liệu gyro trần thay cho rate đã fusion trong device motion), vì trình bù bias lúc chạy sẽ lặng lẽ sửa tín hiệu giữa chừng, in bậc thang lên đường cong. Hai, resample về lưới đều trước, timestamp trên cả hai OS đều rung; tầng resample 100 Hz sẵn từ bài đếm bước dùng lại nguyên xi. Cấm sạc khi đang log. Pin ấm lên là một dốc bias.
Phép tính trong 16 dòng
Có prefix sum thì mỗi trung bình cluster tốn O(1), cả đường cong chạy vài giây trên laptop, kể cả bản chồng nhau:
// w[0..N-1]: gyro rate samples (deg/s), fs: sample rate (Hz)
S[0] = 0
for i in 0..N-1:
S[i+1] = S[i] + w[i] // prefix sums
for tau in logspace(2/fs, (N/fs)/10, 60):
m = round(tau * fs) // samples per cluster
if m < 2 or 2*m >= N: continue
acc = 0; K = 0
for k in 0 .. N - 2*m: // overlapping clusters
a = (S[k+m] - S[k]) / m // mean of cluster at k
b = (S[k+2*m] - S[k+m]) / m // mean of the next one
acc = acc + (b - a)²
K = K + 1
adev[tau] = sqrt(acc / (2·K)) // Allan deviation at tau
Cho vào độ/s. Điểm tại τ = 1 s nhân 60 ra N theo độ/√h. Đáy phẳng nhân 3.600 rồi chia 0,664 ra B theo độ/giờ; hằng số 0,664 là √(2·ln2/π) từ phép tính flicker noise, kiểm chứng một lần rồi tin cả đời.
Đọc slope trên đồ thị log-log
Plot xong thì đọc như khớp mẫu. Bên trái, slope −1/2: vùng white noise, đọc N ngay tại τ = 1 s. Khúc giữa phẳng dần khi flicker chiếm quyền, điểm thấp nhất đánh dấu bias instability. Bên phải, slope +1/2: rate random walk, theo σ(τ) = K·√(τ/3). Nếu đoạn τ ngắn nhất dốc −1 thay vì −1/2 thì đó là nhiễu lượng tử hóa từ độ phân giải đầu ra của cảm biến, vô hại.
Mấy cái bướu mới đáng xem. Nhiễu tuần hoàn chu kỳ T0 in lên đường cong một dao động, bướu đầu rơi quanh τ ≈ T0/2. Đêm đầu quên tắt điều hòa, cả 3 máy log đêm đó mọc bướu ở τ ≈ 500 s; đo lại chu kỳ máy nén thì ra chừng 17 phút. Con quay tường thuật trung thực cả tòa nhà. Từ đêm sau điều hòa tắt.
Một tuần, năm chiếc điện thoại
Setup ghi cho đủ: 5 máy (hai iPhone, một Android tầm Pixel, hai Android tầm trung, đời 2019–2025), mỗi máy log 7–9 tiếng qua đêm ở 100 Hz trên tấm granite dày 30 mm, bật airplane mode, tắt màn hình, không sạc, cửa studio khóa. 8 tiếng ở 100 Hz là 2,9 triệu sample mỗi trục, cỡ 35 MB float32 mỗi máy.
Số liệu trục z, vì heading dùng trục này. ARW chạy từ 0,15 độ/√h trên flagship mới nhất đến 0,9 độ/√h trên máy tầm trung đời 2019; ba máy còn lại nằm giữa 0,2 và 0,6. Bias instability lần lượt 16, 19, 24, 31 và 44 độ/giờ, đáy rơi trong khoảng τ = 80 đến 400 s. Chỉ hai máy già nhất lộ đuôi +1/2 đáng tin trước khi thanh sai số nhão ra; một trong hai máy, đuôi co lại khi log lại trong hộp xốp, tức có phần do nhiệt, studio về đêm nguội đi chừng 2 °C. Tất cả rơi đúng lớp số mà datasheet MEMS chịu nhận nếu đọc kỹ.
Quy ra góc cho dễ hình dung. N = 0,3 độ/√h là 0,005 độ/√s; tích phân hở 60 s cho 0,04° sai heading, đi hết cuốc 4 phút trong hầm cho 0,08°. White noise vô nghĩa ở thang thời gian này. Cái đáy mới cắn. B = 20 độ/giờ là 0,33 độ/phút, mà đáy hiểu theo nghĩa chặt: phần drift sống sót qua mọi kiểu calibrate bias, khéo mấy cũng vậy, vì flicker chính là phần trơ ra khi lấy trung bình thêm chẳng giúp gì nữa.
Wheria dùng mấy con số này vào việc gì
Bài Kalman tả state vector: vị trí x với y, heading θ và một biến bias gyro trôi chậm. Đường Allan chính là datasheet cho biến thứ tư. Process noise của biến này đặt theo đáy với đuôi vừa đo, nên hiệp phương sai bias phình cỡ tốc độ instability; chừng 5 phút không có hiệu chỉnh là bộ lọc thành thật quên bias cũ, độ bất định quay về sàn 0,3–0,7 độ/phút tùy máy. Nguồn hiệu chỉnh gồm các khoảnh khắc la bàn được tin và các đoạn máy gần như đứng yên, đúng kiểu gom mẫu khi gyro báo tốc độ xoay thấp mà bài la bàn từng tả.
Đường cong còn chỉnh đồng hồ gyro-only. Nằm im, máy tầm giữa trôi 0,3–0,7 độ/phút sau khi biến bias hội tụ. Đi bộ tệ hơn, lý do ở mục dưới; con số 1–3 độ/phút trong bài la bàn là số thật khi máy nằm trong túi. Find mode bỏ mũi tên, chuyển sang hướng dẫn ưu tiên khoảng cách khi độ lệch chuẩn heading vượt 25°. Với 3 độ/phút thì phải 8 phút đi bộ không la bàn mới chạm ngưỡng; bộ lọc lật sớm hơn nhiều, vì process noise heading (chừng 2° mỗi bước khi la bàn đáng tin, gần 8° trong vùng nhiều thép) cố tình đặt béo hơn drift đo được. Hiệp phương sai chỉ là lời thú nhận của bộ lọc về phần nó không kiểm chứng nổi.
Kết quả thực dụng: giữa hai lần hiệu chỉnh từ kế, máy đời giữa 2020s giữ heading lệch 1–2° qua quãng 30–90 s mà bộ lọc thường phải gánh trong lối đi nhiều thép; máy cũ nhất trong ngăn kéo test giữ được 3–5°. Đứng cách xe 20 m, lệch heading 5° đẩy đích mũi tên ngang chừng 1,7 m, vẫn hẹp hơn một ô đỗ. Biên đó là lý do Wheria sống sót qua nhiều phút la bàn bị phủ quyết mà không phải vẽ một lời nói dối tự tin.
Chỗ đường cong đứng im hết trung thực
Log qua đêm trên tấm đá là con quay lúc ngoan nhất. Bỏ vào túi quần, ba thứ kéo nó xuống. Nhạy gia tốc thẳng: gyro phổ thông ăn 0,01–0,1 độ/s trên mỗi g gia tốc, mà đi bộ dội ±0,4 g ở nhịp chừng 2 Hz, một phần chỉnh lưu thành bias giả. Nhiệt độ: rời không khí hầm 22 °C vào túi 32 °C, tempco biến chênh lệch đó thành một dốc bias mà đường Allan chưa từng thấy. Thêm chuyện điện thoại còn xoay so với người đi, vải với mô mềm cùng góp, thứ chuyển động bộ lọc không được phép ghi thành heading. Đường cong đứng im là cận dưới vật lý. Mọi tham số tuning trong Wheria nằm trên nó có chủ đích.
Muốn tự chạy thí nghiệm, bản Phyzix nội bộ đang test có thêm chế độ Allan log qua đêm trong các màn lab, vẽ luôn đường cong lẫn slope khớp được; tầng resample với đường ống logging đã sẵn từ vụ đếm bước. Trước khi bản đó ship, 16 dòng pseudo-code phía trên cộng một đêm buồn tẻ là đủ.