Skip to content

Deco Engine

The DiveSuite decompression engine implements the Buhlmann ZHL-16C algorithm using the dive-deco Rust crate, compiled to WebAssembly. WASM is disabled on the web today, and there is no native module on iOS yet. Until Rust computes there, a TypeScript fallback computes plans on every platform, within its refusal scope (bridge-fallback-scope.ts). The fallback is not validated against Subsurface. Decision D27 makes Rust the only deco engine (planned); see Engine availability.

We implement the Buhlmann ZHL-16C dissolved gas model via the dive-deco library (v6.0.6):

  • 16 tissue compartments with half-times from 4 to 635 minutes
  • M-values determine maximum tolerable tissue pressure
  • Gradient factors (GF) allow conservative adjustment (see the default gradient factors under Input Limits)
  • Trimix support for helium-based mixes

Why Buhlmann?

  • Well-documented, peer-reviewed algorithm
  • Used by most modern dive computers
  • Open specification (no licensing issues)
  • Subsurface uses the same algorithm (validation reference)
  • VPM-B is not available: the GPL-derived port was removed in #433, and a clean-room version is planned (D5, P1-17). A VPM-B request is refused with an AlgorithmUnavailable error; it is never answered with a Buhlmann result

The deco engine is written in Rust and compiled to WebAssembly:

Rust Source -> wasm-pack -> WASM Binary -> TypeScript Bridge -> App

On iOS the same Rust code is meant to run through a native UniFFI module (modules/deco-engine-native/). That module has not shipped yet.

Today: the TypeScript fallback computes plans on every platform, because WASM is disabled on the web. It refuses every input it cannot model and never supplies stops of its own. It is not extended. The bridge turns every Rust error except LEVEL_ABOVE_CEILING into a fallback plan.

Planned (D27): Rust becomes the only deco engine.

  • Rust computes on the web, and the fallback is removed as soon as it does. Native iOS follows with the UniFFI module (P1-15).
  • A persistent fallback notice shows while the fallback is active (P1-19).
  • Every Rust error maps to a refusal (P1-18).
  • Where no Rust engine runs (iOS until the native module ships, Android until after Release 1, Expo Go, a failed load or a crashed instance), the planner and every other screen that needs the engine show only a persistent, non-dismissible “engine unavailable” card that says nothing was checked (DIV-115). No plan, value or gas warning is shown in its place, not even from cache.
  • Tools whose Rust path has no reference test are hidden.
rust-engine/
├── src/
│ ├── lib.rs # Public API
│ ├── engine.rs, engine/ # Plan calculation on dive-deco: ascent, deco phases,
│ │ # gas switches, NDL, level ceiling, profile check,
│ │ # gas consumption, warnings, solver limits
│ ├── contract.rs, contract/ # Typed error and warning contract
│ ├── error.rs # DecoError
│ ├── types.rs, types/ # Input and output types
│ ├── validation.rs # Input validation
│ ├── gas.rs, gas/ # Gas physics (MOD, END, EAD, density, HPNS, ICD)
│ ├── cns_otu.rs # CNS% and OTU oxygen toxicity tracking
│ ├── altitude.rs, altitude/ # Altitude adjustments
│ ├── best_mix.rs # Best mix calculator
│ ├── blending.rs # Gas blending (partial pressure fill plans)
│ ├── ccr.rs, scr.rs # Closed-circuit and semi-closed rebreather planning
│ ├── dpv.rs # DPV planning
│ ├── reverse_planning.rs # Reverse planning and turn pressure
│ ├── surface_interval.rs # Tissue state, off-gassing, no-fly time
│ └── wasm/ # WASM exports
├── tests/ # Validation, property-based and robustness tests
├── pkg/ # Generated WASM output
└── Cargo.toml

There is no VPM-B module. See the algorithm section above.

The input and output types are defined once, in Rust (rust-engine/src/types.rs and rust-engine/src/contract.rs). The TypeScript bridge in src/core/engine/ mirrors them. The target (planned, P1-18) is that every failure path in the bridge refuses and returns no output. Today most Rust errors still become a fallback plan.

The safety specification sets these limits. What each layer enforces today differs for altitude:

Parameter Limit
Depth 0 to 300 m
Ascent rate 1 to 18 m/min
Descent rate 5 to 30 m/min
Altitude specified and decided (D19): at most 4000 m
Gradient factors GF Low at most GF High

For altitude, the TypeScript input validation refuses values above 4000 m. The Rust validation (MAX_ALTITUDE) still accepts up to 5000 m. This is a known engine bug that is being fixed; the limit is 4000 m. Other inputs are rejected outside range in Rust.

The app’s default gradient factors are 40/85 for recreational divers, 30/85 for technical and 35/80 for CCR (specification section 6.1).

  • Tissues, ceiling and no-fly time use 1.01325 bar, adjusted for altitude.
  • Bottom-gas MOD uses the site pressure (1.01325 bar adjusted for altitude). For example, EAN32 at ppO2 1.4 bar and 2000 m gives about 35.8 m.
  • Deco-gas MOD uses the 1-bar convention (D20).

The bottom-gas MOD warning in the Rust engine and the WASM calculate_mod still use 1 bar. They have to follow the site-pressure rule (D27) and do not yet.

The CCR module (ccr.rs) provides specialized calculations for closed-circuit rebreather diving:

Feature Function Description
Gas Consumption calculate_ccr_gas() Metabolic O2, diluent, scrubber tracking, bailout
Diluent Flush calculate_diluent_flush() Exponential decay model for loop composition change
Hypoxic Envelope calculate_hypoxic_envelope() Safe depth range for hypoxic trimix diluents
Setpoint Optimizer calculate_optimal_setpoint() Phase-based setpoint schedule for O2 savings
Bailout Optimizer calculate_bailout_optimizer() ICD/density/narcosis scoring for bailout gases

The DPV module (dpv.rs) calculates:

  • Maximum penetration distance (gas-limited or battery-limited)
  • Gas consumption during tow (reduced SAC: 60-70% of normal)
  • Swim-back bailout gas requirements if scooter fails at max penetration

The engine supports depth-segmented ascent rates via AscentRateSegment:

  • Each segment defines a rate for depths from 0 to max_depth
  • Falls back to a single ascent_rate if no profile is defined

Engine changes need an explicit owner request, their own PR, an independent safety review and validation against the references below. Every Rust function needs a unit test, with a target of 95%+ coverage, and invariants are covered by property-based tests (proptest).

Reference What is compared Target tolerance Status
dive-deco crate Buhlmann ZHL-16C coefficients Coefficient match Tested on engine changes
Subsurface (same algorithm, same gradient factors) Deco schedules Within 1 min per stop and 2 min total runtime (D5) Planned (golden set, P1-22)
Subsurface No-stop limits (NDL) Within 1 min Planned (golden set, P1-22)
PADI RDP, US Navy, NOAA tables No-stop limits Upper bound with no allowance: the engine’s NDL is never longer than the shortest published table value Planned (golden set, P1-22)

The golden set and the table bound are not running in CI yet (P1-22). When they run, the tables are a test bound and not a runtime clamp. The tables are not an accuracy target: PADI RDP uses a different algorithm (DSAT). The TypeScript fallback is not validated against Subsurface at all.

Tools that use VPM-B, such as V-Planner, produce different schedules by design and are not a reference for Buhlmann results.

The engine returns errors as typed values and does not panic on bad input. DecoError (rust-engine/src/error.rs) covers, among others:

  • invalid depth, bottom time, gas mix, O2 fraction, gradient factor, ascent rate, altitude and tank values
  • a ppO2 above the limit and a diver-set switch depth deeper than the gas’s MOD at 1.6 bar (refused, never clamped)
  • a plan that cannot be computed within solver limits
  • AlgorithmUnavailable, for example for VPM-B

A refused input produces no plan. The engine never returns a plausible-looking plan in its place.