Works out the concentration downwind of a continuous release, how far a concentration level reaches, and the outline of that level on a site plan.
How the precision is kept
All arithmetic is 64-bit. The concentration itself is a closed form, but the dispersion coefficients are two-digit empirical constants, the reach is found on a fixed grid of distances followed by bisection, and the contour is a polygon through a fixed number of stations. Those choices, not the number type, set the accuracy.
Open item. Refine the reach inside every grid cell where the concentration crosses the endpoint, not only the last one, and state the grid in the result.
Calculations under this policy
7 calculations in 1 source file. 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.
64-bit throughout. With a mixing lid the reflections are a fixed sum of seven image pairs (n = −3 to 3), and the formula switches to a uniformly mixed column when σz > 1.6 × lid height, so the result steps at that switch. It returns 0 for x ≤ 0 and for a release at or above the lid.
process/env/plume.js
lines 47 to 60
CadCraft 0.8.2
/** Concentration (Q units per m³) at (x downwind, y crosswind, z height) from a continuous release Q (mass/s) at effective height H, wind u (m/s) at that height.
* Optional `zi` (m) = mixing height: images reflect the plume between the ground and the lid. */
export function plumeConc({ Q, u, H = 0, stab = 'D', terrain = 'rural', zi = 0 }, x, y = 0, z = 0) {
if (!(Q >= 0) || !(u > 0)) throw new Error('Plume needs Q >= 0 and wind u > 0.');
if (x <= 0) return 0;
const { sy, sz } = sigma(stab, x, terrain), lat = Math.exp(-y * y / (2 * sy * sy));
if (zi > 0) {
if (H >= zi) return 0; // released above the lid: nothing reaches the ground in this model
if (sz > 1.6 * zi) return Q / (Math.sqrt(2 * Math.PI) * u * sy * zi) * lat;
let s = 0; for (let n = -3; n <= 3; n++) s += Math.exp(-((z - H + 2 * n * zi) ** 2) / (2 * sz * sz)) + Math.exp(-((z + H + 2 * n * zi) ** 2) / (2 * sz * sz));
return Q / (2 * Math.PI * u * sy * sz) * lat * s;
}
return Q / (2 * Math.PI * u * sy * sz) * lat * (Math.exp(-((z - H) ** 2) / (2 * sz * sz)) + Math.exp(-((z + H) ** 2) / (2 * sz * sz)));
}
Reach of a concentration level
64-bit
The farthest downwind distance where the ground-level centerline concentration is still at or above an endpoint.
Scans 401 distances at a constant ratio between xMin and xMax (5 m to 50 km by default, about 2.3 % apart), keeps the farthest one at or above the endpoint, then halves the next grid cell 60 times. A stretch above the endpoint that lies wholly between two grid distances is not seen.
process/env/plume.js
lines 61 to 71
CadCraft 0.8.2
/** Farthest downwind distance (m) where the ground-level centreline concentration is still >= Cend; scans a log grid then refines. null when it is never reached. */
export function plumeReach(p, Cend, { xMin = 5, xMax = 50000 } = {}) {
if (!(Cend > 0)) throw new Error('Give the endpoint concentration (kg/m³).');
const at = (x) => plumeConc(p, x, 0, 0); let last = null, n = 400;
for (let i = 0; i <= n; i++) { const x = xMin * Math.pow(xMax / xMin, i / n); if (at(x) >= Cend) last = x; }
if (last == null) return null;
const hi = last * Math.pow(xMax / xMin, 1 / n); let a = last, b = Math.min(hi, xMax);
if (at(b) >= Cend) return b;
for (let i = 0; i < 60; i++) { const m = (a + b) / 2; if (at(m) >= Cend) a = m; else b = m; }
return a;
}
Half-width of a contour
64-bit
Half the crosswind width of a concentration level at a given distance.
w = σy × √(2 × ln(C₀ ÷ C_end))
Closed form from the centerline concentration. The function returns 0 first when the centerline is below the endpoint, so the logarithm is never negative.
The height above ground up to which the concentration stays at or above the endpoint at a given distance.
Bisection on height: 50 halvings of the bracket from 0 to 5σz + H + 10 m. It returns 0 whenever the ground-level value is below the endpoint, also for an elevated release, and returns the bracket top unchanged when the value there is still at or above the endpoint.
process/env/plume.js
line 73
CadCraft 0.8.2
const topHeight = (p, Cend, x) => { let lo = 0, hi = 5 * sigma(p.stab || 'D', x, p.terrain).sz + (p.H || 0) + 10; if (plumeConc(p, x, 0, 0) < Cend) return 0; if (plumeConc(p, x, 0, hi) >= Cend) return hi; for (let i = 0; i < 50; i++) { const m = (lo + hi) / 2; if (plumeConc(p, x, 0, m) >= Cend) lo = m; else hi = m; } return lo; };
Contour polygon in site coordinates
64-bit
The outline of a concentration level as a polygon on the site plan, with its reach and greatest height.
(x, y) → (cx + x·cos t − y·sin t, cy + x·sin t + y·cos t)
Stations are evenly spaced from 1 m to the reach (41 by default) and the outline joins their half-widths with straight segments, so the outline is as fine as the station count. The rotation uses one Math.cos and one Math.sin of the wind angle; points are not rounded.
process/env/plume.js
lines 74 to 81
CadCraft 0.8.2
/** The contour as a footprint polygon in site coordinates: origin at (cx, cy), wind blowing TOWARD angle `toDeg` (mathematical: 0 = +x, 90 = +y). → { reach, polygon: [[x, y]…], height } */
export function plumeFootprint(p, Cend, { cx = 0, cy = 0, toDeg = 0, steps = 40 } = {}) {
const reach = plumeReach(p, Cend); if (reach == null) return { reach: null, polygon: [], height: 0 };
const xs = Array.from({ length: steps + 1 }, (_, i) => 1 + (reach - 1) * i / steps), w = xs.map((x) => halfWidth(p, Cend, x)), t = toDeg * Math.PI / 180, c = Math.cos(t), s = Math.sin(t);
const rot = (x, y) => [cx + x * c - y * s, cy + x * s + y * c];
const polygon = [...xs.map((x, i) => rot(x, w[i])), ...xs.map((x, i) => rot(x, -w[i])).reverse()];
return { reach, polygon, height: Math.max(...xs.map((x) => topHeight(p, Cend, x))) };
}
Gas density relative to air
64-bit
Whether the released gas is heavier than air, which makes a Gaussian result untrustworthy.
ratio = (P·M ÷ R·T) ÷ (P·M_air ÷ R·T_air); dense when ratio > 1.1
Ideal-gas densities at the same pressure, so the pressure and R cancel in the ratio. The molar mass of air is the constant 0.028964 kg/mol and the air temperature defaults to 293.15 K.
process/env/plume.js
lines 82 to 83
CadCraft 0.8.2
/** Is the released gas denser than air at release (so a Gaussian screening result is not trustworthy)? */
export function isDense({ M, T, Pa = P_ATM, Tair = 293.15, ratio = 1.1 }) { const rho = Pa * M / (R * T), rhoAir = Pa * M_AIR / (R * Tair); return { dense: rho / rhoAir > ratio, ratio: rho / rhoAir }; }
In detail
Method
Gaussian plume with ground reflection; Briggs (1973) open-country and urban dispersion coefficients as listed in the file's SOURCES; method of images for a mixing lid.
Number types
64-bit numbers throughout.
Tolerances
Reach: 401 distances at a constant ratio, then 60 bisections. Plume top: 50 bisections. Lid: well-mixed column when σz > 1.6 × lid height.
Limits
The file's REVIEW constant states that the coefficients were typed without the source open and are not confirmed. The contour has 41 stations per side by default. A dense gas is flagged, not modeled.
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.