Works out the steady concentrations in a chain of treatment tanks with recycles and a clarifier, the effluent and waste sludge, the sludge age and the mass balances.
How the precision is kept
64-bit arithmetic. The steady state is reached by an implicit iteration with a finite-difference Jacobian and a dense linear solve, stopped at a relative rate tolerance. COD and nitrogen balances are then computed separately from the final state and reported with their residuals.
Open item. Handle the null that solveLinear returns for a singular matrix; the catch around the solve only handles a thrown error.
Calculations under this policy
8 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.
The 8 by 13 table of how much of each component each process makes or uses.
Each coefficient is a short 64-bit expression of the parameters, rebuilt on every call. The oxygen equivalents 2.86 and 4.57 and the alkalinity divisor 14 are written as decimal constants.
The rate of each of the 8 biological processes at given concentrations.
r₁ = µH × SS ÷ (KS + SS) × SO ÷ (KOH + SO) × XBH (and seven more Monod products)
Negative concentrations are treated as 0 before the Monod terms. The ratio XS ÷ XBH is taken as 0 when XBH is at or below 1e-12 and the last rate as 0 when XS is at or below 1e-12; both thresholds are absolute, in g/m³.
The net production of each of the 13 components from the 8 process rates.
dcₖ/dt = Σⱼ νⱼₖ × ρⱼ
Eight products added in a fixed order per component. Terms of opposite sign cancel near steady state, which is the expected behavior of a balance.
process/env/water.js
lines 62 to 65
CadCraft 0.8.2
/** Reaction rate of every component (object), g/m3/d. */
export function derivs(c, par = PAR) {
const rho = rates(c, par), M = STOICH(par), out = {}; for (const k of COMP) { let s = 0; for (let j = 0; j < 8; j++) s += M[j][k] * rho[j]; out[k] = s; } return out;
}
Clarifier split
64-bit
The effluent and underflow concentrations from the clarifier feed and the capture fraction a person gives.
eff = (1 − capture) × c; und = (Qc × c − Qe × eff) ÷ Qu
A mass balance on each particulate component; soluble components pass unchanged. The underflow is a difference of two flows of mass divided by the underflow, with no rounding.
process/env/water.js
lines 84 to 87
CadCraft 0.8.2
const clarify = (cf) => { // feed concentration vector → { eff, und }
const eff = cf.map((v, i) => (isP[i] ? (1 - plant.capture) * v : v)), und = cf.map((v, i) => (isP[i] && Qu > 0 ? (Qc * v - Qe * eff[i]) / Qu : v));
return { eff, und };
};
Tank mass balances
64-bit
The rate of change of every component in every tank: flow in and out, reaction and aeration.
Transport, reaction and aeration are added as three 64-bit terms per component. Oxygen saturation is the constant 8 g/m³; a tank with a fixed oxygen value has its oxygen rate set to 0.
process/env/water.js
lines 88 to 103
CadCraft 0.8.2
const f = (_t, y) => {
const c = Array.from({ length: n }, (_, i) => y.slice(i * 13, i * 13 + 13)), { und } = clarify(c[n - 1]);
const dy = new Array(y.length).fill(0);
for (let i = 0; i < n; i++) {
const o = obj(c[i]), rho = rates(o, par), t = T[i], q = Qt / t.V;
for (let k = 0; k < 13; k++) {
const key = COMP[k]; let tr;
if (i === 0) tr = (Qin * infl[k] + Qr * und[k] + Qint * c[n - 1][k] - Qt * c[0][k]) / t.V; else tr = q * (c[i - 1][k] - c[i][k]);
let rx = 0; for (let j = 0; j < 8; j++) rx += M[j][key] * rho[j];
let aer = 0; if (key === 'SO' && t.aerated) aer = (t.KLa || 0) * (SO_SAT - c[i][k]);
dy[i * 13 + k] = tr + rx + aer;
if (key === 'SO' && t.SOfixed != null) dy[i * 13 + k] = 0;
}
}
return dy;
};
Steady-state iteration
64-bit
Advances the plant state with implicit steps until nothing changes any more.
(I ÷ h − J) × Δy = F(y); stop when max |Fᵢ| ÷ (|yᵢ| + 1) < tol
The Jacobian is a forward difference with step 1e-6 × max(1, |y|), so the steps are approximate but the stop test is on the true rates: 1e-7 by default, with a cap of 4000 iterations and a warning when the cap is reached. The catch around the linear solve handles a thrown error, while solveLinear in process/numeric.js returns null for a singular matrix. Concentrations other than alkalinity are clamped at 0 after each step.
process/env/water.js
lines 107 to 119
CadCraft 0.8.2
const N = y.length, scale = (v) => Math.abs(v) + 1, resid = (yy) => { const d = f(0, yy); let m = 0; for (let i = 0; i < N; i++) m = Math.max(m, Math.abs(d[i]) / scale(yy[i])); return m; };
const maxIter = 4000, tol = plant.tol ?? 1e-7; let day = 0, conv = false, h = 1e-5, r0 = resid(y), it = 0;
while (it < maxIter) {
const F = f(0, y), Jm = Array.from({ length: N }, () => new Array(N).fill(0));
for (let j = 0; j < N; j++) { const e = 1e-6 * Math.max(1, Math.abs(y[j])), yp = y.slice(); yp[j] += e; const Fp = f(0, yp); for (let i = 0; i < N; i++) Jm[i][j] = (Fp[i] - F[i]) / e; }
const A = Jm.map((row, i) => row.map((v, j) => (i === j ? 1 / h : 0) - v));
let dy; try { dy = solveLinear(A, F); } catch { h /= 4; it++; continue; }
let ch = 0; for (let i = 0; i < N; i++) ch = Math.max(ch, Math.abs(dy[i]) / scale(y[i]));
if (ch > 0.2 && h > 1e-9) { h /= 2; it++; continue; }
y = y.map((v, i) => (COMP[i % 13] === 'SALK' ? v + dy[i] : Math.max(0, v + dy[i]))); day += h; if (ch < 0.05) h = Math.min(h * 2, 1e9);
r0 = resid(y); it++;
if (r0 < tol) { conv = true; break; }
}
COD, nitrogen and oxygen balances, sludge age
64-bit
Checks that what enters the plant equals what leaves plus what reacts, and gives the sludge retention time.
residual = in − out + reaction; rel = residual ÷ (|in| + 10⁻¹²); SRT = Σ V × solids ÷ (Qw × Xw + Qe × Xe)
Computed from the final state and the rate functions, not from the iteration's own numbers. The residual is a difference of large nearly equal totals, which is what it is meant to measure; it is reported unrounded and raises an error above 1e-3 on a converged run.
The oxygen the reactions use next to what the aerators supply, in kg per day.
kg/d = g/d ÷ 1000
Two divisions by 1000; the returned numbers are unrounded. toFixed(1) is used only in the warning text, and the warning fires when supply is below 95 % of use.
process/env/water.js
lines 160 to 165
CadCraft 0.8.2
/** Oxygen: requirement (kg/d) next to what the aerators supply, as Problems when they disagree. */
export function oxygenReport(res) {
const used = res.balances.oxygen.used / 1000, supplied = res.balances.oxygen.supplied / 1000, ps = [];
if (res.converged && supplied < 0.95 * used) ps.push(problem('oxygen', 'warning', `Aeration supplies ${supplied.toFixed(1)} kg O2/d but the reactions use ${used.toFixed(1)} kg/d.`, [{ kind: 'plant', id: 'plant' }]));
return { usedKgPerDay: used, suppliedKgPerDay: supplied, problems: ps };
}
In detail
Method
IAWQ Activated Sludge Model No. 1 (13 components, 8 processes) as listed in the file's SOURCES; pseudo-transient continuation, (I/h − J)·Δy = F.
Number types
64-bit numbers in plain arrays and objects.
Tolerances
Stop when the largest |rate| ÷ (|value| + 1) is below 1e-7 (plant.tol) or after 4000 iterations. Jacobian step 1e-6 × max(1, |y|). Step halved when the largest relative change exceeds 0.2. Balance error flagged above 1e-3 relative. Ratios guarded at 1e-12 g/m³.
Limits
The file's REVIEW constant states the matrix and parameter set are not confirmed against the ASM1 report. No settler model, no temperature correction.
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.