Model 04 M Values - matej-hron/decojs GitHub Wiki
Model-04 — M-Values
An M-value is the maximum tolerated inert-gas tissue pressure at a given ambient pressure, per compartment. Above M = supersaturation that the algorithm treats as unsafe. Workman (1965) introduced the concept (see References); Bühlmann recast it as a linear function of ambient with two tabulated coefficients (see References).
Bühlmann linear form
$$M(P_{amb}) = a + \frac{P_{amb}}{b}$$
// js/deco/gradients.js:20-22
export function getMValue(ambientPressure, a, b) {
return a + ambientPressure / b;
}
Where $a$ and $b$ are variant-specific per-compartment constants (see Model-01-Compartments). Both depend on the compartment (and $a$ on the variant A/B/C); neither depends on depth.
Graphical interpretation
On a P-P diagram (tissue pressure $P_t$ on $y$, ambient pressure $P_{amb}$ on $x$), the M-value is a straight line:
- $y$-intercept: $a$
- slope: $1/b$
Above the line = M-value violation. The M-value chart in js/charts/MValueChart.js draws 16 of these lines in parallel (one per compartment) and plots the tissue state as 16 markers. The widely-used visual intuition: "how high above the ambient line, and how close to the M-value line, is each tissue right now?"
Ceiling — the shallowest safe ambient pressure
Rearranging $M = a + P_{amb}/b$ for the case where the tissue sits exactly on the GF-adjusted M-value gives the ceiling: the minimum $P_{amb}$ at which the compartment is still within limits. For arbitrary GF:
$$P_{ceiling} = \frac{b \cdot (P_t - GF \cdot a)}{b \cdot (1 - GF) + GF}$$
// js/deco/gradients.js:147-152
export function getCompartmentCeiling(tissuePressure, a, b, gf) {
// P_ceiling = b × (P_tissue - GF × a) / (b × (1 - GF) + GF)
const numerator = b * (tissuePressure - gf * a);
const denominator = b * (1 - gf) + gf;
return numerator / denominator;
}
Derivation
At the ceiling, the tissue sits exactly at the GF-adjusted limit — the linear blend between $P_{amb}$ (at $GF = 0$) and the raw M-value (at $GF = 1$):
$$P_t = P_{amb} + GF \cdot (M(P_{amb}) - P_{amb})$$
Substitute $M(P_{amb}) = a + P_{amb}/b$ and expand:
$$P_t = P_{amb} + GF \cdot a + \frac{GF \cdot P_{amb}}{b} - GF \cdot P_{amb}$$
Collect $P_{amb}$ terms on the right:
$$P_t - GF \cdot a = P_{amb} \cdot \left(1 - GF + \frac{GF}{b}\right) = P_{amb} \cdot \frac{b \cdot (1 - GF) + GF}{b}$$
Solve for $P_{amb}$:
$$P_{amb} = \frac{b \cdot (P_t - GF \cdot a)}{b \cdot (1 - GF) + GF}$$
Sanity check: at $GF = 0$ this collapses to $P_{amb} = P_t$ (no supersaturation tolerated — the tissue must already be at ambient). At $GF = 1$ it reduces to $P_{amb} = b \cdot P_t - a \cdot b$, the raw Bühlmann inverse of $M = a + P_{amb}/b$ solved for ambient.
This is the central equation for deco-stop depths. DecoJS evaluates it for each of the 16 compartments and takes the deepest (highest $P_{amb}$) ceiling — that's the depth the diver cannot go shallower than right now:
// js/deco/gradients.js:163-187 (body)
export function getDiveCeiling(tissuePressures, gf) {
let maxCeiling = -Infinity;
let controllingComp = null;
for (const comp of COMPARTMENTS) {
const tissueP = tissuePressures[comp.id];
const ceiling = getCompartmentCeiling(tissueP, comp.aN2, comp.bN2, gf);
if (ceiling > maxCeiling) {
maxCeiling = ceiling;
controllingComp = comp.id;
}
}
const finalCeiling = Math.max(surfacePressure, maxCeiling);
// ...
}
The compartment winning maxCeiling is the controlling (leading) compartment at that moment.
The clamp and pressure-to-depth conversion use the selected dive-site surface
pressure; SURFACE_PRESSURE remains the sea-level default.
GF-adjusted M-value
At GF = 1.0 (100%), the raw Bühlmann limit stands. At lower GF, only a fraction of the supersaturation budget is spent:
$$M_{adj}(P_{amb}, GF) = P_{amb} + GF \cdot (M(P_{amb}) - P_{amb})$$
// js/deco/gradients.js:35-38
export function getAdjustedMValue(ambientPressure, a, b, gf) {
const mValue = getMValue(ambientPressure, a, b);
return ambientPressure + gf * (mValue - ambientPressure);
}
At GF = 0.5, the allowed tissue pressure sits exactly halfway between ambient and the raw M-value. At GF = 0, $M_{adj} = P_{amb}$ — no supersaturation tolerated at all. The ceiling equation above is the inverse of this, solved for $P_{amb}$.
See Model-05-Gradient-Factors for how $GF$ itself varies during ascent.
Instantaneous GF — "how full is this tissue right now?"
Given a current tissue and ambient state, compute the fraction of the supersaturation budget already used:
$$GF_{inst}(P_t, P_{amb}) = \frac{P_t - P_{amb}}{M(P_{amb}) - P_{amb}}$$
// js/deco/gasKinetics.js:36-46
export function calculateInstantGF(tissuePressure, ambientPressure, compartment) {
const mValue = getMValue(ambientPressure, compartment.aN2, compartment.bN2);
const denominator = mValue - ambientPressure;
if (Math.abs(denominator) < 1e-10) {
return tissuePressure > ambientPressure ? Infinity : -Infinity;
}
return (tissuePressure - ambientPressure) / denominator;
}
- $GF_{inst} = 0$: tissue at ambient (no supersaturation)
- $GF_{inst} = 1$: tissue exactly at the raw M-value
- $GF_{inst} > 1$: violation
- Negative: undersaturated (still on-gassing)
Used by the GFChart (js/charts/GFChart.js) to render real-time saturation fraction per compartment. calculateMaxGF() identifies the supersaturated compartment with the highest instantaneous GF for the chart's Highest GF envelope. This is not necessarily the compartment controlling the decompression ceiling: the controller is the compartment with the deepest tolerated depth under the applied GF, and differing Bühlmann $a$/$b$ coefficients mean the two orderings can diverge.
Reading horizontal intersections
In the sandbox M-value P-P chart, focus or hover a current tissue point and press
T to lock a horizontal ruler at its current tissue pressure. The ruler reports
the ambient pressure and equivalent depth where that tissue intersects:
- the equilibrium line, where tissue and ambient pressure are equal;
- the GF Low limit;
- the interpolated GF-ramp limit;
- the GF High limit.
The GF-ramp intersection answers how shallow the selected compartment permits an
ascent while GF increases from GF Low at pAnchor toward GF High at the
surface. Both the tolerated ambient pressure and the GF at that intersection
are solved together. Press T again to remove the ruler. If no point is
hovered, the shortcut uses the tissue when exactly one compartment is visible.
Worked example
TC1 under variant C: $a = 1.1696$, $b = 0.5578$. At $P_{amb} = 1.0$ bar (sea level):
$$M(1.0) = 1.1696 + \frac{1.0}{0.5578} = 1.1696 + 1.7928 = 2.9624 \text{ bar}$$
A tissue sitting at $P_t = 2.9624$ bar of N₂ at the surface would be exactly at the raw M-value — GF 100/100 would permit this, anything lower would not. That corresponds to a supersaturation gradient of $2.9624 - 1.0 = 1.9624$ bar above ambient, purely in nitrogen. No small amount.
At GF = 0.70: $M_{adj}(1.0) = 1.0 + 0.70 \cdot (2.9624 - 1.0) = 1.0 + 1.3737 = 2.3737$ bar. Same tissue would need to be below 2.3737 bar to surface cleanly under GF 30/70.
Controlling compartment, dynamically
During an ascent the controlling compartment typically shifts:
- Early in a deep ascent: fast compartments (TC1–4) dominate — they took on the most gas and are closest to their M-value.
- Later / shallower: slow compartments (TC10+) take over — they're still loading while fast tissues have already off-gassed.
getDiveCeiling() re-evaluates every step, so the controlling compartment can and does change mid-dive.
Cross-references
- Model-01-Compartments — where $a$ and $b$ come from.
- Model-05-Gradient-Factors — how GF is varied with depth via
pAnchor. - Algo-06-Ceiling-Time-Series — the ceiling sampled at every dive-time point, for chart overlay.
- Sandbox: M-Values — interactive playground for the M-value formula and where its coefficients come from.