-
Notifications
You must be signed in to change notification settings - Fork 0
Model 05 Gradient Factors
Bühlmann M-values represent the DCS-threshold supersaturation. Diving right up to M is "on the line" — any error, poor perfusion, or individual susceptibility and you cross over. Gradient Factors (Erik Baker, 1998; see References) add a configurable safety margin by treating the supersaturation budget as a fraction of the raw Bühlmann allowance. GF 100/100 means Bühlmann-native. GF 30/70 means deeper first stops (30% of the supersaturation budget used when the first stop triggers) and a more conservative surfacing (only 70% used at the surface).
-
$GF_{low}$ : fraction of the supersaturation budget permitted at the first-stop depth. -
$GF_{high}$ : fraction permitted at the surface.
Between first stop and surface, GF is linearly interpolated. Below first-stop depth,
The UI and dive-setup JSON store GF as percent; the algorithmic core uses fractions. Conversion happens at the boundary.
// js/diveSetup.js (DEFAULT_GF_LOW, DEFAULT_GF_HIGH)
DEFAULT_GF_LOW = 100, DEFAULT_GF_HIGH = 100// js/decoModel.js:33-34
export const DEFAULT_GF_LOW = 1.0; // 100%
export const DEFAULT_GF_HIGH = 1.0; // 100%Any function in decoModel.js taking a gf parameter expects the 0–1 form.
This is where implementations diverge. Two approaches are both called "GF interpolation":
- Depth-based ramp (naive / simplified): GF linearly interpolates from the chosen first-stop depth to the surface. The anchor is the depth of the first mandatory stop.
-
pAnchor-based ramp (Baker-intended; decotengu; DecoJS): GF interpolates from the ambient pressure where
$GF_{max}$ across all 16 compartments first reaches$GF_{low}$ during a simulated free ascent. Not from the stop depth.
The difference matters. Two dives with the same max depth but different bottom times have different tissue loadings, so they hit
// js/decoModel.js:257 (signature)
export function findGFLowAnchor(tissuePressures, currentDepth, n2Fraction, gfLow,
ascentRate = ASCENT_SPEED, gasSwitchPoints = null)Simulates an ascent from currentDepth in 0.1-bar steps (~1 m precision). At each step it runs Schreiner over all 16 compartments for the segment, then computes
// js/decoModel.js:308-321
if (gfMax >= gfLow) {
const comp = COMPARTMENTS.find(c => c.id === leadingCompartment);
const tissuePressure = tissues[leadingCompartment];
// pAnchor = ceiling at GF_low = (Pt - GF_low * a) / (GF_low/b + 1 - GF_low)
const exactPAnchor = getCompartmentCeiling(tissuePressure, comp.aN2, comp.bN2, gfLow);
const exactAnchorDepth = Math.max(0, (exactPAnchor - SURFACE_PRESSURE) / PRESSURE_PER_METER);
const finalPAnchor = Math.max(SURFACE_PRESSURE, exactPAnchor);
// ...
return { pAnchor: finalPAnchor, anchorDepth: finalAnchorDepth, leadingCompartment, tissuesAtAnchor };
}Returns:
{ pAnchor, anchorDepth, leadingCompartment, tissuesAtAnchor }If the simulated ascent reaches the surface without hitting SURFACE_PRESSURE (js/decoModel.js:338-345) — which means the dive is within NDL and no ramp applies.
Once pAnchor is fixed, the GF at any ambient pressure is:
// js/decoModel.js:424-444
export function interpolateGF(currentAmbient, pAnchor, gfLow, gfHigh) {
if (currentAmbient >= pAnchor) {
return gfLow;
}
if (currentAmbient <= SURFACE_PRESSURE) {
return gfHigh;
}
const range = pAnchor - SURFACE_PRESSURE;
if (range <= 0) {
return gfHigh;
}
const fraction = (pAnchor - currentAmbient) / range;
return gfLow + fraction * (gfHigh - gfLow);
}Note the denominator uses SURFACE_PRESSURE = 1.01325 conceptually but the formula above uses 1.0 bar for brevity — the code uses the exact constant. Numerically the difference is ~1.3%; algorithmically it's deliberate (the ramp converges to the surface atmospheric pressure, not to 1 bar).
The GFChart (js/charts/GFChart.js) renders this as a shaded band:
- Left edge at pAnchor, height =
$GF_{low}$ . - Right edge at surface, height =
$GF_{high}$ . - Linear ramp between.
- Per-compartment tissue trails show how each compartment's instantaneous GF evolved through the dive. The leading tissue at any given moment is the one whose trail is highest — that's the one that constrains the next ascent step.
The M-value chart in js/mvalues.js renders the same ramp projected onto the P-P diagram: a segment from (pAnchor, M_adj at GF_low) to (1.0 bar, M_adj at GF_high).
During ascent, switching from back gas to a richer deco mix abruptly changes findGFLowAnchor() takes a gasSwitchPoints parameter so its simulated ascent respects these switches — otherwise pAnchor would be computed assuming back gas all the way up and come out wrong:
// js/decoModel.js:274-296
const switches = gasSwitchPoints
? [...gasSwitchPoints].sort((a, b) => b.switchDepth - a.switchDepth)
: [];
let currentN2 = n2Fraction;
// ... inside the ascent loop ...
for (const sp of switches) {
if (nextDepth <= sp.switchDepth && sp.n2 < currentN2) {
currentN2 = sp.n2;
break;
}
}Switches are sorted deepest-first so the simulation applies the richest permissible gas at each depth. generateDecoSchedule() (js/decoModel.js:899) is the caller that passes in the switch points — see Algo-04-Deco-Stop-Loop for how they're computed from MODs.
40 m dive on air, 25 min bottom time, GF 30/85.
generateDecoProfile() in js/diveSetup.js runs the full pipeline:
- Simulate descent + bottom time via
calculateTissueLoading(). - Call
findGFLowAnchor()with the final tissues andgfLow = 0.30. -
findGFLowAnchor()steps up in 0.1-bar increments. For this exposure, the leading tissue is typically TC4 or TC5; pAnchor comes out around 2.4 bar (roughly 14 m depth). - At
$P_{amb} = 2.4$ bar,$GF = 0.30$ (just hits the GF-low ceiling). - At
$P_{amb} = 1.0$ bar (surface),$GF = 0.85$ . - Between: linear ramp. E.g., at 6 m (
$P_{amb} = 1.61$ bar),$GF = 0.30 + (0.85 - 0.30) \cdot (2.4 - 1.61)/(2.4 - 1.01) = 0.30 + 0.55 \cdot 0.568 = 0.612$ .
Exact numbers depend on the variant (A/B/C) and the descent/ascent profile shape, but 14 m ± 2 m is the typical pAnchor range for a 40 m / 25 min air dive with
-
Model-04-M-Values —
$M$ ,$M_{adj}$ , and the ceiling equation that pAnchor is computed from. - Algo-03-First-Stop-Ramped-GF — the first-stop-finding loop that uses pAnchor to locate the shallowest mandatory stop.
- Algo-06-Ceiling-Time-Series — how pAnchor is pre-computed once and reused across a full dive's ceiling time series.
- References — Baker's original GF papers ("Understanding M-values", "Clearing Up The Confusion About 'Deep Stops'").