Summary
For CCR (rebreather) dives, decompression and TTS are computed as if the diver breathed the first tank's gas open-circuit, ignoring the constant-ppO2 loop. This understates nitrogen loading, so the app reports a shorter TTS / shallower obligation than reality.
Example: dive 003 (test/dives/003_ccr_with_setpoint_switch_and_calculated_po2.ssrf.xml), GF 45/75, at minute 40 (44.1 m, loop ppO2 1.26 bar):
- App shows TTS 17 min
- Subsurface shows TTS 24 min
Root cause
A rebreather holds ppO2 at a constant setpoint, so the inspired inert-gas partial pressure is ambient − ppO2 (split by the diluent's He:N2 ratio). The app instead uses a fixed open-circuit fraction from the first tank:
lib/features/dive_log/presentation/providers/profile_analysis_provider.dart (~L648): o2Fraction is taken from dive.tanks.first.gasMix — for dive 003 that is EAN40 → fN2 = 0.60.
lib/features/dive_log/data/services/profile_analysis_service.dart (~L573-585): gas segments are only built for DiveMode.oc; for CCR/SCR it calls processProfile(fN2, fHe) with that fixed fraction.
- The resolved
rebreatherPpO2Curve is passed into analyze(...) but used only for CNS/OTU, never for inert-gas loading. BuhlmannAlgorithm has no CCR mode.
Modeling the loop as EAN40 (60% N2) loads far less nitrogen than the real air-diluent loop at a constant ~1.3 bar setpoint, so deco comes out too short.
Evidence (probe)
Parsing dive 003 and running the deco algorithm directly at minute 40:
| Model |
fN2 |
TTS @ min 40 |
| App's current CCR path (EAN40, first tank) |
0.60 |
16 min (matches displayed 17) |
| Air diluent as fixed OC fraction |
0.79 |
60 min |
| CCR constant-ppO2 (correct physics) |
ambient − ppO2 |
~24 min (Subsurface) |
The app's 16 min reproduces its displayed 17, confirming it models the CCR dive as open-circuit EAN40.
Proposed fix
Make the deco path CCR-aware:
- Drive inert-gas loading from
inspired_inert_pp = ambient − loopPpO2, split by the diluent cylinder's He:N2 ratio (not the first tank).
- Hold ppO2 at the setpoint through the TTS ascent simulation as well (inert fraction changes with depth as ppO2 stays constant), mirroring Subsurface.
- ppO2 source: measured O2 cells / dc-supplied ppO2, falling back to the setpoint — consistent with the existing CNS/OTU ppO2 source rule (
resolveRebreatherPpO2). The curve is already resolved and passed into analyze(...).
Scope: CCR first, its own PR. SCR is a follow-up (depth-varying loop FO2 rather than a fixed setpoint).
Notes
Summary
For CCR (rebreather) dives, decompression and TTS are computed as if the diver breathed the first tank's gas open-circuit, ignoring the constant-ppO2 loop. This understates nitrogen loading, so the app reports a shorter TTS / shallower obligation than reality.
Example: dive 003 (
test/dives/003_ccr_with_setpoint_switch_and_calculated_po2.ssrf.xml), GF 45/75, at minute 40 (44.1 m, loop ppO2 1.26 bar):Root cause
A rebreather holds ppO2 at a constant setpoint, so the inspired inert-gas partial pressure is
ambient − ppO2(split by the diluent's He:N2 ratio). The app instead uses a fixed open-circuit fraction from the first tank:lib/features/dive_log/presentation/providers/profile_analysis_provider.dart(~L648):o2Fractionis taken fromdive.tanks.first.gasMix— for dive 003 that is EAN40 →fN2 = 0.60.lib/features/dive_log/data/services/profile_analysis_service.dart(~L573-585): gas segments are only built forDiveMode.oc; for CCR/SCR it callsprocessProfile(fN2, fHe)with that fixed fraction.rebreatherPpO2Curveis passed intoanalyze(...)but used only for CNS/OTU, never for inert-gas loading.BuhlmannAlgorithmhas no CCR mode.Modeling the loop as EAN40 (60% N2) loads far less nitrogen than the real air-diluent loop at a constant ~1.3 bar setpoint, so deco comes out too short.
Evidence (probe)
Parsing dive 003 and running the deco algorithm directly at minute 40:
ambient − ppO2The app's 16 min reproduces its displayed 17, confirming it models the CCR dive as open-circuit EAN40.
Proposed fix
Make the deco path CCR-aware:
inspired_inert_pp = ambient − loopPpO2, split by the diluent cylinder's He:N2 ratio (not the first tank).resolveRebreatherPpO2). The curve is already resolved and passed intoanalyze(...).Scope: CCR first, its own PR. SCR is a follow-up (depth-varying loop FO2 rather than a fixed setpoint).
Notes