# Magnetometers and compass nightmares > Magnetometers in steel garages see a distorted Earth field. How Wheria calibrates, when it trusts the gyro instead, and why we warn instead of guessing. 2026-02-18 · 22 min read · wheria, physics · by ntan (ntan) for uranashel Canonical HTML: https://uranashel.com/blog/compass-calibration-story.html --- 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](https://uranashel.com/blog/kalman-filter-parking.html). ## 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](https://uranashel.com/blog/cross-platform-two-codebases.html). 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](https://uranashel.com/blog/phyzix-sensor-hacking.html) 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](https://uranashel.com/blog/step-detection-imu.html), and the assembled underground stack lives in [indoor navigation when GPS dies](https://uranashel.com/blog/building-wheria-indoor.html). --- uranashel · [Home](https://uranashel.com/) · [Apps](https://uranashel.com/apps.html) · [Lab](https://uranashel.com/lab.html) · [About](https://uranashel.com/about.html) · [Blog](https://uranashel.com/blog/) · [Developers](https://uranashel.com/developers/) · [API docs](https://uranashel.com/docs/) · [Privacy](https://uranashel.com/privacy.html) Machine-readable: [llms.txt](https://uranashel.com/llms.txt) · [sitemap.xml](https://uranashel.com/sitemap.xml) · [openapi.json](https://uranashel.com/openapi.json) · [API](https://uranashel.com/api/v1/)