Skip to content

Algo 01 Ascent Simulation

matejhron edited this page May 10, 2026 · 2 revisions

Algo-01 — Ascent Simulation

Replay a full dive waypoint array over all 16 ZH-L16 compartments and produce a time series of tissue pressures. This drives both chart rendering (the tissue-loading plot) and downstream ceiling computation.

Entry point

// js/decoModel.js:1178 (signature)
export function calculateTissueLoading(profile, surfaceInterval = 60, options = {})

Returns:

{
  timePoints: [...],          // minutes, at CALC_INTERVAL (10 s) resolution
  depthPoints: [...],         // meters, interpolated between waypoints
  ambientPressures: [...],    // bar
  alveolarN2Pressures: [...], // bar, post water-vapor correction
  n2Fractions: [...],         // reflects gas switches
  gasNames: [...],
  gasSwitches: [...],         // explicit {time, depth, fromGasName, gasName, gasId} events
  compartments: { 1: { pressures: [...], halfTime, label, color }, ..., 16: {...} }
}

CALC_INTERVAL = 10 seconds (js/decoModel.js:15); every 10 s a new row is pushed. Smaller steps do not meaningfully change the final tissue state — the Schreiner equation is exact for a constant rate, so the step only affects where samples are taken for plotting, not numerical accuracy.

Initial tissue state

// js/decoModel.js:93-95
export function getInitialTissueN2(n2Fraction = N2_FRACTION) {
    return getAlveolarN2Pressure(SURFACE_PRESSURE, n2Fraction);
}

For air this evaluates to $(1.01325 - 0.0627) \cdot 0.7902 \approx 0.7510$ bar. All 16 compartments are initialized to the same value — reasonable because an extended surface interval at constant breathing gas equilibrates every half-time ($6 T_{1/2}$ closes 98.4 % of the gradient; even TC16's 635 min half-time reaches near-equilibrium within a few days).

// js/decoModel.js:1254-1259
const currentPressures = {};
const initialN2Fraction = getN2FractionAtTime(0);
const initialN2 = getInitialTissueN2(initialN2Fraction);
COMPARTMENTS.forEach(comp => {
    currentPressures[comp.id] = initialN2;
});

Segment dispatch

Inside the main loop, DecoJS avoids calling Schreiner with rate = 0 (a numerically bad idea — the Schreiner form divides by $k$ and reduces to Haldane only in the limit). It explicitly dispatches:

// js/decoModel.js:1399-1424
const ambientRate = (nextAmbient - currentAmbient) / stepDuration;
const avgN2Fraction = (stepN2Fraction + nextN2Fraction) / 2;
const alveolarRate = ambientRate * avgN2Fraction;

COMPARTMENTS.forEach(comp => {
    if (Math.abs(alveolarRate) < 0.0001) {
        // Constant depth - use Haldane equation
        currentPressures[comp.id] = haldaneEquation(
            currentPressures[comp.id], currentAlveolar, stepDuration, comp.halfTime
        );
    } else {
        // Depth change - use Schreiner equation
        currentPressures[comp.id] = schreinerEquation(
            currentPressures[comp.id], currentAlveolar, alveolarRate, stepDuration, comp.halfTime
        );
    }
});

Threshold 0.0001 bar/min is tight enough that stop segments (rate exactly 0) always hit the Haldane branch. For a 3 m ascent at 10 m/min the alveolar rate is ≈ 0.079 bar/min — safely in the Schreiner branch.

Waypoint-aware stepping

The loop does not simply advance by 10 s — it snaps to the next waypoint time if a straight 10 s step would cross one:

// js/decoModel.js:1337-1352
let nextTime = currentTime + intervalMinutes;
const nextWaypointTime = (waypointIndex < profile.length - 1)
    ? profile[waypointIndex + 1].time
    : totalTime + 1;
if (currentTime < nextWaypointTime && nextTime > nextWaypointTime) {
    nextTime = nextWaypointTime;
}
if (currentTime < lastWaypoint.time && nextTime > lastWaypoint.time) {
    nextTime = lastWaypoint.time;
}

This guarantees each segment is driven by exactly one (wp_i, wp_{i+1}) pair — no half-step spans a descent/level boundary, which would blur the rate into something neither Haldane nor Schreiner models exactly.

Gas switches

Each waypoint may carry an optional gasId field. calculateTissueLoading reads this via the getN2FractionAtTime() closure (js/decoModel.js:1188-1208) — the current gas sticks until a waypoint with a new gasId is seen. Tissue pressure is continuous across a switch; only the alveolar target $P_{alv}$ changes, so the Schreiner rate for the next segment picks up the new $f_{N_2}$. See Algo-05-Multi-Gas-Switching for how these waypoints are produced.

Surface interval

The surfaceInterval parameter (default 60 min) appends depth = 0 time after the final waypoint, using air (N2_FRACTION = 0.7902) regardless of the final in-water gas:

// js/decoModel.js:1301-1303
const currentN2Fraction = currentTime > lastWaypoint.time
    ? N2_FRACTION  // Surface interval uses air
    : getN2FractionAtTime(currentTime);

This lets unit tests chain dives, though the current chart layer renders only dives[0] (see CLAUDE.md — repetitive-dive UI is on the roadmap).

Cross-references

Clone this wiki locally