How OpenSpiro calculates VO2 (and what to watch out for)
A long read for VO2 analyzer nerds: the Haldane transformation, wet- vs dry-basis bookkeeping, why OpenSpiro picks a different math path per device, and the sensor realities that make or break your readings.
A VO2 number on a screen looks deceptively simple: 3,200 mL/min, 45 mL/kg/min, done. In reality, that single value rests on a chain of conventions and assumptions that vary by device, by sensor, and by ambient conditions. Get any link in the chain wrong and your VO2 can drift 20–50% without anything looking obviously broken. This article walks through what OpenSpiro actually does, why it does it differently for different analyzers, and the practical realities of getting trustworthy numbers.
The fundamental formula: Haldane mass balance
VO2 isn't measured directly. What gas analyzers actually measure are: how much air came out of you (volume), and what fraction of that air was O2 and CO2. To turn those into "millilitres of oxygen per minute consumed" you need to know how much air went in — and that's where the Haldane transformation comes in.
Nitrogen is metabolically inert: the body neither produces nor consumes it. So the nitrogen flux in equals the nitrogen flux out. With expired O2 (FEO2) and CO2 (FECO2) fractions known, and inspired air at known fractions (FIO2 ≈ 0.2095, FICO2 ≈ 0.0004), you can derive the inspired volume from the expired volume:
Haldane ratio = FEN2 / FIN2 = (1 − FEO2 − FECO2) / (1 − FIO2 − FICO2)
VO2 = VE × (FIO2 × Haldane − FEO2)
VCO2 = VE × (FECO2 − FICO2 × Haldane)
Easy on paper. The trouble starts with what VE and the gas fractions actually mean physically.
STPD, wet vs dry: the bookkeeping problem
Volume isn't volume. Air at body temperature, fully saturated with water vapor (BTPS), occupies more space than the same molecules at 0 °C, 1013 hPa, dry (STPD). To compare metabolic rates across labs and conditions, every analyzer corrects volumes to STPD. The standard correction is:
STPD = (P_amb / 1013.25) × (273.15 / T_K) × (1 − PH2O / P_amb)
The first two terms convert temperature and pressure. The last term subtracts water vapor — which only makes sense if you're trying to count dry molecules. Whether you apply it depends on whether your O2 and CO2 sensors see wet or dry gas:
- Dry-basis sensors (e.g. paramagnetic O2 cells with a sample-drying line): the sensor's
FEO2is already the fraction of dry expired air. Apply the full STPD correction including the water-vapor term, and use the textbook Haldane formula above. - Wet-basis sensors (most galvanic O2 cells, NDIR CO2 sensors without drying): the sensor reports the fraction of wet expired air, where water vapor still occupies space. Now the bookkeeping has to be consistent: keep everything on a wet basis. The
FIO2of inspired wet ambient air is0.2095 × (1 − PH2O_amb/P_amb), the Haldane denominator includes water vapor explicitly, and the volume STPD correction omits the water-vapor term (it's already accounted for on the gas side).
Both conventions are physically correct in isolation. Mix them — apply a textbook dry-basis formula to a sensor that reads wet — and you'll inflate VO2 by roughly 5–20% depending on conditions. That's exactly the bug we found and fixed for Calibre Bio devices in OpenSpiro 0.1.1-alpha.11.
Why OpenSpiro uses a different model per device
Different analyzers expose different signals. There's no single universal formula that produces correct VO2 for every device — what's correct depends on which gases are dried, whether VO2 is computed on-device, and which environment data is available. So OpenSpiro detects which device you've connected and picks a calculation model automatically. The active model is shown as a badge next to the session timer and is recorded with every saved session.
Three models are currently shipped:
Calibre Bio: wet-basis Haldane
The Calibre's gas sensors are not dried — readings include water vapor. OpenSpiro keeps every quantity on a wet basis, exactly matching the official Calibre Android app's calculation:
SVP(T) = 6.1 · exp(17.27 T / (T + 237.3)) // hPa, Magnus
H2O_exh = SVP(T_exh) · RH_exh / P_amb // expired water vapor fraction
H2O_amb = SVP(T_amb) · RH_amb / P_amb // ambient water vapor fraction
FIO2_wet = (1 − H2O_amb) · 0.2095 // inspired dry O2, wet-corrected
FICO2_wet = (1 − H2O_amb) · 0.0004
Haldane = (1 − FEO2 − FECO2 − H2O_exh)
/ (1 − FIO2_wet − FICO2_wet − H2O_amb)
STPD_num = (P_amb / (T_exh + 273.15)) · 0.2695 // partial STPD, no humidity term
VO2 = | FIO2_wet · Haldane − FEO2 | · V · STPD_num
VCO2 = ( FECO2 − FICO2_wet ) · V · STPD_num
Bit-identical to the Calibre app, breath by breath. The cost is that you can't apply textbook dry-basis formulas on top — they would double-count humidity.
VO2 Master: device-trusted
The VO2 Master computes VO2 and VCO2 internally and streams the finished values over BLE. OpenSpiro takes the device's VO2/VCO2 verbatim and only computes the derived quantities — RER, fat/CHO split, calories. The device's own calibration and assumptions feed in directly. If you suspect a systematic bias, recalibrate the device per the manufacturer's procedure; OpenSpiro doesn't second-guess.
Generic (fallback)
If a device is recognised as a metabolic analyzer but isn't yet in our model table, OpenSpiro falls back to the wet-basis formula. That's a reasonable default for any galvanic / electrochemical O2 sensor that reports wet readings, which is most of the market. As we add explicit support for new device families, this fallback gets narrower.
Sensor realities (the things that actually move your numbers)
The math is the easy part. Sensor behaviour, room conditions, and how the mask sits on your face contribute far more variance than the choice of formula. A few practical realities:
Sensor drift. Galvanic O2 cells slowly lose signal as their electrolyte depletes (typical lifetime 1–2 years of regular use). NDIR CO2 sensors drift with temperature and ambient pressure. If your analyzer supports calibration, run it before every session — otherwise expect a slow upward bias in VO2 over the sensor's life.
Sensor technologies. Worth knowing what's inside the box you bought:
- O2 — Galvanic cells (cheap, drift-prone, used in most portable units), paramagnetic (lab-grade, expensive), zirconia (fast, runs hot), optical / luminescence-quenching (driftless, fast response, no consumable parts — increasingly common in modern lab analyzers and next-generation portables).
- CO2 — NDIR (the standard for portable units; fast, low drift), Severinghaus electrodes (lab-only).
- Flow — Pneumotachograph (resistance-based, calibration-sensitive), differential pressure (common in portable analyzers, very sensitive to drafts), ultrasonic (expensive but essentially driftless).
Each technology trades off response time, drift, and absolute accuracy. The numbers your screen shows reflect whatever's in your device — software can't fix bad sensor data.
Drafts and air movement. Portable analyzers see breath-scale volume and pressure changes. Run sessions in a closed indoor room without fans or open windows. A direct breeze on the mask creates phantom flow on the sensor and pulls ambient air past the mask seal into the exhaled gas — VO2 will drift upward by 5–15% with even moderate cross-breeze. Compact portable analyzers like the Calibre Bio are especially affected because their flow sensors read tiny pressure differences. If you can feel air moving on your face, the calculation can't recover from it.
Humidity and water vapor. Exhaled air is fully saturated at ~37 °C, ~100% RH. Ambient air is usually drier (20–60% RH). Water vapor takes up "space" and dilutes the measured O2 and CO2 fractions. Get this correction wrong and VO2 swings by 5–20%. Per-device math handles this consistently — see the Calibre Bio block above.
Missing sensors. Not every analyzer measures every quantity. The VO2 Master, for example, reports only ambient temperature and humidity, not the exhaled gas conditions; for those OpenSpiro substitutes physiological estimates (33.5 °C, 95% RH) since exhaled gas is essentially body-temperature and saturated regardless. The Calibre measures both sides. When a sensor is missing, the calculation falls back to the best available substitute.
Masks and dead space. The mask between your face and the analyzer adds dead space — air that gets rebreathed instead of refreshed. Standard mouthpieces add ~30 mL; full-face masks add 100–250 mL. More dead space inflates apparent CO2 at low ventilation and slightly suppresses VO2. A well-fitting mask matters more than dead space — leaks are worse, because ambient air sneaks in undetected and pulls O2/CO2 fractions toward atmospheric values, which inflates VO2 in a way no calculation can correct for.
Calibration matters in two flavours. Span/zero on gas sensors — most devices auto-calibrate against ambient air at startup, fixing the reference at "20.93% O2, 0.04% CO2". If ambient air isn't actually that, your readings will be biased. Volume calibration — the flow sensor needs a 3-litre syringe pump or factory-set characterisation. Volume error scales VO2 directly: a 5% volume error becomes a 5% VO2 error. Without good calibration, expect day-to-day variability of ±5%. Trends across weeks are still meaningful; single-session absolute numbers, less so.
A note on RER smoothing
OpenSpiro deliberately doesn't replicate the official Calibre app's 300-breath sliding-window RER smoothing or its 0.85 Bayesian prior. We consider that approach a flaw: 300 breaths is roughly 20–25 minutes of exercise breath rates, far longer than RER's metabolic dynamics, so during a graded test the displayed RER lags the true value by minutes — long enough to miss the lactate-threshold inflection altogether. The 0.85 prior also biases every early-session display toward that value regardless of what the subject is actually doing.
OpenSpiro uses a Median(5) + EMA smoother instead (≈30 seconds, 5–10 breaths), which preserves transient detail while still suppressing single-breath sensor noise. Single-breath RER may briefly show values above 1.0 during high-intensity work as a result — that's expected and physiologically real, not an artifact.
Why the transparency matters
Every saved session in OpenSpiro records which calculation model produced its readings, and CSV / JSON exports tag the model and the OpenSpiro version. If you ever need to defend a number — to a coach, a sports scientist, or your future self three years from now — the math is fully traceable. The full per-device formula reference is built into the app under the Help tab.
References & further reading
- New methods for calculating metabolic rate with special reference to protein metabolism — Weir JB de V, J Physiol 1949
- Calculation of substrate oxidation rates in vivo from gaseous exchange — Frayn KN, J Appl Physiol 1983
- A new method for detecting anaerobic threshold by gas exchange — Beaver, Wasserman, Whipp, J Appl Physiol 1986
- Improved Magnus form approximation of saturation vapor pressure — Alduchov & Eskridge, J Appl Meteorol 1996
- Standardisation of spirometry — Miller, Hankinson, Brusasco et al., Eur Respir J 2005