Barometers and parking floor math

Áp kế và toán tầng hầm

The barometer in your phone is a MEMS pressure sensor: a silicon membrane that flexes as air presses on it, read out as absolute atmospheric pressure in hectopascals. Sitting still on a desk, a good one shows noise of about 0.3–1 Pa RMS. That number turns interesting next to a second one. Near sea level, atmospheric pressure falls by roughly 12 Pa per meter of altitude gained, and dividing the two says the sensor can resolve a few centimeters of height change on paper. In a live garage, with air handling and weather in the loop, call it 1 m of usable vertical resolution. That is enough to separate basement B2 from B3 in a structure built at 3.2 m per floor. Replacing horizontal positioning is a different job entirely, and GPS underground has problems of its own, covered in why GPS lies.

So Wheria treats the barometer as a floor hint, not a navigation solution. Tap Park and the app records the current pressure P0 as a session baseline, optionally flagged if you are still outdoors at street level, where weather drift has had no time to accumulate. From then on, samples arrive at 1–10 Hz depending on how the OS schedules the sensor, and each becomes ΔP = P − P0. Two filters stand between that raw difference and anything the interface is willing to call a floor.

Where 12 Pa per meter comes from

Take a horizontal slab of still air: thickness Δh, cross-section A, density ρ. It weighs ρ·A·Δh·g. The only thing holding it up is the pressure difference between its bottom face and its top face, so (P_bottom − P_top)·A = ρ·g·A·Δh. The area cancels. What remains is the hydrostatic relation ΔP = ρ·g·Δh, or rearranged into the direction we actually use it, Δh = ΔP/(ρg).

Plug in numbers. Air density near sea level runs from about 1.15 kg/m³ on a 32 °C Saigon afternoon to 1.225 kg/m³ in the 15 °C standard atmosphere, and g = 9.81 m/s², so ρg lands between 11.3 and 12.0 Pa per meter. Call it 12. One garage floor at 3.2 m is then worth about 37 Pa of pressure change. The temperature spread introduces a 3–5% scale error, which across a five-floor basement stays well under half a floor and disappears into the uncertainty band.

The full barometric formula is exponential, P(h) = P0·e^(−h/H), with scale height H = RT/(Mg) ≈ 8,400 m. Garages are not 8,400 m deep. Over the 16 m of a five-level basement, the linear approximation deviates from the exponential by about 0.1% of the signal, so we linearize without guilt.

Two filters and what they keep

The stationary noise floor of 0.3–1 Pa RMS turns out to be the smallest problem. Walking pumps the pressure field around the phone. HVAC fans in enclosed garages add step changes whenever a ventilation zone switches over. The pipeline therefore low-passes ΔP at about 0.05 Hz, treating anything faster than a 20-second period as mechanical noise, since a stair descent of a full floor does not complete in under 20 s.

The slow end is worse. Weather fronts move ambient pressure by 200–400 Pa per day, which averages 8–17 Pa per hour and runs several times faster while a front actually passes. Left uncorrected, a cold front arriving during a long dinner reads as a full floor of descent that never happened. A high-pass around 0.001 Hz (periods longer than roughly 17 minutes) strips that trend out. Between the two cutoffs sits the passband where real transitions live: a one- or two-floor stair descent takes 40–90 s and comes through untouched.

Long sessions still need care, because a baseline recorded at noon means little at 21:00. On the walk back, the estimator re-anchors: pressure observed at the street entrance becomes a fresh reference, and the depth profile learned on the way out is replayed against it. The stale P0 survives only as a consistency check.

Where the naive formula breaks

Elevators are the classic failure. Shaft pressurization and cabin ventilation produce pressure transients faster than anything a ramp walk can generate, and the sensor has no idea it is inside an elevator — from where it sits, the weather just improved by 0.7 hPa in 15 seconds. Wheria survives this only because the IMU reports elevator-like motion and the filter down-weights barometer trust for that interval.

Subtler offenders exist too. Parking with the engine or the climate control running shifts cabin pressure slightly before you step out, so a baseline can be born a few pascals wrong. iPhone and Pixel barometers carry different absolute offsets, sometimes approaching a full hectopascal, which is why we store per-session baselines at park time instead of attempting aviation-style QNH altimeter correction against a weather service. Architecture matters as well: the pressurized elevator shafts of a tower like Landmark 81 behave nothing like the open spiral ramps of a suburban mall, and ktuyen segments the barometer regression suite by garage architecture for exactly this reason rather than assuming one calibration fits all.

The estimator in 20 lines

Stripped of platform scaffolding, the whole floor estimator fits on one screen:

// every barometer sample, 1-10 Hz
fun onSample(p: Pa, t: Time) {
    val raw  = p - p0                      // Pa relative to Park
    weather  = ema(raw, tauSec = 900)      // slow trend ~ 0.001 Hz high-pass
    val band = lowPass(raw - weather, fcHz = 0.05)

    val trust = when (imu.activity) {
        ELEVATOR   -> 0.1                  // transients lie; wait it out
        WALKING    -> 1.0
        STATIONARY -> 0.6
        else       -> 0.3
    }

    val slab       = structure.slabHeight ?: 3.2   // m, learned per garage
    val paPerFloor = RHO_G * slab                  // 11.6 x 3.2 ~ 37 Pa
    val floors     = round(band / paPerFloor)
    val sigma      = noiseSigma(band) / paPerFloor

    publish(FloorHint(floors, plusMinusOne = true, trust, sigma))
}

Slab height defaults to 3.2 m per level and is user-configurable, and if you have parked in the same structure before, the value learned from prior visits overrides the default. Everything else is bookkeeping around one division.

What we display and what we refuse to claim

The interface shows something like "Likely B3" with an explicit ±1 floor uncertainty band. A floor label you typed manually is never auto-overwritten. Barometer inference is probabilistic; a wrong automatic label erodes trust faster than no label at all, and the label you entered while standing next to your own car outranks any filter output.

The regression numbers justify the humility. In Landmark-style open slab garages at 3.2 m floor height, median classification error is 0 floors when the user takes the stairs. Elevator-only transitions mislabel roughly 30% of the time once IMU activity context is withheld — nothing then distinguishes walking down a ramp from standing in a moving box. A rider going from B1 to B3 produces a pressure profile indistinguishable from someone who walked two flights of stairs in 10 s; the giveaway is the accelerometer, which reports the characteristic vibration and near-zero horizontal displacement of elevator travel. That fusion logic lives in the same pipeline as step odometry, described in step detection and IMU odometry, and compass heading. It is also why ktuyen logs "stairs only" and "elevator only" test cases separately in her regression spreadsheet instead of averaging them into one flattering number.

Bench notes

Method: three phones (iPhone 13, Pixel 7, Galaxy A54) face up on a shelf, screens off, logging pressure at 1 Hz for 48 hours, detrended hourly against a straight line. Detrended noise came out at 0.35, 0.6 and 0.9 Pa RMS respectively, comfortably inside the 0.3–1 Pa RMS band we design around. All three tracked the same synoptic weather curve. Absolute offsets between units, though, spanned 40–110 Pa — and that figure is the entire argument for per-session baselines, because 110 Pa is 3 floors of phantom altitude the moment you compare absolute readings across devices.

Two shorter notes from the same week. A stairwell door slammed 2 m away spikes the trace by 25–30 Pa for under 1 s; the 0.05 Hz low-pass removes it completely. Closing a car door with the windows up produces a 60–90 Pa transient inside the cabin, which is one more reason the baseline flag prefers a reading taken after you have stepped out.

Try the math yourself

The Lab page has a garage scroll demo that visualizes the hydrostatic relationship in real time; drag through the floors and watch ΔP track depth. The barometer is one input among several — the full stack is described in indoor navigation when GPS dies, and the floor hint enters the Extended Kalman Filter alongside step odometry and compass heading, as covered in Kalman filters for parking. For the raw instrument with none of our filtering opinions on top, Phyzix exposes barometer data in SI units in its environment instruments section. Watch it for 10 minutes and you will see everything this post described: the noise floor, the weather drift and, when somebody slams a door, the spike.

Áp kế trong điện thoại là cảm biến áp suất MEMS: một màng silicon võng xuống khi không khí đè lên, đọc ra áp khí quyển tuyệt đối theo hectopascal. Để yên trên bàn, nhiễu cỡ 0,3–1 Pa RMS. Số đó chỉ thú vị khi đặt cạnh số thứ hai: gần mực nước biển, áp suất giảm khoảng 12 Pa mỗi mét lên cao. Chia hai số cho nhau, trên giấy sensor phân giải được vài centimet độ cao. Trong hầm thật, có quạt gió với thời tiết chen vào, cứ tính tròn 1 m. Đủ phân biệt B2 với B3 trong hầm xây 3,2 m mỗi tầng. Còn thay GPS ngang thì không — chuyện GPS dưới hầm tệ thế nào đã viết trong vì sao GPS nói dối.

Nên Wheria chỉ coi áp kế là gợi ý tầng, không phải lời giải dẫn đường. Bấm Đỗ xe, app ghi áp suất hiện tại P0 làm baseline của session, kèm cờ tùy chọn nếu bạn còn đứng ngoài trời ngang mặt đường — lúc drift thời tiết chưa kịp tích lũy. Từ đó sample về với nhịp 1–10 Hz tùy OS xếp lịch, mỗi sample thành ΔP = P − P0. Giữa con số thô đó với chữ "tầng" trên màn hình còn hai bộ lọc đứng chắn.

12 Pa mỗi mét từ đâu ra

Lấy một lát không khí nằm ngang: dày Δh, tiết diện A, mật độ ρ. Lát này nặng ρ·A·Δh·g. Thứ duy nhất đỡ nó là chênh áp giữa mặt dưới với mặt trên: (P_dưới − P_trên)·A = ρ·g·A·Δh. Rút gọn A. Còn lại quan hệ thủy tĩnh ΔP = ρ·g·Δh, hay viết theo chiều mình dùng: Δh = ΔP/(ρg).

Thay số. Mật độ không khí gần mực nước biển chạy từ khoảng 1,15 kg/m³ buổi chiều Sài Gòn 32 °C đến 1,225 kg/m³ ở khí quyển chuẩn 15 °C, còn g = 9,81 m/s². Vậy ρg rơi vào khoảng 11,3–12,0 Pa mỗi mét. Cứ tính 12. Một tầng hầm 3,2 m tương đương chừng 37 Pa. Chênh nhiệt độ gây sai số tỷ lệ 3–5%, qua hầm năm tầng vẫn chưa tới nửa tầng, band bất định nuốt gọn.

Công thức khí áp đầy đủ là hàm mũ, P(h) = P0·e^(−h/H), với thang độ cao H = RT/(Mg) ≈ 8.400 m. Hầm xe không sâu 8.400 m. Qua 16 m của hầm năm tầng, xấp xỉ tuyến tính lệch hàm mũ chừng 0,1% tín hiệu. Tuyến tính hóa không cần áy náy.

Hai bộ lọc giữ lại gì

Nhiễu đứng yên 0,3–1 Pa RMS hóa ra là chuyện nhỏ nhất. Đi bộ bơm trường áp quanh máy. Quạt HVAC trong hầm kín tạo bậc nhảy mỗi khi đổi zone thông gió. Nên pipeline low-pass ΔP ở khoảng 0,05 Hz: cái gì nhanh hơn chu kỳ 20 giây coi là nhiễu cơ học, vì xuống trọn một tầng cầu thang không ai xong dưới 20 giây.

Đầu chậm mới phiền. Front thời tiết đẩy áp môi trường 200–400 Pa mỗi ngày, trung bình 8–17 Pa mỗi giờ, lúc front đi qua còn nhanh gấp mấy lần. Không xử lý thì một front lạnh đến giữa bữa tối dài đọc ra thành nguyên một tầng đi xuống chưa từng xảy ra. High-pass quanh 0,001 Hz (chu kỳ dài hơn cỡ 17 phút) cắt trend đó đi. Kẹp giữa hai cutoff là passband nơi chuyển tầng thật sống: xuống cầu thang một hai tầng mất 40–90 giây, đi qua nguyên vẹn.

Session dài vẫn phải cẩn thận, baseline ghi lúc trưa chẳng nói được gì lúc 21 giờ. Trên đường quay lại, bộ ước lượng neo lại: áp đo ở cửa vào ngang mặt đường thành mốc mới, profile độ sâu học lúc đi ra được chiếu lại lên mốc đó. P0 cũ chỉ giữ để đối chiếu.

Công thức naive vỡ ở đâu

Thang máy là ca kinh điển. Áp suất hóa hố thang với quạt cabin tạo transient nhanh hơn mọi thứ đi ramp sinh ra nổi, mà sensor thì không biết nó đang ở trong thang máy — theo góc nhìn của nó, thời tiết vừa đẹp lên 0,7 hPa trong 15 giây. Wheria sống sót vì IMU báo chuyển động kiểu thang máy và bộ lọc giảm trọng số tin áp kế trong khoảng đó.

Mấy thủ phạm kín tiếng hơn: đỗ xe còn nổ máy hoặc bật điều hòa làm áp trong cabin lệch nhẹ trước khi bạn bước ra, baseline sinh ra đã sai vài pascal. Áp kế iPhone với Pixel mang offset tuyệt đối khác nhau, có khi gần cả hectopascal, nên bọn mình lưu baseline theo từng session lúc đỗ thay vì hiệu chỉnh kiểu QNH hàng không dựa vào dịch vụ thời tiết. Kiến trúc cũng quan trọng: hố thang máy điều áp của tòa tháp như Landmark 81 chẳng giống gì ramp xoắn hở của mall ngoại ô. ktuyen tách bộ regression áp kế theo kiến trúc hầm đúng vì lý do này, thay vì tin một bản hiệu chuẩn dùng được mọi nơi.

Bộ ước lượng trong 20 dòng

Bỏ hết code nền tảng, toàn bộ bộ ước lượng tầng nằm gọn một màn hình:

// moi sample ap ke, 1-10 Hz
fun onSample(p: Pa, t: Time) {
    val raw  = p - p0                      // Pa so voi luc Do xe
    weather  = ema(raw, tauSec = 900)      // trend cham ~ high-pass 0.001 Hz
    val band = lowPass(raw - weather, fcHz = 0.05)

    val trust = when (imu.activity) {
        ELEVATOR   -> 0.1                  // transient noi doi; cho qua
        WALKING    -> 1.0
        STATIONARY -> 0.6
        else       -> 0.3
    }

    val slab       = structure.slabHeight ?: 3.2   // m, hoc theo tung ham
    val paPerFloor = RHO_G * slab                  // 11.6 x 3.2 ~ 37 Pa
    val floors     = round(band / paPerFloor)
    val sigma      = noiseSigma(band) / paPerFloor

    publish(FloorHint(floors, plusMinusOne = true, trust, sigma))
}

Cao sàn mặc định 3,2 m mỗi tầng, user chỉnh được; đỗ lại đúng hầm cũ thì giá trị học từ những lần trước đè lên mặc định. Phần còn lại chỉ là sổ sách quanh một phép chia.

Hiển thị gì và từ chối tuyên bố gì

Giao diện hiện kiểu "Có thể B3" kèm band bất định ±1 tầng nói rõ. Nhãn tầng bạn tự gõ thì không bao giờ bị ghi đè tự động. Suy luận áp kế mang tính xác suất; nhãn tự động sai phá niềm tin nhanh hơn không có nhãn nào. Nhãn bạn nhập lúc đứng cạnh xe đứng trên mọi output của bộ lọc.

Số liệu regression biện hộ cho thái độ khiêm tốn đó. Hầm sàn mở kiểu Landmark cao 3,2 m mỗi tầng, lỗi phân loại trung vị là 0 tầng khi user đi cầu thang. Chuyển tầng chỉ bằng thang máy sai nhãn cỡ 30% nếu giấu context hoạt động IMU đi — khi đó chẳng còn gì phân biệt đi bộ xuống ramp với đứng trong cái hộp di chuyển. Người đi thang máy từ B1 xuống B3 tạo profile áp không phân biệt nổi với người đi bộ hai tầng cầu thang trong 10 giây; thứ tố giác là gia tốc kế, báo rung đặc trưng cùng dịch chuyển ngang gần bằng 0 của chuyến thang máy. Logic fusion đó nằm chung pipeline với odometry bước — xem phát hiện bước và IMU odometry — cùng heading la bàn. Cũng vì vậy ktuyen log riêng case "chỉ cầu thang" với case "chỉ thang máy" trong spreadsheet regression, thay vì gộp trung bình thành một con số đẹp.

Ghi chú bench

Phương pháp: ba máy (iPhone 13, Pixel 7, Galaxy A54) đặt ngửa trên kệ, tắt màn hình, log áp 1 Hz suốt 48 giờ, mỗi giờ detrend bằng đường thẳng. Nhiễu sau detrend ra 0,35, 0,6 cùng 0,9 Pa RMS, nằm gọn trong band 0,3–1 Pa RMS mà bọn mình thiết kế quanh nó. Cả ba bám cùng một đường thời tiết synoptic. Riêng offset tuyệt đối giữa các máy lệch nhau 40–110 Pa — con số đó là toàn bộ lập luận cho baseline theo session, vì 110 Pa là 3 tầng độ cao ma nếu dại dột so đọc số tuyệt đối giữa hai máy.

Hai ghi chú ngắn cùng tuần. Cửa buồng thang bộ đóng sầm cách 2 m làm trace vọt 25–30 Pa dưới 1 giây; low-pass 0,05 Hz nuốt sạch. Đóng cửa ô tô khi kính kéo kín tạo transient 60–90 Pa trong cabin, thêm một lý do cờ baseline thích số đo sau khi bạn đã bước ra ngoài.

Tự thử phép toán

Trang Lab có demo scroll hầm xe visual hóa quan hệ thủy tĩnh theo thời gian thực; kéo qua từng tầng và xem ΔP bám độ sâu. Áp kế chỉ là một input trong nhiều input — cả stack nằm trong dẫn đường trong nhà khi GPS chết, còn gợi ý tầng vào Extended Kalman Filter cùng odometry bước với heading la bàn như Kalman cho đỗ xe. Muốn xem dụng cụ thô không qua lớp lọc nào của bọn mình, Phyzix mở dữ liệu áp kế đơn vị SI trong nhóm công cụ môi trường. Nhìn 10 phút là thấy đủ mọi thứ bài này tả: sàn nhiễu, drift thời tiết và cú vọt khi có ai đóng sầm cửa.