-
Notifications
You must be signed in to change notification settings - Fork 0
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.
// js/decoModel.js:1040 (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.
// js/decoModel.js:93-95
export function getInitialTissueN2(n2Fraction = N2_FRACTION) {
return getAlveolarN2Pressure(SURFACE_PRESSURE, n2Fraction);
}For air this evaluates to
The actual seeding code (js/decoModel.js:1115–1126) checks for options.initialTissuePressures first:
// js/decoModel.js:1115-1126
const seededPressures = options.initialTissuePressures || null;
COMPARTMENTS.forEach(comp => {
currentPressures[comp.id] = seededPressures
? seededPressures[comp.id]
: initialN2;
});For a repetitive dive the diver enters the water with residual nitrogen from the previous dive. Callers pass a { [compartmentId]: nitrogenPressureBar } map in options.initialTissuePressures to seed every compartment from the prior-dive tissue state rather than surface equilibrium. This is how js/tripPlanner.js chains tissue loading across dives: the endTissue map from dive N is surface-off-gassed with simulateDepthTime, and the result is passed as initialTissuePressures to calculateTissueLoading (and to generateDecoProfile) for dive N+1. When the option is absent, behaviour is unchanged — surface equilibrium via getInitialTissueN2.
Inside the main loop, DecoJS avoids calling Schreiner with rate = 0 (a numerically bad idea — the Schreiner form divides by
// js/decoModel.js:1267-1289
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.
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:1204-1218
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.
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
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:1169-1171
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).
- Model-02-Haldane-Equation and Model-03-Schreiner-Equation — the equations being dispatched.
-
Algo-06-Ceiling-Time-Series — consumes the
resultsobject fromcalculateTissueLoadingto overlay ceilings. -
Architecture —
calculateTissueLoadingis the main hot-path consumed by every chart component.