Resources Numerical accuracy Calculation policy

Release, fire, explosion and toxic screening

Works out how much escapes through a hole, how far fire radiation, blast overpressure and a toxic or flammable cloud reach, and the chance of harm from a dose.

How the precision is kept

64-bit arithmetic through published screening equations with empirical constants. Distances that have no closed form are found by bisection, the heavy-gas cloud by a fixed-step integration, and the probability by a polynomial approximation of the error function, which limits it to about seven decimal places.

Open item. Replace the five-term error-function approximation with one whose error is at the level of 64-bit rounding, so the probability carries more than seven decimal places.

Calculations under this policy

19 calculations in 3 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

process/safety/consequence.js CadCraft 0.8.2 64-bit 12 calculations Reviewed

Tests tools/process-test/test-safety.mjs, tools/process-test/test-dec.mjs

Liquid release through a hole

64-bit

The mass flow and exit velocity of a liquid leaving through a hole.

v = √(2ΔP ÷ ρ + 2g·h); ṁ = Cd × A × ρ × v

One 64-bit expression; the value under the root is floored at 0. The discharge coefficient defaults to 0.61.

  • process/safety/consequence.js
  • lines 39 to 43
  • CadCraft 0.8.2
/** Liquid through a hole. dP gauge Pa over the outside, h liquid head above the hole (m). → { mdot kg/s, v m/s } */
export function liquidRelease({ A, d, Cd = 0.61, rho, dP = 0, h = 0 }) {
  const a = A ?? (d ? Math.PI * d * d / 4 : 0); if (!(a > 0) || !(rho > 0)) throw new Error('Liquid release needs a hole area (or diameter) and a density.');
  const v = Math.sqrt(Math.max(0, 2 * dP / rho + 2 * G * h)); return { mdot: Cd * a * rho * v, v, A: a };
}

Gas release through a hole

64-bit

The mass flow of a gas leaving through a hole, choked or not.

choked: ṁ = Cd·A·P₀·√(k·M ÷ (Z·R·T) × (2 ÷ (k + 1))^((k+1)/(k−1)))

Choked when P₀ ÷ Pa is at or above ((k + 1) ÷ 2)^(k/(k−1)). The subsonic branch takes a difference of two powers of the pressure ratio, which loses digits as the ratio approaches 1. Returns 0 when P₀ is not above the outside pressure.

  • process/safety/consequence.js
  • lines 44 to 51
  • CadCraft 0.8.2
/** Gas through a hole, P0 and Pa absolute (Pa). Cd defaults to 1 (the conservative value; a person may lower it). */
export function gasRelease({ A, d, Cd = 1, P0, Pa = P_ATM, T, M, k = 1.4, Z = 1 }) {
  const a = A ?? (d ? Math.PI * d * d / 4 : 0); if (!(a > 0) || !(P0 > 0) || !(T > 0) || !(M > 0) || !(k > 1)) throw new Error('Gas release needs area (or diameter), P0, T, M and k > 1.');
  if (P0 <= Pa) return { mdot: 0, choked: false, ratio: Pa / P0 };
  const crit = Math.pow((k + 1) / 2, k / (k - 1)), choked = P0 / Pa >= crit;
  if (choked) return { mdot: Cd * a * P0 * Math.sqrt(k * M / (Z * R * T) * Math.pow(2 / (k + 1), (k + 1) / (k - 1))), choked, ratio: Pa / P0 };
  const r = Pa / P0; return { mdot: Cd * a * P0 * Math.sqrt(2 * M / (Z * R * T) * k / (k - 1) * (Math.pow(r, 2 / k) - Math.pow(r, (k + 1) / k))), choked, ratio: r };
}

Flash fraction

64-bit

The fraction of a pressurized liquid that turns to vapor on release.

F = 1 − exp(−cp·(T − T_b) ÷ h_fg)

For a small superheat the result subtracts a number close to 1 from 1; Math.expm1 is not used. Returns 0 at or below the boiling point.

  • process/safety/consequence.js
  • lines 52 to 53
  • CadCraft 0.8.2
/** Fraction of a pressurised liquid that flashes to vapour on release to atmosphere (0 if T <= Tb). cp J/kg K, hfg J/kg. */
export function flashFraction({ cp, T, Tb, hfg }) { if (!(cp > 0 && hfg > 0)) throw new Error('Flash needs cp and the heat of vaporisation.'); return T <= Tb ? 0 : 1 - Math.exp(-cp * (T - Tb) / hfg); }

Pool evaporation rate

64-bit

The evaporation rate from a liquid pool in wind.

k_g = 0.004786 × U^0.78 × d^(−0.11) × Sc^(−0.67); ṁ = k_g × A × M × P_v ÷ (R·T)

An empirical correlation with a four-digit coefficient. The wind speed is floored at 0.1 m/s and the pool diameter is that of a circle of the same area.

  • process/safety/consequence.js
  • lines 56 to 61
  • CadCraft 0.8.2
/** Evaporation from a pool, kg/s. U wind m/s at about 10 m, Pv Pa, Sc the vapour's Schmidt number in air (the person's). */
export function poolEvaporation({ area, U, Pv, M, T, Sc }) {
  if (!(area > 0 && U >= 0 && Pv >= 0 && M > 0 && T > 0 && Sc > 0)) throw new Error('Evaporation needs area, wind, vapour pressure, M, T and Sc.');
  const d = Math.sqrt(4 * area / Math.PI), kg = 0.004786 * Math.pow(Math.max(U, 0.1), 0.78) * Math.pow(d, -0.11) * Math.pow(Sc, -0.67);
  return { mdot: kg * area * M * Pv / (R * T), kg, d };
}

Radiation distance from a point source

64-bit

The distance at which the heat flux from a fire falls to a given level.

r = √(τ·χ·Q ÷ (4π·q))

One 64-bit expression; products, one division, one square root.

  • process/safety/consequence.js
  • lines 71 to 72
  • CadCraft 0.8.2
/** Radiation distance (m) from the point source to the flux level q (W/m²): r = sqrt(tau chi Q/(4 pi q)). chi (radiative fraction) and tau (transmissivity) are the person's. */
export function radiationDistance({ Q, chi, tau = 1, q }) { if (!(Q > 0 && chi > 0 && q > 0)) throw new Error('Radiation distance needs Q, the radiative fraction chi and the flux level q.'); return Math.sqrt(tau * chi * Q / (4 * Math.PI * q)); }

Pool fire

64-bit

Flame height, heat release and ground distances to each flux level for a burning pool.

H = 42·D × (m″ ÷ (ρ_a·√(g·D)))^0.61; ground = √(r² − (H ÷ 2)²)

The flame height is an empirical correlation. The ground distance is a square root of a difference of squares and is set to 0 when the flux level is not reached at ground level; close to that point it loses digits.

  • process/safety/consequence.js
  • lines 76 to 81
  • CadCraft 0.8.2
/** Pool fire: D pool diameter (m), mPrime burning flux kg/(m² s) (the person's, from the fuel), flame height by Thomas, point source at mid-height; ground distances to each level. */
export function poolFire({ D, mPrime, dHc, chi, tau = 1, levels, rhoAir = 1.2 }) {
  if (!(D > 0 && mPrime > 0)) throw new Error('A pool fire needs a diameter and a burning flux.');
  const H = 42 * D * Math.pow(mPrime / (rhoAir * Math.sqrt(G * D)), 0.61), area = Math.PI * D * D / 4, Q = mPrime * area * dHc;
  return { H, Q, mdot: mPrime * area, distances: levels.map((q) => { const r = radiationDistance({ Q, chi, tau, q }), h = H / 2; return { q, r, ground: r > h ? Math.sqrt(r * r - h * h) : 0 }; }) };
}

TNT-equivalent mass

64-bit

The mass of TNT with the same explosion energy as the fuel.

W = η × m × ΔH_c ÷ E_TNT

One expression. The TNT energy defaults to 4.184e6 J/kg; the yield has no default.

  • process/safety/consequence.js
  • lines 84 to 85
  • CadCraft 0.8.2
/** TNT-equivalent mass (kg). eta is the yield fraction the person justifies; no default is given. */
export function tntEquivalent({ m, dHc, eta, eTNT = 4.184e6 }) { if (!(m > 0 && dHc > 0 && eta > 0 && eta <= 1)) throw new Error('TNT equivalence needs mass, dHc and an explicit yield eta between 0 and 1.'); return eta * m * dHc / eTNT; }

Blast overpressure

64-bit

The side-on overpressure at a distance from a TNT charge.

Z = R ÷ ∛W; P ÷ P_a = 808·(1 + (Z ÷ 4.5)²) ÷ √((1 + (Z ÷ 0.048)²)(1 + (Z ÷ 0.32)²)(1 + (Z ÷ 1.35)²))

One 64-bit expression of sums of squares, so nothing cancels. The constants are the empirical fit's two- and three-digit values.

  • process/safety/consequence.js
  • lines 86 to 87
  • CadCraft 0.8.2
/** Side-on overpressure (Pa) at R metres from W kg TNT (Kinney-Graham). */
export function blastOverpressure(W, Rm, Pa = P_ATM) { if (!(W > 0 && Rm > 0)) return 0; const Z = Rm / Math.cbrt(W); return Pa * 808 * (1 + (Z / 4.5) ** 2) / Math.sqrt((1 + (Z / 0.048) ** 2) * (1 + (Z / 0.32) ** 2) * (1 + (Z / 1.35) ** 2)); }

Distance to an overpressure

64-bit

The distance at which the blast overpressure falls to a given level.

Bisection on a logarithmic scale between 0.01 m and 100 km with 80 halvings; the lower end of the final bracket is returned. Returns 0 when the overpressure at 0.01 m is already below the level.

  • process/safety/consequence.js
  • lines 88 to 89
  • CadCraft 0.8.2
/** Distance (m) at which the overpressure falls to P (Pa); monotone, found by bisection. */
export function blastDistance(W, P, Pa = P_ATM) { if (!(W > 0 && P > 0)) return 0; let lo = 0.01, hi = 1e5; if (blastOverpressure(W, lo, Pa) < P) return 0; for (let i = 0; i < 80; i++) { const m = Math.sqrt(lo * hi); if (blastOverpressure(W, m, Pa) >= P) lo = m; else hi = m; } return lo; }

Probit of a dose

64-bit

The probit value for a concentration and exposure time.

Pr = a + b × ln(Cⁿ × t)

One expression; the constants a, b and n are inputs with no default.

  • process/safety/consequence.js
  • lines 91 to 92
  • CadCraft 0.8.2
// ---- toxic --------------------------------------------------------------------------------------------------------------------------
export const probit = ({ a, b, n, C, t }) => a + b * Math.log(Math.pow(C, n) * t);

Error function

64-bit

The error function used to turn a probit into a probability.

erf(x) ≈ 1 − (a₁t + a₂t² + a₃t³ + a₄t⁴ + a₅t⁵)·e^(−x²), t = 1 ÷ (1 + 0.3275911·|x|)

A five-term polynomial approximation (the coefficients of Abramowitz and Stegun formula 7.1.26), not an exact error function. That formula's published maximum absolute error is 1.5 × 10⁻⁷, so the value is good to about seven decimal places whatever the number type.

  • process/safety/consequence.js
  • line 93
  • CadCraft 0.8.2
const erf = (x) => { const t = 1 / (1 + 0.3275911 * Math.abs(x)), y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x); return x >= 0 ? y : -y; };

Probability from a probit

64-bit

The probability that corresponds to a probit value.

P = ½ × (1 + erf((Pr − 5) ÷ √2))

Carries the accuracy of the error-function approximation, about seven decimal places. For low probits it adds 1 to a value close to −1, so small probabilities have few correct digits.

  • process/safety/consequence.js
  • line 94
  • CadCraft 0.8.2
export const probitToProbability = (pr) => 0.5 * (1 + erf((pr - 5) / Math.SQRT2));
process/safety/blast.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests tools/process-test/test-chem4.mjs

Blast curve interpolation

64-bit

Reads a scaled overpressure or duration from the curve points a person typed.

ln y = ln y₀ + (ln y₁ − ln y₀) × (ln R − ln R₀) ÷ (ln R₁ − ln R₀)

Straight-line interpolation between logarithms of the two bracketing points, then Math.exp. Outside the typed range the first or last value is used unchanged.

  • process/safety/blast.js
  • lines 28 to 32
  • CadCraft 0.8.2
function interpLog(pts, R, col) {
  if (R <= pts[0][0]) return pts[0][col]; if (R >= pts[pts.length - 1][0]) return pts[pts.length - 1][col];
  for (let i = 0; i + 1 < pts.length; i++) if (R >= pts[i][0] && R <= pts[i + 1][0]) return Math.exp(lerp(Math.log(R), Math.log(pts[i][0]), Math.log(pts[i + 1][0]), Math.log(pts[i][col]), Math.log(pts[i + 1][col])));
  return NaN;
}

Sachs scaling length

64-bit

The length that scales distances for an explosion of a given energy.

L = ∛(E ÷ P₀)

One Math.cbrt of one division.

  • process/safety/blast.js
  • line 33
  • CadCraft 0.8.2
export const sachsLength = (E, P0 = 101325) => Math.cbrt(E / P0);

Multi-energy overpressure

64-bit

The overpressure and positive duration at a distance from a blast source of given energy and strength.

R̄ = R ÷ L; ΔP = P̄(R̄) × P₀; duration = t̄(R̄) × L ÷ 340

64-bit scaling around the interpolated curve value. The speed of sound in the duration is the constant 340 m/s.

  • process/safety/blast.js
  • lines 34 to 39
  • CadCraft 0.8.2
export function overpressure(pack, strength, E, R, P0 = 101325) {
  const c = cleanPack(pack); if (!c.ok) { const e = new Error(NEEDS_SOURCE + ' (' + c.problems.join('; ') + ')'); e.needsSource = true; throw e; }
  const cv = c.pack.curves[strength]; if (!cv) throw new Error('the pack has no curve for strength ' + strength);
  const Rb = R / sachsLength(E, P0), Pb = interpLog(cv.points, Rb, 1), td = cv.points[0].length >= 3 ? interpLog(cv.points.filter((q) => q.length >= 3), Rb, 2) : null;
  return { dP: Pb * P0, Rbar: Rb, Pbar: Pb, duration: td != null ? td * sachsLength(E, P0) / 340 : null, reviewed: c.pack.review.state === 'confirmed' };
}

Distance to a multi-energy overpressure

64-bit

The distance at which the overpressure of a blast source falls to a given level.

Bisection on a logarithmic scale between 0.001 and 1000 scaling lengths with 100 halvings. When the level lies outside that range the nearer end of the range is returned without a flag. The curve must fall steadily with distance for the result to be unique.

  • process/safety/blast.js
  • lines 40 to 46
  • CadCraft 0.8.2
/** Distance at which the overpressure falls to dP (Pa); the curve must be monotone decreasing in the range. */
export function distanceFor(pack, strength, E, dP, P0 = 101325) {
  let lo = 1e-3 * sachsLength(E, P0), hi = 1e3 * sachsLength(E, P0);
  if (overpressure(pack, strength, E, lo, P0).dP < dP) return lo; if (overpressure(pack, strength, E, hi, P0).dP > dP) return hi;
  for (let i = 0; i < 100; i++) { const m = Math.sqrt(lo * hi); if (overpressure(pack, strength, E, m, P0).dP > dP) lo = m; else hi = m; }
  return Math.sqrt(lo * hi);
}

Overpressure from several sources

64-bit

The overpressure at a point from each blast source and which one governs.

E = volume × energy density; R = max(distance, 0.01 m)

The distance is Math.hypot of the plan offsets, floored at 0.01 m. Sources are not added together; the largest overpressure is reported.

  • process/safety/blast.js
  • lines 47 to 52
  • CadCraft 0.8.2
/** sources [{ id, x, y, volume (m3 of flammable mixture), energyDensity (J/m3, the person's), strength 1..10 }]; at {x, y}. The method treats separate sources separately: the largest governs. */
export function cloud(pack, sources, at, P0 = 101325) {
  const rows = sources.map((s) => { const E = s.volume * s.energyDensity, R = Math.max(Math.hypot(at.x - s.x, at.y - s.y), 0.01); return { id: s.id, E, R, ...overpressure(pack, s.strength, E, R, P0) }; });
  const g = rows.reduce((a, b) => (b.dP > a.dP ? b : a), rows[0]);
  return { rows, governing: g.id, dP: g.dP, notes: ['sources are treated separately (no interaction); this is a screening estimate, not an engineering approval'] };
}

Blast contour circles

64-bit

Circles on the site plan at given overpressure levels.

The radius is the unrounded bisection result. The label shows the level in kPa to one decimal.

  • process/safety/blast.js
  • lines 53 to 57
  • CadCraft 0.8.2
/** Contour circles (plan shapes in site coordinates) of the governing source at given overpressures (Pa), for CivilCraft's site plan. */
export function contours(pack, source, levels, P0 = 101325) {
  const E = source.volume * source.energyDensity;
  return levels.map((l) => ({ id: `${source.id}:blast:${l}`, type: 'circle', cx: source.x, cy: source.y, r: distanceFor(pack, source.strength, E, l, P0), label: `${(l / 1000).toFixed(1)} kPa`, level: l, unit: 'Pa' }));
}
process/safety/heavygas.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests tools/process-test/test-chem4.mjs

Heavy-gas cloud spreading

64-bit

Radius, height, concentration and drift of a dense gas cloud over time, and when it dilutes to a given concentration.

dR/dt = Cg·√(g′·H); dV/dt = π·R²·a_top·u* + 2π·R·H·a_edge·dR/dt; g′ = g·(ρ − ρ_a) ÷ ρ_a

Classical fourth-order Runge-Kutta with 40 000 fixed steps of tEnd ÷ 40000; the step is not adapted and time is a running sum. Results are stored every 40th step. The reach is the end of the first step at or below the end concentration, not interpolated. The buoyancy term is floored at 0 under the square root.

  • process/safety/heavygas.js
  • lines 16 to 34
  • CadCraft 0.8.2
export function instantaneous(spec) {
  const { mass, rhoGas, rhoAir = 1.2, Cg, aTop, aEdge, ustar = 0, uWind = 0, R0, tEnd, cEnd = null } = spec;
  for (const [k, v] of Object.entries({ mass, rhoGas, Cg, R0, tEnd })) if (!(v > 0)) throw new Error('heavy gas: give ' + k);
  if (!(aTop >= 0 && aEdge >= 0)) throw new Error('heavy gas: give the entrainment constants aTop and aEdge (zero is allowed)');
  if (!(rhoGas > rhoAir)) throw new Error('the gas is not heavier than air at this temperature: use the plume models instead');
  const Vg = mass / rhoGas; let V = Vg / 1, R = R0, t = 0, x = 0; V = Math.max(V, 1e-9); let H = V / (Math.PI * R * R);
  const out = { t: [0], R: [R], H: [H], c: [Vg / V], x: [0], reach: null }, N = 40000; let dt = tEnd / N;
  const state = (R_, V_) => { const H_ = V_ / (Math.PI * R_ * R_), c = Vg / V_, rho = rhoAir + c * (rhoGas - rhoAir), gp = G * (rho - rhoAir) / rhoAir; return { H_, c, gp }; };
  for (let s = 0; s < N && t < tEnd; s++) {
    const f = (R_, V_) => { const { H_, gp } = state(R_, V_), dR = Cg * Math.sqrt(Math.max(gp, 0) * H_), dV = Math.PI * R_ * R_ * aTop * ustar + 2 * Math.PI * R_ * H_ * aEdge * dR; return [dR, dV]; };
    const [a1, b1] = f(R, V), [a2, b2] = f(R + dt / 2 * a1, V + dt / 2 * b1), [a3, b3] = f(R + dt / 2 * a2, V + dt / 2 * b2), [a4, b4] = f(R + dt * a3, V + dt * b3);
    R += dt / 6 * (a1 + 2 * a2 + 2 * a3 + a4); V += dt / 6 * (b1 + 2 * b2 + 2 * b3 + b4); t += dt; x += uWind * dt;
    const st = state(R, V);
    if (s % 40 === 0) { out.t.push(t); out.R.push(R); out.H.push(st.H_); out.c.push(st.c); out.x.push(x); }
    if (cEnd != null && st.c <= cEnd && !out.reach) { out.reach = { time: t, R, x }; break; }
  }
  const st = state(R, V); out.final = { R, H: st.H_, c: st.c, V, gprime: st.gp }; out.massConserved = out.final.c * V * rhoGas; out.notes = ['isothermal, flat ground, no obstacles, no heat transfer from the ground; once g\' is small the cloud is passive and the plume models take over'];
  return out;
}

In detail

Method
Orifice equations, Mackay and Matsugu evaporation, Thomas flame height, point-source radiation, Kinney and Graham blast curve, multi-energy curves from a pack a person supplies, a box model for heavy gas, and probit dose response, as listed in the files' SOURCES.
Number types
64-bit numbers. g = 9.80665 m/s², atmosphere 101325 Pa, air density 1.2 kg/m³ by default.
Tolerances
Blast distance: 80 bisections on a log scale between 0.01 m and 100 km. Multi-energy distance: 100 bisections between 0.001 and 1000 scaled lengths. Heavy gas: 40 000 fixed Runge-Kutta steps. Error function: published maximum error 1.5 × 10⁻⁷.
Limits
The files' REVIEW constants state the equations are not confirmed against the sources. Curves and entrainment constants are inputs. No obstacles, terrain or rainout.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach