How AirwayLab computes its metrics
Every number AirwayLab shows you comes from code you can read. The analysis engine is open source under GPL-3.0 at github.com/airwaylab-app/airwaylab. This page walks through each metric: what goes in, what the formula does, and where the colour bands sit. It also says what the number can and cannot tell you.
Two rules apply to everything below. Every metric carries a plain-language “what this means and does it matter” line. And nothing here is a medical conclusion or a therapy instruction. Your clinician can help interpret these findings in context.
This page documents the public engine snapshot, version 0.11.0 for the flow engines and 0.9.0 for oximetry, as recorded in lib/engine-version.ts. The hosted app at airwaylab.app runs engine 0.13.0. When a formula changes, the version bumps and your nights get re-analysed.
Where the hosted app is ahead of the public snapshot
Four changes are live in the hosted app and have not yet been published to the engine repository. Everything else on this page matches both.
- FL Score colour bands sit at 60 and 70 instead of 30 and 50, because the night-average FL Score clusters around 54 even for unobstructed breathing. The thresholds table below already shows the live bands.
- IFL Symptom Risk rescales the FL Score against that floor of 54 before weighting, so unobstructed breathing adds no risk. This is version 2 of the composite formula.
- BiPAP premature and late cycling are scored from when pressure leaves the IPAP plateau relative to the flow zero-crossing: late when more than 50 ms after it, premature when more than 300 ms before it. The public snapshot uses the pressure-threshold rule described below.
- Breath detection in the NED engine gained an onset floor and an amplitude gate against a 120-second baseline, which drops fewer real breaths on noisy nights.
Disclaimer
AirwayLab is not a medical device. It is not FDA or CE cleared. All analysis is for informational and educational purposes only. Always discuss results with your sleep physician before making therapy changes.
Which numbers come from the device and which does AirwayLab compute?
Your machine already reports a set of numbers in its summary file (STR.edf on ResMed). AirwayLab shows those unchanged. Everything else is computed from the raw flow waveform, the pressure channel, or your oximeter file.
| Source | Metrics |
|---|---|
| Reported by the device, shown unchanged | AHI, hypopnea index, obstructive and central apnea indices, machine RERA index, Cheyne-Stokes %, leak (50th/70th/95th percentile and max), minute ventilation, respiratory rate, tidal volume, inspiratory time, I:E ratio, mask pressure, usage hours, mask on/off events, prescribed settings (mode, pressures, EPR, rise time, trigger and cycle sensitivity). Reader: lib/parsers/machine-summary-extractor.ts. |
| Computed by AirwayLab from the 25 Hz flow waveform | Glasgow Index, WAT scores (FL Score, Regularity, Periodicity), NED, Flatness Index, RERA-like sequence index, Estimated Arousal Index, algorithm hypopneas and brief obstructions, amplitude stability, IFL Symptom Risk, Estimated RDI. |
| Computed by AirwayLab from flow plus pressure (bilevel only) | Detected EPAP and IPAP, trigger delay, auto-trigger %, time at IPAP, premature and late cycling, tidal-volume proxy and its variability, end-expiratory pressure. |
| Computed by AirwayLab from a device signal | Spontaneous % and Timed % on AirCurve bilevel machines. Read from the per-breath trigger/cycle channel and aggregated by us. The event-code mapping comes from the OSCAR project and has not been validated against an independent reference. |
| Computed by AirwayLab from your oximeter CSV | ODI-3, ODI-4, time below 90% and 94%, mean and minimum SpO2, heart-rate surges, coupled desaturation-and-surge events. |
Plain-language line: If a number matches your machine’s own report, it is the device’s number. If it is new to you, we computed it and the sections below say how.
How does AirwayLab find each breath?
Every flow-based metric starts by cutting the night into breaths. The flow signal is sampled 25 times per second. An inspiration starts where flow crosses zero going up and ends where it crosses zero going down. Breaths shorter than about 0.3 to 0.4 seconds, or with almost no flow, are dropped as noise. Gaps in the recording, for example mask-off periods, are kept as gaps and never bridged.
Each engine keeps its own segmenter with slightly different minimums, so per-breath counts can differ by a few breaths between engines. Code: glasgow-index.ts, wat-engine.ts, ned-engine.ts.
Plain-language line: Breath detection is the foundation. If your night has heavy leak or a lot of mask-off time, fewer clean breaths get scored and every metric on this page gets noisier.
How is the Glasgow Index computed?
Ported from the open-source GlasgowIndex tool by DaveSkvn (GPL-3.0). Each breath is checked for nine shape traits. Each trait becomes a score from 0 to 1, the share of breaths that show it. The nine scores are summed, so the overall index runs from 0 to 9. The original tool summarised on a 0 to 3 scale. We keep the full sum for granularity, which is why typical nights score 0 to 3 and scores above 3 are rare.
| Component | What it checks | Flags a breath when |
|---|---|---|
| Skew | Left/right volume asymmetry | left-half volume share is below 45% or above 55% |
| Top heavy | Volume in the top 90% band | more than 40% of the breath sits in the top band |
| Flat top | Variance of the middle 50% of the inspiration | variance below 0.75 |
| Spike | Sharp early peak | less than 20% of the breath sits in the top 90% band |
| Multi-peak | Extra bumps on the inspiratory curve | a second peak bump is found |
| No pause | Rest between expiration end and next inspiration | fewer than 10 samples of rest |
| Inspiratory rate | Breaths per minute | more than 20 |
| Multi-breath | Inspiration without a matching exhale | no linked exhale found |
| Variable amplitude | 5-breath rolling amplitude variance | variance above 4 |
Colour bands: green up to 1.0, amber up to 2.0, red above. Multi-session nights are averaged by duration.
Plain-language line: The Glasgow Index counts how many of your breaths look distorted. A higher score means more breaths with an odd shape. We now rank it below the direct NED analysis, because NED reads the same thing straight from the 25 Hz signal. Treat Glasgow as a second opinion, not the headline.
How are the WAT scores computed?
WAT stands for Wobble Analysis Tool. It produces three scores, each 0 to 100.
| Metric | Computed from | How | Range | What this means and does it matter |
|---|---|---|---|---|
| FL Score | Each inspiration’s top half (samples above 50% of peak) | Variance of that top region. Score = (0.05 - variance) / 0.05 x 100, clamped 0 to 100, then averaged over the night. A flat top gives low variance and a high score. | 0-100 | An uncalibrated relative index. Scores cluster between roughly 50 and 65 even for unobstructed breathing. It is not a percentage of flow-limited breaths. Compare your own nights, never the absolute number. |
| Regularity Score | Minute ventilation in 60-second windows stepped every 5 seconds | Sample Entropy (m = 2, r = 0.2 x SD) of that series. Score = 100 - (SampEn / 2.5) x 100, clamped. | 0-100 | Higher means a steadier breathing rhythm through the night. No established clinical threshold exists for it. |
| Periodicity Index | Same minute-ventilation series, detrended | FFT power in the 0.01 to 0.03 Hz band (30 to 100 second cycles) as a share of total power, x 200, capped at 100. | 0-100 | Higher means more of your ventilation swings in slow waves, the pattern seen in periodic breathing. No established clinical threshold exists for it. |
Both series metrics return zero when the night is too short for four windows. Colour bands for the FL Score are in the thresholds table below. Regularity and Periodicity share the same shape at 30/50 and 20/40. Reference: Richman and Moorman (2000) for Sample Entropy. Code: wat-engine.ts.
How are NED, Flatness Index and RERA-like sequences computed?
NED is negative effort dependence: more effort, less air. For each breath, AirwayLab compares peak inspiratory flow with the flow at the midpoint of inspiration.
| Metric | Unit | How | Cut-offs |
|---|---|---|---|
| NED | % | (Qpeak - Qmid) / Qpeak x 100, where Qmid is the flow at 50% of inspiratory time | Clear flow limitation at 34% or more, borderline 10 to 34% |
| Flatness Index (FI) | ratio 0-1 | Mean inspiratory flow divided by peak flow | 0.85 or more counts as flow limited |
| Combined flow limitation % | % of breaths | NED at or above 34% OR FI at or above 0.85 | green up to 20%, amber up to 40% |
| M-shape % | % of breaths | A dip below 80% of peak between 25% and 75% of inspiration, with flow above threshold on both sides | No established clinical threshold |
| Early-peak % (Tpeak/Ti) | % of breaths | Peak reached before 45% of inspiratory time | Context only, see limitations |
| RERA-like sequence index | events per hour | Runs of 3 to 15 consecutive breaths with NED above 20% or FI at or above 0.85. A run counts when any one holds: NED rises along the run (slope above 0.5). The run ends in a recovery breath (NED below 10%) plus a sigh (peak above 1.5 x mean). Any breath in the run passes 34%. | green up to 5, amber up to 10 |
Colour bands for NED mean: green up to 15%, amber up to 25%. Code: ned-engine.ts.
Plain-language line: NED tells you how often the airway narrows mid-breath. The RERA-like index counts how often that narrowing builds up over several breaths and then releases. These are the metrics we trust most from the flow signal, with one big caveat: they move when your machine’s Rise Time setting changes, so compare nights on the same settings.
How is the Estimated Arousal Index computed?
The Estimated Arousal Index (EAI) looks for the breathing signature of an arousal without any EEG. An event needs all of the following:
- At least 2 of the previous 5 breaths were flow limited (NED 20% or more, or FI 0.85 or more).
- Breathing rate jumps more than 35% above its 120-second rolling baseline.
- Breath volume jumps more than 50% above that baseline.
After an event, 30 seconds pass before another can count. Index = events per hour.
Colour bands: green up to 5, amber up to 10.
Plain-language line: EAI is a secondary marker. Flow-based estimates like this one typically read 2 to 3 times higher than an in-lab arousal index measured with EEG (Mansour et al. 2019, Jordan et al. 2011). Use it for trend, not for comparison with a sleep-study report.
How are hypopneas and the Estimated RDI computed?
Hypopnea index has two paths. If your SD card carries the machine’s event file (EVE.edf), AirwayLab uses the device’s own hypopnea count. Otherwise it falls back to an algorithm: peak flow is compared with a rolling 30-breath median, and a drop of 30% or more that lasts 10 seconds or longer counts as a hypopnea. A drop of more than 40% lasting only one or two breaths is logged separately as a brief obstruction. Each event is also checked for whether NED saw it, and events with NED below 34% are flagged as “NED-invisible”.
Estimated RDI = RERA-like index + hypopnea index. The AASM definition of RDI also includes apneas. AirwayLab cannot identify apneas from flow alone, so this is a conservative lower bound. It is closest to a full RDI when apneas are rare.
Colour bands: hypopnea index green up to 2, amber up to 5. Brief obstructions green up to 3, amber up to 6. Estimated RDI green up to 5, amber up to 15.
Plain-language line: Estimated RDI is the floor, not the ceiling. If your machine reports apneas, add them mentally. Code: derived-metrics.ts.
How is IFL Symptom Risk computed?
IFL Symptom Risk is a weighted blend of four flow metrics on a 0 to 100 scale:
0.35 x FL Score + 0.30 x NED mean + 0.20 x normalised FI + 0.15 x normalised Glasgow
In the hosted app the FL Score term is first floor-rescaled, see the version note at the top of this page.
Normalised FI = (FI mean - 0.5) / 0.5 x 100, floored at 0. Normalised Glasgow = Glasgow / 9 x 100. The weights follow the inspiratory-flow-limitation literature (Gold’s IFL model, Mann et al. 2024 on flow limitation and daytime sleepiness). Colour bands: green up to 20, amber up to 45. Code: ifl-risk.ts.
Plain-language line: This is one composite number that rolls up “how flow limited was the night”. It inherits every limitation of its four inputs, including the uncalibrated FL Score. When IFL Risk and EAI disagree, the app says so and your clinician can help interpret these findings in context.
How are the oximetry metrics computed?
Oximetry is optional and needs a separate CSV from a Viatom / Wellue ring (Checkme O2 Max, O2Ring). The recording is cleaned first:
- The first 15 and last 5 minutes are trimmed.
- Samples with motion above 5 are dropped.
- SpO2 outside 50 to 100 is dropped.
- Heart-rate values that look like double-counting are corrected or dropped.
A night needs at least 60 clean samples or it is reported as not analysed. The app shows how many samples survived.
| Metric | Unit | How | Green / amber |
|---|---|---|---|
| ODI-3 and ODI-4 | events per hour | SpO2 drops of 3% (or 4%) below a rolling 2-minute baseline peak, with a 30-second cooldown | 5 / 15 and 3 / 10 |
| Time below 90% and below 94% | % of night | Share of clean samples under the threshold | 5 / 15 and 10 / 30 |
| Mean and minimum SpO2 | % | Plain mean and minimum of clean samples | mean green at 95 or above, amber at 92 |
| Heart-rate surges (8, 10, 12, 15 bpm) | events per hour | HR above a 30-second rolling baseline by the threshold, 30-second cooldown | HR surge 10 bpm: 10 / 20 |
| Coupled events | events per hour | A desaturation with a heart-rate surge (6 or 10 bpm) within 30 seconds either side | context |
| Coupling with RERA-like sequences | % | Clock offset between machine and ring is estimated by testing shifts of up to 120 seconds and a 5 to 45 second physiological delay. Needs at least 10 RERA-like events. | context |
Code: oximetry-engine.ts, cross-device-engine.ts.
Plain-language line: ODI and time below 90% follow standard definitions and are the closest thing on this page to numbers a sleep lab would recognise. The ring is a consumer sensor and the cleaning steps are ours, so the values are comparable across your nights, not with a lab study. The clock alignment between ring and machine is a best-guess match and gets a “low confidence” label when fewer than a quarter of events line up.
How are the BiPAP settings metrics computed?
On bilevel machines AirwayLab reads the pressure channel next to flow. EPAP and IPAP are detected as the 10th and 90th percentile of pressure across the night. The engine only runs when the gap between them is at least 1 cmH2O. Breaths are re-segmented on a smoothed flow signal and kept when they last 1 to 10 seconds. Trigger delay is the time from flow onset until pressure passes 15% of the way from EPAP to IPAP. A breath is auto-triggered when pressure is already rising before flow starts. Time at IPAP counts samples at or above 90% of pressure support. Premature cycling is pressure dropping below the halfway point while flow is still above 25% of peak. Tidal volume is the integral of inspiratory flow.
The hosted app scores cycle timing from plateau departure instead, see the version note at the top of this page.
Colour bands: trigger delay green up to 300 ms, amber up to 500 ms. Auto-trigger green up to 2%, amber up to 5%. Premature and late cycling green up to 2%, amber up to 10%. Tidal-volume variability green up to 25%, amber up to 30%. Code: settings-engine.ts.
Plain-language line: These describe how well your breaths and the machine’s pressure changes line up. Absolute millilitre values depend on your device’s flow calibration, so use the night-over-night change and the variability percentage, not the raw volume.
What do the green, amber and red bands mean?
Every metric gets a directional label: lower, typical, or higher. The cut-offs are working thresholds we set from the research literature and our own data. They are not clinical reference ranges and no regulator has reviewed them. The full table lives in lib/thresholds.ts. The table below is generated from the same thresholds file this site runs, so it always matches the hosted app. You can override any threshold in the app settings, which stores the override in your browser only.
Outcome
| Metric | Green up to | Amber up to | Direction |
|---|---|---|---|
| HR surge, 10 bpm (events/hr) | 10 | 20 | lower is better |
| ODI-3 (events/hr) | 5 | 15 | lower is better |
| ODI-4 (events/hr) | 3 | 10 | lower is better |
| Time below 90% SpO2 (%) | 5 | 15 | lower is better |
| Time below 94% SpO2 (%) | 10 | 30 | lower is better |
| Mean SpO2 (%) | 95 | 92 | higher is better |
| Coupled desaturation-and-surge events (%) | 30 | 50 | lower is better |
| RERA-like coupling, night 2nd half (%) | 10 | 25 | lower is better |
Pattern
| Metric | Green up to | Amber up to | Direction |
|---|---|---|---|
| IFL Symptom Risk (0-100) | 20 | 45 | lower is better |
| NED mean (%) | 15 | 25 | lower is better |
| NED 95th percentile (%) | 30 | 50 | lower is better |
| NED clear flow limitation (%) | 2 | 5 | lower is better |
| Combined flow limitation (%) | 20 | 40 | lower is better |
| RERA-like sequence index (events/hr) | 5 | 10 | lower is better |
| Estimated Arousal Index (events/hr) | 5 | 10 | lower is better |
| Brief obstructions (events/hr) | 3 | 6 | lower is better |
| Hypopnea index (events/hr) | 2 | 5 | lower is better |
| Amplitude variability, CV (%) | 20 | 30 | lower is better |
| Unstable amplitude epochs (%) | 15 | 25 | lower is better |
| Estimated RDI (events/hr) | 5 | 15 | lower is better |
Context
| Metric | Green up to | Amber up to | Direction |
|---|---|---|---|
| Glasgow Index (0-9) | 1 | 2 | lower is better |
| FL Score, WAT (0-100) | 60 | 70 | lower is better |
| Regularity Score, WAT (0-100) | 30 | 50 | lower is better |
| Periodicity Index, WAT (0-100) | 20 | 40 | lower is better |
Device
| Metric | Green up to | Amber up to | Direction |
|---|---|---|---|
| Machine-reported AHI (events/hr) | 5 | 10 | lower is better |
| Leak, 95th percentile (L/min) | 24 | 40 | lower is better |
| Spontaneous cycling (%) | 80 | 60 | higher is better |
| Trigger delay (ms) | 300 | 500 | lower is better |
| Auto-trigger rate (%) | 2 | 5 | lower is better |
| Inspiratory time, Ti (ms) | 1200 | 1000 | higher is better |
| I:E ratio | 1.2 | 1 | higher is better |
| Time at IPAP (ms) | 600 | 400 | higher is better |
| IPAP dwell (%) | 45 | 35 | higher is better |
| Premature cycling (%) | 2 | 10 | lower is better |
| Late cycling (%) | 2 | 10 | lower is better |
| Tidal-volume variability, CV (%) | 25 | 30 | lower is better |
| EPAP stability, delta (cmH2O) | 0.5 | 1 | lower is better |
Plain-language line: A colour is a reading aid, not a verdict. Two greens and one red on the same night means “look at the red one with your clinician”, nothing more.
Are these metrics calibrated or clinically validated?
No. This is the honest answer we owe you, and the one we gave on r/SleepApnea.
- None of the flow-derived scores have been calibrated against polysomnography or against a scored sleep study.
- The thresholds are our working cut-offs, not clinical reference ranges.
- Oximetry metrics follow standard definitions but come from a consumer ring and our cleaning pipeline.
- Machine AHI is the device’s own number. It is reliable when elevated and misses flow limitation and RERA-like events when normal.
- The engines are deterministic: the same night always scores the same. That makes them a consistent ruler, which is why trends across your own nights carry more meaning than any absolute value.
- When we fix a scoring error, it goes in the public changelog and the engine version bumps.
We grade our own metrics by reliability in lib/metric-registry.ts. Tier 1 is the oximetry set. Tier 2 is the NED family. Tier 3 is context only: Glasgow, the WAT scores, M-shape and Tpeak/Ti. That file is the same grading the app uses.
Plain-language line: Use AirwayLab to see patterns in your own data and to bring better questions to your appointment. Your clinician can help interpret these findings in context.
What are the known limitations?
- Rise Time changes the numbers.NED, FI and the RERA-like index shift when your machine’s Rise Time setting changes, because Rise Time changes peak flow. Compare nights on the same settings.
- Tpeak/Ti is a BiPAP artifact. Across 385,000 bilevel breaths, 94% peaked before 45% of inspiration. On bilevel machines this metric describes the pressure profile, not your airway.
- Glasgow is superseded. It predates the direct NED analysis and stays for continuity.
- FL Score is uncalibrated. It sits around 50 to 65 on unobstructed breathing and is not a percentage of anything.
- EAI over-counts versus EEG. Expect 2 to 3 times a lab arousal index.
- Estimated RDI is a lower bound. Apneas are not identified from flow.
- Hypopneas depend on the file set. With EVE.edf we use the device count, without it the algorithm.
- Spontaneous and Timed percentages rely on an event-code mapping borrowed from OSCAR that we have not validated independently.
- Ring-to-machine clock alignment is a heuristic and is labelled low confidence when the match is weak.
- Tidal volume in millilitresdepends on the device’s flow calibration.
- Heavy leak or long mask-off periods reduce the number of clean breaths and make everything noisier.
- Device support differs. Supported devices and their known gaps are listed in the device guides.
How can I check the code myself?
The engine is public. Start at the repository, then:
- lib/analyzers/ - Glasgow, WAT, NED, oximetry, settings and cross-device engines
- lib/thresholds.ts - every colour band
- lib/metric-registry.ts - our reliability tiers per metric
- lib/metric-explanations.ts - the plain-language lines the app shows
- lib/parsers/ - how SD-card and oximeter files are read
- CHANGELOG.md - every scoring fix
Found a scoring error? Open an issue. We fix it in the open.