Summary
DiveState carries a single gas_fraction for the whole dive, stops included. Real ascents switch to an oxygen-rich mix at a planned depth — EAN50 at 21 m, oxygen at 6 m are the conventional ones — and most of the reduction in stop time comes from exactly that. The schedules this tool produces are therefore correct for "the whole dive on back gas" and considerably longer than any plan a diver would actually run.
Why it matters
The bottom gas is the wrong gas to decompress on. Dropping the inert fraction at a stop widens the gradient between tissue and inspired pressure, which is what makes shallow stops clear quickly. Without it:
- reported stop times overstate the real obligation, sometimes by a large factor at the 6 m and 3 m stops;
--gas ean40 looks strictly better than --gas air on deco while being undiveable at depth (see the MOD_EXCEEDED finding), when the real answer is to dive air and switch to EAN40 on the way up;
- there is no way to express the standard recreational and technical ascent plans at all.
Design sketch
A gas plan is a list of (switch_depth, gas_fraction) ordered shallow-to-deep, resolved to "the richest mix breathable at this depth". The natural home is DiveState, since it already owns the gas:
@dataclass
class GasPlan:
stages: list[tuple[float, float]] # (max_depth, n2_fraction)
def at_depth(self, depth: float) -> float: ...
DiveState.gas_fraction becomes a property resolving the plan at the current depth, so step_at_pressure and load_gas_at_pressure need no signature change. The ascent walk is where the care is: walk_ceiling_to_surface currently threads one gas_fraction through the whole generator, and it would need to resolve per step instead.
Scope
configs/limits.py — standard stage presets alongside GAS_MIXES.
model/dive_state.py — GasPlan, and gas_fraction resolved from depth.
model/splinter_decompression.py — walk_ceiling_to_surface and calculate_deco_schedule take the plan rather than a scalar fraction.
processors/dive_analysis.py — _check_ppo2 must validate each stage at its own depth range, not one fraction against the whole profile. A switch gas is by design over its own MOD at depth; the check has to become "breathable where it is actually breathed", which is a real change in meaning, not a refactor.
handlers/profile_analyser.py — a --deco-gas DEPTH:MIX flag, repeatable.
README.md — the gas section, and the "Comparing breathing gases" scenario, which currently draws the wrong conclusion for want of switching.
Open questions
- Switch time. A real switch costs a minute or two at the stop for the valve drill and a buddy check. Model it as a fixed cost, or ignore it?
Ignoring it is optimistic in the same direction as everything else here.
- CNS / OTU. Not modelled today (
_check_ppo2 bounds partial pressure only). Deco on oxygen is where the CNS clock actually bites, so switching makes the absence of that accounting more visible. Probably its own issue.
- Interacts with the helium issue: a trimix plan without switching is not a plan. Sequencing them together is likely cheaper than doing either twice.
Summary
DiveStatecarries a singlegas_fractionfor the whole dive, stops included. Real ascents switch to an oxygen-rich mix at a planned depth —EAN50at 21 m, oxygen at 6 m are the conventional ones — and most of the reduction in stop time comes from exactly that. The schedules this tool produces are therefore correct for "the whole dive on back gas" and considerably longer than any plan a diver would actually run.Why it matters
The bottom gas is the wrong gas to decompress on. Dropping the inert fraction at a stop widens the gradient between tissue and inspired pressure, which is what makes shallow stops clear quickly. Without it:
--gas ean40looks strictly better than--gas airon deco while being undiveable at depth (see theMOD_EXCEEDEDfinding), when the real answer is to dive air and switch to EAN40 on the way up;Design sketch
A gas plan is a list of
(switch_depth, gas_fraction)ordered shallow-to-deep, resolved to "the richest mix breathable at this depth". The natural home isDiveState, since it already owns the gas:DiveState.gas_fractionbecomes a property resolving the plan at the current depth, sostep_at_pressureandload_gas_at_pressureneed no signature change. The ascent walk is where the care is:walk_ceiling_to_surfacecurrently threads onegas_fractionthrough the whole generator, and it would need to resolve per step instead.Scope
configs/limits.py— standard stage presets alongsideGAS_MIXES.model/dive_state.py—GasPlan, andgas_fractionresolved from depth.model/splinter_decompression.py—walk_ceiling_to_surfaceandcalculate_deco_scheduletake the plan rather than a scalar fraction.processors/dive_analysis.py—_check_ppo2must validate each stage at its own depth range, not one fraction against the whole profile. A switch gas is by design over its own MOD at depth; the check has to become "breathable where it is actually breathed", which is a real change in meaning, not a refactor.handlers/profile_analyser.py— a--deco-gas DEPTH:MIXflag, repeatable.README.md— the gas section, and the "Comparing breathing gases" scenario, which currently draws the wrong conclusion for want of switching.Open questions
Ignoring it is optimistic in the same direction as everything else here.
_check_ppo2bounds partial pressure only). Deco on oxygen is where the CNS clock actually bites, so switching makes the absence of that accounting more visible. Probably its own issue.