Magnetometers and compass nightmares

Từ kế và cơn ác mộng la bàn

A magnetometer measures the local magnetic field vector B in microtesla, reporting three orthogonal components in the phone's body frame. Earth's share runs roughly 25–65 µT depending on latitude and local geomagnetic anomalies; around our test city the reference sits near 44 µT with a shallow inclination of about 9°, courtesy of the magnetic equator passing not far south. That field will orient a compass on any street corner. It is also fragile enough that a steel parking column 2 m away can rotate apparent north by 40–90° while the phone lies perfectly still in your hand. Wheria's heading pipeline therefore treats the compass as guilty until proven innocent: magnetometer measurements enter the fusion filter only when field magnitude and inclination fall within the bands Earth could plausibly produce, and the interface surfaces that distrust explicitly instead of drawing a confident arrow toward a wall.

Heading earns this paranoia because it multiplies every other error in pedestrian dead reckoning. Miss 2 steps and you are off by roughly 1.4 m. Hold a 15° heading bias across an 80 m walk and the endpoint slides sideways by 80·sin(15°) ≈ 21 m — the wrong aisle, possibly the wrong wing of the garage. No other sensor in the stack can do that much damage that quietly.

Two ways steel lies to a magnetometer

Rotate an ideal magnetometer through every orientation and the sampled vectors trace a sphere centered at the origin, with radius equal to the local field strength. Real phones trace something worse, and the calibration literature splits the damage into two mechanisms. Hard iron errors add a constant offset on each sensor axis: a speaker magnet, a ferrous phone case, or permanent magnetization of nearby steel shifts the center of the measurement ellipsoid away from the origin. The classic user remedy is the figure-eight motion, which samples the field across many orientations and lets an algebraic fit recover the offset vector.

Soft iron is the nastier mechanism. Ferromagnetic material close to the sensor distorts the field itself, stretching the sphere into an ellipsoid instead of merely translating it. Parking pillars, rebar in the concrete, and the body of your own car all contribute, and the distortion varies with position — a calibration valid at one row can fail 3 m later. The standard sensor model wraps both effects into one line, B_meas = S·B_true + b, with b the hard-iron offset and S a 3×3 soft-iron matrix. Estimate both, invert, done. On paper.

Why the textbook fit fails in a basement

Fitting the full 3×3 matrix requires diverse phone orientations sampled over time. On a laboratory rotation rig this is a solved problem. It is unrealistic when someone is walking through a basement trying to remember whether they turned left at pillar 47. There is a deeper obstacle too: underground, S is not even a constant. The steel that defines it changes every few meters as you pass columns and parked cars, so a matrix fitted at one row is a precise model of a place you have already left.

Wheria gave up on chasing S online. We estimate only the hard-iron term, slowly, during straight walking segments: samples accumulate into a recursive fit while the gyroscope reports low turn rate, and anything captured during a sharp turn gets discarded, because the true field direction cannot change at the rate a quick pivot produces. When the fitted offset jumps by more than a few µT between segments, the estimator resets instead of averaging two different magnetic environments into one wrong answer. Soft iron gets handled downstream, by refusing to believe implausible fields at all.

Deciding when the field is believable

Every magnetometer sample faces three gates before the filter will touch it. Magnitude: |B| within ±8 µT of the local reference. Inclination: the angle between B and the horizontal plane, measured against the gravity estimate from the accelerometer, within ±10° of the reference value. Turn rate: gyroscope z-axis below about 0.3 rad/s, so the sample was not taken mid-pivot. Pass all three and the tilt-compensated heading ψ = atan2(−B_y, B_x) becomes a measurement. Fail any one and the sample lands in the diagnostics log and goes no further.

The magnitude gate does most of the work, and the physics behind it is compact. A straight cable carrying direct current I produces B = µ0·I/(2πr) at distance r. An EV fast-charging run at 100 A puts 20 µT on your sensor from 1 m away — nearly half of Earth's entire field, pointed wherever the cable happens to go. Superimpose that on the geomagnetic field and |B| almost always leaves the Earth band, so the gate trips before the heading corrupts.

// per magnetometer sample, resampled to 100 Hz
mag  = norm(B)                          // µT
inc  = asin(dot(B, g_unit))             // angle vs horizontal
turn = abs(gyro_z)                      // rad/s

ok_mag  = abs(mag - REF_MAG) < 8.0      // REF_MAG ≈ 44 µT here
ok_inc  = abs(inc - REF_INC) < 10°
ok_turn = turn < 0.3                    // not mid-pivot

if ok_mag && ok_inc && ok_turn {
    ekf.updateHeading(atan2(-B.y, B.x), R_NOMINAL)
    hardIronFit.accumulate(B)           // straight segments only
} else {
    ekf.holdHeadingMeasurement()        // gyro carries heading
    log.diagnostics(B, mag, inc, turn)
}

Bench note: we logged magnetometer magnitude at 100 Hz on 4 phones (2 iPhone, 2 Pixel) across 6 walks through the same tower basement, about 40 minutes of data in total. |B| ranged from 18 µT beside an EV charging cabinet to 140 µT flush against a structural column, against a steady 43–45 µT on the street above. Roughly 31% of basement samples failed at least one gate. Down there, the compass simply does not exist a third of the time.

Fusion when the field lies anyway

Between detected steps the gyroscope carries heading alone, integrating angular rate at 100–200 Hz. Uncorrected consumer gyro drift sits on the order of 1–3° per minute. Across the half-second gap between footfalls that is nothing. Across a 2-minute walk with no magnetometer assistance it is unacceptable.

The Extended Kalman Filter arbitrates between the two sources. Gated magnetometer headings arrive as measurements with nominal noise R, and the gyro propagates state between them. When the variance of heading residuals spikes, the filter inflates R by an order of magnitude, which reduces the system to gyro-only dead reckoning until the field stabilizes. That spike is the telltale signature of entering a steel zone near EV charging stations, where DC cables and transformers superimpose local fields. Residual variance also catches what the per-sample gates cannot: sequences where each measurement looks Earth-like on its own but the series disagrees with the gyro about how fast the world is turning. The matrix bookkeeping behind all of this lives in Kalman filters for parking.

What the screen admits

Distrust that stays buried inside a filter helps nobody. Wheria promotes it to the interface. A compass confidence chip turns yellow, then red, as heading variance grows, and the augmented-reality arrow widens its cone of uncertainty to match. When heading standard deviation exceeds 25°, Find mode drops the arrow entirely and shifts to distance-first presentation: walk approximately 60 m, then rescan the compass in a clearer area. We would rather under-instruct than confidently navigate you into structural concrete.

The policy came from our own walks. My regression car sat at floor B2, pillar E9, one aisle over from a row of chargers; on a bad afternoon the chip held red for about 40 s while the screen offered nothing but a distance count. I still reached the car without a single wrong turn. Distance plus pillar signage is a surprisingly complete navigation system, and the arrow rejoined two aisles later once residual variance settled.

The regression matrix and platform parity

ktuyen's regression matrix documents the worst environments and reruns them every release: electric vehicle charging bays with heavy DC cabling, structural steel within 2 m of the walking path, and the occasional second phone in a back pocket — a moving hard-iron source that no amount of figure-eight waving will remove. Each row records whether the confidence chip fired, how long heading stayed degraded and whether the fallback presentation appeared. We can live with a garage that defeats the compass. What we can't live with is the compass failing without telling anyone.

Platform parity carries its own trap here: iOS and Android report magnetometer axes with different native conventions. We normalize to a consistent north-east-down or east-north-up frame inside shared logic modules before either SwiftUI or Compose renders a direction, the same layering described in our two-codebase setup. Phyzix shows raw and calibrated magnetometer vectors side by side, letting students watch the ellipsoid flatten as they walk into Landmark 81's basement — the same data Wheria consumes, presented as physics rather than navigation. Sensor hacking with Phyzix covers how to capture those traces yourself.

Physics does not allow a 5° accuracy guarantee in every garage. What Wheria can guarantee is narrower: it will not walk you into a wall with confidence, and when the instrument distrusts itself, that distrust shows up on screen. Heading feeds the step-based position updates covered in step detection and IMU odometry, and the assembled underground stack lives in indoor navigation when GPS dies.

Từ kế đo vector từ trường B theo microtesla, trả ba thành phần trực giao trong khung thân máy. Phần của Trái Đất khoảng 25–65 µT tùy vĩ độ và anomaly địa từ. Quanh thành phố mình test, tham chiếu chừng 44 µT, inclination nông cỡ 9° vì xích đạo từ chạy ngay phía nam. Ngoài phố, field đó đủ cho la bàn chỉ đúng. Nhưng chỉ cần một cột thép cách 2 m, hướng bắc biểu kiến xoay 40–90° trong khi máy nằm im trong tay. Nên pipeline heading của Wheria coi la bàn có tội cho đến khi chứng minh vô tội: mẫu từ kế chỉ được vào bộ lọc fusion khi độ lớn field và inclination nằm trong band mà Trái Đất có thể tạo ra, còn giao diện hiện rõ mức nghi ngờ thay vì vẽ mũi tên tự tin chỉ vào tường.

Heading đáng bị soi kỹ vậy vì nó nhân mọi sai số khác trong dead reckoning đi bộ. Đếm hụt 2 bước, lệch chừng 1,4 m. Còn giữ bias heading 15° suốt 80 m thì điểm cuối trượt ngang 80·sin(15°) ≈ 21 m — sai hẳn dãy xe, có khi sai cả cánh hầm. Không cảm biến nào khác phá được nhiều đến thế mà êm đến thế.

Hai kiểu thép lừa từ kế

Xoay một từ kế lý tưởng qua đủ mọi hướng, các vector đo được vẽ thành mặt cầu tâm tại gốc, bán kính bằng độ lớn field local. Máy thật vẽ ra thứ xấu hơn. Sách vở về hiệu chuẩn chia lỗi này thành hai cơ chế. Hard iron cộng offset hằng lên từng trục cảm biến: nam châm loa, ốp lưng nhiễm sắt hoặc thép gần bị từ hóa vĩnh viễn đẩy tâm ellipsoid đo lệch khỏi gốc. Cách chữa quen thuộc là vẽ số 8 trong không khí — động tác đó lấy mẫu field qua nhiều hướng, đủ cho một phép fit đại số tìm ra vector offset.

Soft iron khó chịu hơn. Vật liệu ferromagnetic sát cảm biến méo chính field, kéo mặt cầu thành ellipsoid chứ không chỉ tịnh tiến. Cột hầm, cốt thép trong bê tông và thân xe của chính bạn đều góp phần. Méo này đổi theo vị trí: calibrate đúng ở hàng này, đi 3 m có thể sai. Model chuẩn gói cả hai vào một dòng, B_meas = S·B_true + b, với b là offset hard-iron và S là ma trận soft-iron 3×3. Ước lượng cả hai, nghịch đảo, xong. Trên giấy.

Vì sao fit sách giáo khoa chết trong hầm

Muốn fit đủ ma trận 3×3 phải lấy mẫu ở nhiều tư thế máy theo thời gian. Trên rig xoay trong lab, bài toán đã giải xong. Còn với người đang đi trong hầm, vừa đi vừa cố nhớ đã quẹo trái ở cột 47 hay chưa, chuyện đó phi thực tế. Có trở ngại sâu hơn: dưới hầm, S không phải hằng số. Thép định hình nó đổi sau vài mét khi bạn ngang qua cột và xe đang đỗ, nên ma trận fit ở hàng này chỉ mô tả chính xác một chỗ bạn đã rời khỏi.

Wheria bỏ hẳn việc đuổi theo S online. Chỉ ước lượng offset hard-iron, chậm rãi, trong các đoạn đi thẳng: mẫu chỉ được gom vào phép fit đệ quy khi con quay báo tốc độ quay thấp, còn mẫu bắt trong lúc quẹo gắt bị bỏ, vì hướng field thật không thể đổi nhanh cỡ một cú xoay người. Offset fit được mà nhảy quá vài µT giữa hai đoạn thì estimator reset, khỏi lấy trung bình hai môi trường từ khác nhau ra một đáp số sai. Soft iron xử lý ở tầng sau, bằng cách từ chối tin mọi field phi lý.

Quyết định lúc nào field đáng tin

Mỗi sample từ kế phải qua ba gate trước khi bộ lọc đụng tới. Độ lớn: |B| trong ±8 µT quanh tham chiếu local. Inclination: góc giữa B và mặt phẳng ngang, đo dựa trên vector trọng lực từ gia tốc kế, trong ±10° so với tham chiếu. Tốc độ quay: trục z con quay dưới chừng 0,3 rad/s, tức sample không bị lấy giữa lúc xoay. Qua đủ ba gate, heading đã bù nghiêng ψ = atan2(−B_y, B_x) thành một phép đo. Rớt một gate, sample chỉ còn nằm trong log chẩn đoán.

Gate độ lớn gánh phần lớn việc. Vật lý phía sau khá gọn. Dây thẳng mang dòng một chiều I tạo field B = µ0·I/(2πr) ở khoảng cách r. Một phiên sạc nhanh EV 100 A đặt 20 µT lên cảm biến từ khoảng cách 1 m — gần nửa toàn bộ field Trái Đất, chĩa theo hướng sợi cáp muốn. Chồng thứ đó lên field địa từ thì |B| gần như luôn văng khỏi band Trái Đất, gate kịp sập trước khi heading hỏng.

// mỗi sample từ kế, resample về 100 Hz
mag  = norm(B)                          // µT
inc  = asin(dot(B, g_unit))             // góc so với mặt ngang
turn = abs(gyro_z)                      // rad/s

ok_mag  = abs(mag - REF_MAG) < 8.0      // REF_MAG ≈ 44 µT tại đây
ok_inc  = abs(inc - REF_INC) < 10°
ok_turn = turn < 0.3                    // không giữa cú xoay

if ok_mag && ok_inc && ok_turn {
    ekf.updateHeading(atan2(-B.y, B.x), R_NOMINAL)
    hardIronFit.accumulate(B)           // chỉ đoạn đi thẳng
} else {
    ekf.holdHeadingMeasurement()        // gyro gánh heading
    log.diagnostics(B, mag, inc, turn)
}

Ghi chú bench: log độ lớn từ kế ở 100 Hz trên 4 máy (2 iPhone, 2 Pixel) qua 6 lượt đi trong cùng một hầm tòa tháp, tổng cỡ 40 phút dữ liệu. |B| chạy từ 18 µT cạnh tủ sạc EV tới 140 µT khi áp sát cột chịu lực, so với mức 43–45 µT đều đặn trên mặt phố. Khoảng 31% sample dưới hầm rớt ít nhất một gate. Dưới đó, một phần ba thời gian la bàn coi như không tồn tại.

Fusion khi field vẫn nói dối

Giữa các bước chân, con quay gánh heading một mình, tích phân tốc độ góc ở 100–200 Hz. Không hiệu chỉnh, drift con quay hàng phổ thông cỡ 1–3° mỗi phút. Trong khe nửa giây giữa hai bước chân, con số đó chẳng là gì. Qua một quãng đi 2 phút không có từ kế hỗ trợ thì không chấp nhận được.

Extended Kalman Filter đứng giữa phân xử. Heading từ kế đã qua gate vào như phép đo với nhiễu danh định R, con quay lan truyền trạng thái ở giữa. Khi phương sai residual heading vọt lên, bộ lọc thổi R lên một bậc, hệ coi như về dead reckoning thuần gyro cho tới khi field ổn lại. Cú vọt đó là dấu hiệu đặc trưng của việc bước vào vùng thép gần trạm sạc EV, nơi cáp DC và biến áp chồng field local lên nhau. Phương sai residual còn bắt được thứ gate từng-sample bỏ sót: chuỗi đo mà từng mẫu trông giống Trái Đất nhưng cả dãy cãi nhau với con quay về tốc độ thế giới đang xoay. Phần sổ sách ma trận nằm ở bộ lọc Kalman cho chuyện đỗ xe.

Màn hình thú nhận gì

Nghi ngờ mà chôn trong bộ lọc thì chẳng giúp ai. Wheria đẩy nó lên giao diện. Chip confidence la bàn chuyển vàng rồi đỏ theo phương sai heading, mũi tên AR mở rộng cone bất định tương ứng. Khi độ lệch chuẩn heading vượt 25°, chế độ Find bỏ hẳn mũi tên, chuyển sang trình bày ưu tiên khoảng cách: đi chừng 60 m rồi quét lại la bàn ở chỗ thoáng hơn. Thà chỉ dẫn thiếu còn hơn tự tin dắt bạn vào bê tông chịu lực.

Chính sách này ra đời từ những lượt đi thật. Xe test của mình đỗ tầng B2, cột E9, cách dãy sạc một lối đi; buổi chiều xấu trời chip giữ màu đỏ chừng 40 giây, màn hình chỉ còn con số khoảng cách. Vẫn tới xe, không quẹo nhầm lần nào. Khoảng cách cộng biển số cột hóa ra là một hệ dẫn đường khá đủ. Mũi tên quay lại sau hai dãy khi residual lắng xuống.

Ma trận regression và parity hai nền tảng

Ma trận regression của ktuyen ghi các môi trường tệ nhất và chạy lại mỗi release: bãi sạc EV cáp DC nặng, thép kết cấu trong vòng 2 m quanh lối đi, thỉnh thoảng thêm chiếc điện thoại thứ hai trong túi quần sau — một nguồn hard-iron di động mà vẽ số 8 kiểu gì cũng không khử nổi. Mỗi hàng ghi chip confidence có bật không, heading suy giảm bao lâu và màn hình fallback có xuất hiện không. Hầm đánh bại la bàn: chấp nhận. Hầm đánh bại la bàn trong im lặng: bug.

Parity nền tảng có bẫy riêng: iOS và Android trả trục từ kế theo convention native khác nhau. Team chuẩn hóa về một khung north-east-down hoặc east-north-up thống nhất trong module logic dùng chung, trước khi SwiftUI hay Compose vẽ bất kỳ hướng nào, cùng cách chia tầng như trong bài hai codebase. Phyzix đặt vector từ kế raw và đã calibrate cạnh nhau, để học sinh nhìn ellipsoid bẹt dần khi bước xuống hầm Landmark 81 — đúng dòng data Wheria dùng, nhưng bày ra như vật lý thay vì dẫn đường. Muốn tự bắt các trace đó, xem vọc cảm biến với Phyzix.

Không hứa la bàn chính xác 5° trong mọi hầm, vì vật lý không cho phép lời hứa đó. Chỉ hứa không tự tin dắt bạn vào tường và hứa rằng khi dụng cụ nghi ngờ chính nó, bạn thấy mức nghi ngờ ấy ngay trên màn hình. Heading nuôi các cập nhật vị trí theo bước trong phát hiện bước chân và IMU, còn cả stack dưới lòng đất ráp lại trong dẫn đường trong nhà khi GPS chết.