Secondary Sedimentation Tank

Introduction

The design of secondary sedimentation tanks (also known as secondary clarifiers or final settling tanks) is critical to the design of a wastewater plant. The tanks retain biomass (mixed liquor) within the aeration tanks and limit secondary effluent solids for subsequent processes. Conventional up-flow tanks use a blanket of solids which settle down to the bottom of the tank while clear water flows up to launders where the effluent is conveyed to the next process.

Sludge Settleability

The measure of sludge settleability is ultimately the Settling Velocity and Sludge Volume Index (SVI). The SVI being the most common measure is the volume (L) which the mass of sludge (mg) occupies after set duration of time, which gives the units of mL/g often denoted as SVI30 for a 30 minute test. As this is the volume in which a gram of sludge occupies, a lower SVI is better at settling than a high SVI.

The standard procedure has a long 1 to 2 L beaker with a mixer at 1 rpm, settling both RAS and MLSS for 30 minutes each. Measurement of the sludge blanket height at 5 and 15 minutes to indicate the initial settling velocity (to convert to m/h) and sludge volume (mL/g).

In general, longer sludge ages have better and more consistent settleability. Younger sludge ages (particularly noted in biological phosphorous removal processes and carbonaceous plants) have much worse and less stable settling sludge. This is as the floccs are looser and the biomass tends to the growth phase rather than respiration and densification.

Stirred Specific Sludge Volume Index

The standard method for establishing a reference settleability is to measure both the mixed liquor and the return activated sludge (RAS) and interpolate to a reference at 3,500 mg/L. This is known as the Specific Sludge Volume Index. This method solves the problems whereby higher concentrations occupy larger volumes, such that the mL/g is higher due to thickness rather than poorer settleability.

For design, it is common to use the 95th percentile SSVI as the worst case settling. This allows the tank to operate throughout all the year, especially as SSVI can deteriorate with the seasons.

The equation is as follows:
$$SSVI_{3.5} = \text{SVI}_{MLSS} – (3,500-X_{MLSS}) \cdot \frac{\text{SVI}_{RAS} – \text{SVI}_{MLSS}}{X_{RAS} – X_{MLSS}}$$
\text{SSVI}_{3.5} = Specific Sludge Volume Index (mL \cdot g^{-1})
X_{MLSS} = Mixed liquor concentration (kg \cdot m^{-3})
X_{RAS} = RAS concentration (kg \cdot m^{-3})

Standardised SSVI Result

Reference Concentration: 3.50 g/L (3500 mg/L)
Interpolated SSVI3.5: 0.0 mL/g

Outdated SVI Method

A old, but common method of managing an SSVI is that the MLSS is diluted to 1 g/L to be settled within the testing apparatus. This allows the direct measurement of volume in the settling tube to determine how many millilitres one gram occupies. This dilution means that the hindered settling is not observed within the settling tube.

Alternate Processes

Not all processes are continuous and require SSVIs by the methods described above. Some processes, namely sequence batch reactors (SBRs) such as Nereda, HYBACS and high rate settling processes do not measure SSVI over 30 or 60 minutes, but require much faster settling tests. High rate settling processes may use and SVI test at 15 minutes.

State Point Diagram

The state-point diagram takes the settling properties of the sludge and applies this to a clarifier. With the feed Q and RAS (as a ratio of Q) flowing into the clarifier, the settleability dictates what flow and mass loading rates will yield successful operation of the tank. The concentration of sludge varies throughout the depth of the tank, being clear at the top and as thick as RAS at the bottom, the settling solids flux at each point is required to be less than that of the settling curve.

Equations and Definitions

Defining the feed to the clarifier by Qin.
$$Q_{in} = Q \cdot (1+R)$$
Q_{in} = Flow into clarifier (m^3 \cdot h^{-1})
Q = Flow into plant, or secondary treatment (m^3 \cdot h^{-1})
R = Ratio of RAS to Q

Vesilind Settling Flux curve uses X as the variable down the clarifier with js as the mass flux. This line must be greater than the applied flux for the solids to settle down.
$$j_s=X \cdot V_0 \cdot e^{-k X}$$
j_s = Vesilind settling flux (kg \cdot m^{-2} \cdot h^{-1})
X = Solids concentration (kg \cdot m^{-3})

The applied flux is the mass flow rate over the settleable area.
$$j_{QF}=X_F \cdot \frac{Q_{in}}{A_{st}}$$
j_{QF} = Mass flux (kg \cdot m^{-2} \cdot h^{-1})
X_F = Feed mixed liquor concentration (kg \cdot m^{-3})
A_{st} = Surface area of SST available for settling (m^2)

To assess the critical points, a series of heuristics developed are used as pass/fail criteria with a safety factor. This safety factor is commonly 20% more settling area than the minimum. The pass fail criteria developed by Wahlberg and Keinath (1988), WRc (1991) and Ekama (1997).

Ekama (1997) shows the hindered settling velocity as:
$$ \begin{aligned} \frac{V_0}{k} &= 69.7 \cdot e^{-0.016 \cdot \text{SSVI}_{3.5}} \\ k &= 0.88 – (0.393 \cdot \log_{10⁡} \frac{V_0}{k}) \end{aligned} $$
V_0 = Terminal settling velocity (m \cdot h^{-1})
k = Hindered settling parameter (m^3 \cdot kg^{-1})

Wahlberg and Keinath (1988)
$$ \begin{aligned} V_0 &= 0.436 – (0.00384 \cdot \text{SSVI}_{3.5}) + (0.00000543 \cdot {\text{SSVI}_{3.5}}^2) \\ k &= 15.3 – 0.0615 \cdot \text{SSVI}_{3.5} \end{aligned} $$

WRc (1991)
$$ \begin{aligned} V_0 &= 9.32 – 0.039 \cdot \text{SSVI}_{3.5} \\ k &= 0.269 + 0.00122 \cdot \text{SSVI}_{3.5} \end{aligned} $$
The critical RAS ratio by the WRc also calculated as:
$$ R_C = \frac{X_F}{\frac{4}{k} – X_F} $$
Where if R > R_C then operation will become unstable and solids overload becomes likely.

Building a State Point Diagram

The state point diagram shows the mass flux settling curve, where the loading must be below the this with an additional safety factor.

  • The Underflow line (green) must be below the settling curve line (blue).
  • The critical/limiting flux is the threshold where the loading is equal to settling, which is the tangent of the settling curve at the second root of the second derivative. G''(X_F) = V_0 \cdot k \cdot e^{-k \cdot X_F} \cdot (kX_F - 2) where X_{C} = \frac{2}{X_F}
  • Where the Underflow line (green) intercepts the x-axis, this is the RAS concentration
  • Where the State Point is, this is the influent MLSS loading
Advanced Settings
Hydraulic Loading (m3 m-2 h-1)
Actual hydraulic loading: 0.00 m/h
Hydraulic loading limit: 1.50 m/h
Assessment: PASS
Loading Rate Limits jL (kg m-2 h-1)
Actual flux loading: 0.00
WRc nomograph limit: 0.00 (Safety Factor: 0.00)
Vesilind curve limit: 0.00 (Safety Factor: 0.00)
Assessment: PASS
Underflow Limit
Actual peak underflow velocity: 0.00 m/h
Critical underflow velocity: 0.00 m/h
Assessment: PASS

Minimum Return Activated Sludge Flow

There a myriad of process implications for the settling design. Namely, the RAS rate is sensitive to both too high and too low. If the RAS is too low, the accumulation of sludge in the bottom of the tank may denitrify. This will cause bubbles of nitrogen gas in the flocs to form resulting in floating sludge. The maximum retention time of sludge in the tank should be 1 hour, and the minimum RAS flow is 40% of Average Daily Flow (ADF).

Notably, the RAS flow should be able to limit the RAS thickness to no more than twice the MLSS concentration.

Concluding Assessment

The design requires a series of pass/fail criteria.

  • The up-flow velocity in the tank cannot exceed half the settling rate of the sludge.
  • The applied solids loading rate cannot exceed the settling flux with an additional safety factor of 20%.
  • The RAS flow must be high enough to not prevent thickening failure.

Authors Note

The author has issued the calculations for use. The calculations should reference the page version (and date) as the page is being continuously updated. I hope that you learn both how to design clarifiers and how to code in HTML. The raw HTML is available below.

Interpolation Code
<div class="flux-calculator-container">
<!-- Primary Inputs Grid -->
<div class="flux-inputs-wrapper">
<div class="input-group">
<label for="mlssConc">MLSS Concentration (mg/L):</label>
<input type="number" id="mlssConc" value="3000" step="100" min="0">
</div>
<div class="input-group">
<label for="sviMlss">SVI of MLSS (mL/g):</label>
<input type="number" id="sviMlss" value="120" step="5" min="0">
</div>
<div class="input-group">
<label for="rasConc">RAS Concentration (mg/L):</label>
<input type="number" id="rasConc" value="8000" step="100" min="0">
</div>
<div class="input-group">
<label for="sviRas">SVI of RAS (mL/g):</label>
<input type="number" id="sviRas" value="80" step="5" min="0">
</div>
</div>
<!-- Chart -->
<div class="flux-chart-wrapper">
<canvas id="ssviChart"></canvas>
</div>
<!-- Interpolation Result -->
<h4>Standardised SSVI Result</h4>
<div class="assessment-data">
<div>Reference Concentration: <strong>3.50 g/L (3500 mg/L)</strong></div>
<div>Interpolated SSVI<sub>3.5</sub>: <strong><span id="outSsvi">0.0</span> mL/g</strong></div>
</div>
</div>
<!-- Load Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.flux-calculator-container { width: 100%; margin: 1.5em 0; font-family: inherit; }
.flux-inputs-wrapper { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.input-group label { display: block; font-weight: 600; font-size: 0.9em; margin-bottom: 5px; }
.input-group input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
.flux-chart-wrapper { width: 100%; min-height: 350px; position: relative; margin-bottom: 20px; }
/* Plain text assessment styling */
.assessment-section { margin-top: 10px; }
.section-heading { margin-top: 15px; margin-bottom: 10px; font-size: 1.1em; border-bottom: 1px solid #eaeaea; padding-bottom: 5px; }
.assessment-data { display: flex; flex-direction: column; gap: 6px; font-size: 0.95em; }
</style>
<script>
(function() {
const inputMlssConc = document.getElementById('mlssConc');
const inputSviMlss = document.getElementById('sviMlss');
const inputRasConc = document.getElementById('rasConc');
const inputSviRas = document.getElementById('sviRas');
const outSsvi = document.getElementById('outSsvi');
const ctx = document.getElementById('ssviChart').getContext('2d');
// Custom plugin to draw a vertical reference line at 3.5 g/L
const verticalLinePlugin = {
id: 'verticalLine',
beforeDraw: chart => {
const xAxis = chart.scales.x;
const yAxis = chart.scales.y;
const xValue = 3.5; // 3.5 g/L reference line
if (xValue >= xAxis.min && xValue <= xAxis.max) {
const ctx = chart.ctx;
const xPixel = xAxis.getPixelForValue(xValue);
ctx.save();
ctx.beginPath();
ctx.moveTo(xPixel, yAxis.top);
ctx.lineTo(xPixel, yAxis.bottom);
ctx.lineWidth = 1.5;
ctx.strokeStyle = 'rgba(107, 114, 128, 0.5)'; // Gray dashed
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.restore();
}
}
};
let ssviChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [
{
label: 'Interpolation Trend',
data: [],
borderColor: '#2563eb',
borderWidth: 2,
borderDash: [4, 4],
fill: false,
pointRadius: 0
},
{
label: 'Measured Test Points (MLSS & RAS)',
data: [],
backgroundColor: '#16a34a',
borderColor: '#16a34a',
pointRadius: 4,
showLine: false
},
{
label: 'Reference SSVI @ 3.5 g/L',
data: [],
backgroundColor: '#e11d48',
borderColor: '#e11d48',
pointRadius: 4,
pointHoverRadius: 10,
showLine: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Solids Concentration (g/L)' },
min: 0
},
y: {
type: 'linear',
title: { display: true, text: 'Sludge Volume Index (mL/g)' },
min: 0
}
},
plugins: {
legend: { position: 'top' },
tooltip: {
callbacks: {
label: function(context) {
return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + ' mL/g @ ' + context.parsed.x.toFixed(2) + ' g/L';
}
}
}
}
},
plugins: [verticalLinePlugin]
});
function updateChart() {
const mlss = parseFloat(inputMlssConc.value);
const sviMlss = parseFloat(inputSviMlss.value);
const ras = parseFloat(inputRasConc.value);
const sviRas = parseFloat(inputSviRas.value);
// Convert to g/L for chart aesthetics
const x1 = mlss / 1000;
const y1 = sviMlss;
const x2 = ras / 1000;
const y2 = sviRas;
const refX = 3.5; // 3.5 g/L (3500 mg/L)
// Prevent divide by zero if inputs are identical
let interpolatedSsvi = 0;
if (x1 === x2) {
interpolatedSsvi = y1;
} else {
// Linear Interpolation Formula: y = y1 + (x - x1) * ((y2 - y1) / (x2 - x1))
const slope = (y2 - y1) / (x2 - x1);
interpolatedSsvi = y1 + (refX - x1) * slope;
}
// Update Text Output
outSsvi.innerText = interpolatedSsvi.toFixed(1);
// Calculate line bounds for drawing the trend line nicely across the chart
const slope = (x2 === x1) ? 0 : (y2 - y1) / (x2 - x1);
const intercept = y1 - (slope * x1);
const minX = 0;
const maxX = Math.max(x1, x2, refX) * 1.2; // Pad X axis
const minY_line = intercept;
const maxY_line = intercept + (slope * maxX);
// Set Datasets
ssviChart.data.datasets[0].data = [
{ x: minX, y: minY_line },
{ x: maxX, y: maxY_line }
];
ssviChart.data.datasets[1].data = [
{ x: x1, y: y1 },
{ x: x2, y: y2 }
];
ssviChart.data.datasets[2].data = [
{ x: refX, y: interpolatedSsvi }
];
// Dynamic Scaling
ssviChart.options.scales.x.max = Math.ceil(maxX);
// Find max Y among the points and the 0/maxX limits to scale cleanly
const allY = [y1, y2, interpolatedSsvi, minY_line, maxY_line];
const highestY = Math.max(...allY);
ssviChart.options.scales.y.max = Math.ceil((highestY > 0 ? highestY : 200) * 1.1 / 10) * 10; // Round to nearest 10
ssviChart.update();
}
[inputMlssConc, inputSviMlss, inputRasConc, inputSviRas].forEach(i => i.addEventListener('input', updateChart));
// Initial Render
updateChart();
})();
</script>
State Point Diagram Code
<div class="flux-calculator-container">
<!-- Primary Inputs Grid -->
<div class="flux-inputs-wrapper">
<div class="input-group">
<label for="flowQ">Effluent Flow, <em>Q</em> (ML/d):</label>
<input type="number" id="flowQ" value="350" step="10" min="0">
</div>
<div class="input-group">
<label for="ratioR">Recycle Ratio, <em>R</em> (Q<sub>R</sub> / Q):</label>
<input type="number" id="ratioR" value="0.9" step="0.05" min="0" max="3">
</div>
<div class="input-group">
<label for="mlss">MLSS Concentration (<em>X<sub>F</sub></em>) (mg/L):</label>
<input type="number" id="mlss" value="3000" step="100" min="0">
</div>
<div class="input-group">
<label for="ssvi">SSVI<sub>3.5</sub> (mL/g):</label>
<input type="number" id="ssvi" value="100" step="5" min="50" max="300">
</div>
<div class="input-group">
<label for="diameter">Tank Diameter (m):</label>
<input type="number" id="diameter" value="30" step="1" min="1">
</div>
<div class="input-group">
<label for="numTanks">Tanks in Service:</label>
<input type="number" id="numTanks" value="20" step="1" min="1">
</div>
</div>
<!-- Chart -->
<div class="flux-chart-wrapper">
<canvas id="statePointChart"></canvas>
</div>
<!-- Advanced Settings -->
<h5>Advanced Settings</h5>
<div class="flux-inputs-wrapper">
<div class="input-group">
<label for="hlrLimit">Hydraulic Loading Limit (m/h):</label>
<input type="number" id="hlrLimit" value="1.5" step="0.1" min="0.1">
</div>
<div class="input-group">
<label for="safetyFactor">Target Safety Factor:</label>
<input type="number" id="safetyFactor" value="1.2" step="0.1" min="1" max="3">
</div>
<div class="input-group">
<label for="settleableArea">Settleable Area (%):</label>
<input type="number" id="settleableArea" value="96" step="1" min="50" max="100">
</div>
</div>
<!-- Detailed Assessment Data -->
<h5>Hydraulic Loading (m<sup>3</sup> m<sup>-2</sup> h<sup>-1</sup>)</h5>
<div class="assessment-data">
<div>Actual hydraulic loading: <span id="txtActualHlr">0.00</span> m/h</div>
<div>Hydraulic loading limit: <span id="txtLimitHlr">1.50</span> m/h</div>
<div id="hlrPassFail">Assessment: PASS</div>
</div>
<h5>Loading Rate Limits <em>j<sub>L</sub></em> (kg m<sup>-2</sup> h<sup>-1</sup>)</h5>
<div class="assessment-data">
<div>Actual flux loading: <span id="txtActualFlux">0.00</span></div>
<div>WRc nomograph limit: <span id="txtJlWrc">0.00</span> (Safety Factor: <strong><span id="txtSfWrc">0.00</span></strong>)</div>
<div>Vesilind curve limit: <span id="txtJlPw">0.00</span> (Safety Factor: <strong><span id="txtSfPw">0.00</span></strong>)</div>
<div id="loadingPassFail">Assessment: PASS</div>
</div>
<h5>Underflow Limit</h5>
<div class="assessment-data">
<div>Actual peak underflow velocity: <span id="txtActualVu">0.00</span> m/h</div>
<div>Critical underflow velocity: <span id="txtCritVu">0.00</span> m/h</div>
<div id="underflowPassFail">Assessment: PASS</div>
</div>
</div>
<!-- Load Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.flux-calculator-container { width: 100%; margin: 1.5em 0; font-family: inherit; }
.flux-inputs-wrapper { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.input-group label { display: block; font-weight: 600; font-size: 0.9em; margin-bottom: 5px; }
.input-group input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
.flux-chart-wrapper { width: 100%; min-height: 450px; position: relative; margin-bottom: 30px; }
/* Plain text assessment styling */
.section-heading { margin-top: 25px; margin-bottom: 10px; font-size: 1.1em; border-bottom: 1px solid #eaeaea; padding-bottom: 5px; }
.pass-text { color: #155724; font-weight: bold; }
.fail-text { color: #721c24; font-weight: bold; }
.marginal-text { color: #d97706; font-weight: bold; }
</style>
<script>
(function() {
function getWrcParams(ssvi) {
let v0 = 9.32 - (0.039 * ssvi);
if (v0 < 0.5) v0 = 0.5;
let k = 0.269 + (0.00122 * ssvi);
return { v0: v0, k: k };
}
function getPwParams(ssvi) {
let ratio = 69.7 * Math.exp(-0.016 * ssvi);
let k = 0.88 - (0.393 * Math.log10(ratio));
let v0 = ratio * k;
if (v0 < 0.5) v0 = 0.5;
return { v0: v0, k: k };
}
// Fully Robust Tangency Algorithm - Guaranteed to return valid plot points
function calculateLimitingFlux(v0, k, vu) {
// Find the absolute steepest point on the Vesilind Curve
let max_neg_slope = v0 / (Math.E * Math.E);
// If actual underflow is steeper than the curve can handle, it is unconstrained
let is_unconstrained = vu >= (max_neg_slope - 0.0001);
// We clamp target_vu just to draw the visual reference line safely on the chart
let target_vu = Math.min(vu, max_neg_slope - 0.0001);
let low = 2 / k;
let high = 60.0;
let x_L = (low + high) / 2;
// Binary search for exact tangency
for (let i = 0; i < 60; i++) {
x_L = (low + high) / 2;
let current_slope_magnitude = v0 * Math.exp(-k * x_L) * (k * x_L - 1);
if (current_slope_magnitude > target_vu) low = x_L;
else high = x_L;
}
let visual_jL = (x_L * v0 * Math.exp(-k * x_L)) + (target_vu * x_L);
// If unconstrained, true jL is theoretically infinite
let jL = is_unconstrained ? Infinity : visual_jL;
return { jL: jL, visual_jL: visual_jL, xL: x_L, effective_vu: target_vu, is_unconstrained: is_unconstrained };
}
function calculateCriticalVu(v0, k, X, vo) {
let gravity_flux_at_feed = v0 * X * Math.exp(-k * X);
if (vo * X > gravity_flux_at_feed) return Infinity; // Clarification failure bypass
let low = 0.0;
let high = 20.0; // Realistic upper limit for underflow
let vu_crit = high;
for (let i = 0; i < 50; i++) {
let mid = (low + high) / 2;
let result = calculateLimitingFlux(v0, k, mid);
if (result.is_unconstrained) {
// Completely clears the curve, safely underloaded
vu_crit = mid;
high = mid;
} else {
let jL = result.jL;
let jQF = X * (vo + mid);
if (jL >= jQF) { vu_crit = mid; high = mid; }
else { low = mid; }
}
}
return vu_crit;
}
const inputQ = document.getElementById('flowQ');
const inputR = document.getElementById('ratioR');
const inputMlss = document.getElementById('mlss');
const inputSsvi = document.getElementById('ssvi');
const inputDiameter = document.getElementById('diameter');
const inputNumTanks = document.getElementById('numTanks');
const inputHlrLimit = document.getElementById('hlrLimit');
const inputSF = document.getElementById('safetyFactor');
const inputSettleableArea = document.getElementById('settleableArea');
const txtActualHlr = document.getElementById('txtActualHlr');
const txtLimitHlr = document.getElementById('txtLimitHlr');
const hlrPassFail = document.getElementById('hlrPassFail');
const txtActualFlux = document.getElementById('txtActualFlux');
const txtJlWrc = document.getElementById('txtJlWrc');
const txtJlPw = document.getElementById('txtJlPw');
const txtSfWrc = document.getElementById('txtSfWrc');
const txtSfPw = document.getElementById('txtSfPw');
const loadingPassFail = document.getElementById('loadingPassFail');
const txtActualVu = document.getElementById('txtActualVu');
const txtCritVu = document.getElementById('txtCritVu');
const underflowPassFail = document.getElementById('underflowPassFail');
const ctx = document.getElementById('statePointChart').getContext('2d');
let statePointChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [
{ label: 'Gravity Flux Curve (WRc)', data: [], borderColor: '#2563eb', borderWidth: 2.5, fill: false, pointRadius: 0 },
{ label: 'Overflow Line', data: [], borderColor: '#0ea5e9', borderWidth: 2, borderDash: [5, 5], fill: false, pointRadius: 0 },
{ label: 'Operating Underflow Line', data: [], borderColor: '#16a34a', borderWidth: 2.5, fill: false, pointRadius: 0 },
{ label: 'Limiting Flux Line', data: [], borderColor: '#ef4444', borderWidth: 2, borderDash: [6, 4], fill: false, pointRadius: 0 },
{ label: 'Influent MLSS', data: [], borderColor: '#6b7280', borderWidth: 1.5, borderDash: [2, 2], fill: false, pointRadius: 0 },
{ label: 'State Point', data: [], backgroundColor: '#e11d48', borderColor: '#e11d48', pointRadius: 6, showLine: false }
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { type: 'linear', title: { display: true, text: 'Solids Concentration X (g/L or kg/m³)' }, min: 0 },
y: { type: 'linear', title: { display: true, text: 'Solids Flux G (kg/m²·h)' }, min: 0 }
},
plugins: { legend: { position: 'top' } }
}
});
function updateChart() {
const Q = parseFloat(inputQ.value) * 1000 / 24;
const R = parseFloat(inputR.value);
const X = parseFloat(inputMlss.value) / 1000;
const ssvi = parseFloat(inputSsvi.value);
const diameter = parseFloat(inputDiameter.value);
const numTanks = parseInt(inputNumTanks.value);
const hlrLimit = parseFloat(inputHlrLimit.value);
const targetSF = parseFloat(inputSF.value);
const settleableArea = parseFloat(inputSettleableArea.value) / 100;
const areaPerTank = Math.PI * Math.pow(diameter / 2, 2);
const totalArea = areaPerTank * numTanks * settleableArea;
if(totalArea <= 0) return;
const v_o = Q / totalArea;
const v_u = (Q * R) / totalArea;
const jQF = X * (v_o + v_u);
const wrc = getWrcParams(ssvi);
const pw = getPwParams(ssvi);
const res_wrc = calculateLimitingFlux(wrc.v0, wrc.k, v_u);
const res_pw = calculateLimitingFlux(pw.v0, pw.k, v_u);
let sf_wrc = res_wrc.jL / jQF;
let sf_pw = res_pw.jL / jQF;
const vu_crit = calculateCriticalVu(wrc.v0, wrc.k, X, v_o);
// --- Text Updates ---
// Use visual_jL if mathematically unconstrained so we always get a valid number
let num_jL_wrc = res_wrc.is_unconstrained ? res_wrc.visual_jL : res_wrc.jL;
let num_jL_pw = res_pw.is_unconstrained ? res_pw.visual_jL : res_pw.jL;
let num_sf_wrc = num_jL_wrc / jQF;
let num_sf_pw = num_jL_pw / jQF;
// HLR Updates
txtActualHlr.innerText = v_o.toFixed(2);
txtLimitHlr.innerText = hlrLimit.toFixed(2);
if (v_o <= hlrLimit) {
hlrPassFail.innerHTML = `Assessment: <span class="pass-text">PASS</span>`;
} else {
hlrPassFail.innerHTML = `Assessment: <span class="fail-text">FAIL (Exceeds Limit)</span>`;
}
// Flux & Loading Rate Limits
txtActualFlux.innerText = jQF.toFixed(2);
// Output clean numbers only
txtJlWrc.innerText = num_jL_wrc.toFixed(2);
txtJlPw.innerText = num_jL_pw.toFixed(2);
txtSfWrc.innerText = num_sf_wrc.toFixed(2);
txtSfPw.innerText = num_sf_pw.toFixed(2);
// Find the lower of the two safety factors
let min_sf = Math.min(num_sf_wrc, num_sf_pw);
// Assessment strictly compares the lowest SF against the Target SF
if (min_sf >= targetSF) {
loadingPassFail.innerHTML = `Assessment: <span class="pass-text">PASS</span>`;
} else {
loadingPassFail.innerHTML = `Assessment: <span class="fail-text">FAIL (Lowest SF is below Target)</span>`;
}
// Underflow Updates
txtActualVu.innerText = v_u.toFixed(2);
if (vu_crit === Infinity) {
txtCritVu.innerText = "N/A (Clarification Fails)";
underflowPassFail.innerHTML = `Assessment: <span class="fail-text">FAIL (Reduce Flow/MLSS)</span>`;
} else {
txtCritVu.innerText = vu_crit.toFixed(2);
if (v_u >= vu_crit) {
underflowPassFail.innerHTML = `Assessment: <span class="pass-text">PASS</span>`;
} else {
underflowPassFail.innerHTML = `Assessment: <span class="fail-text">FAIL (Increase Recycle)</span>`;
}
}
// --- Chart Geometry updates ---
const X_u = v_u > 0 ? jQF / v_u : X * 10;
const maxCalcX = Math.min(Math.max(X_u * 1.5, 12), 40);
const gravityData = [];
for (let x_val = 0; x_val <= maxCalcX; x_val += 0.2) {
gravityData.push({ x: x_val, y: x_val * wrc.v0 * Math.exp(-wrc.k * x_val) });
}
statePointChart.data.datasets[0].data = gravityData;
statePointChart.data.datasets[1].data = [ { x: 0, y: 0 }, { x: maxCalcX, y: v_o * maxCalcX } ];
// Operating Line
statePointChart.data.datasets[2].data = v_u > 0 ? [ { x: 0, y: jQF }, { x: X_u, y: 0 } ] : [ { x: 0, y: jQF }, { x: maxCalcX, y: jQF } ];
// Visual Critical Flux Line
let eff_vu_wrc = res_wrc.effective_vu;
let visual_jL_wrc = res_wrc.visual_jL;
if (eff_vu_wrc > 0) {
statePointChart.data.datasets[3].data = [ { x: 0, y: visual_jL_wrc }, { x: visual_jL_wrc / eff_vu_wrc, y: 0 } ];
} else {
statePointChart.data.datasets[3].data = [ { x: 0, y: visual_jL_wrc }, { x: maxCalcX, y: visual_jL_wrc } ];
}
statePointChart.data.datasets[4].data = [ { x: X, y: 0 }, { x: X, y: jQF } ];
statePointChart.data.datasets[5].data = [{ x: X, y: v_o * X }];
// Safely round and scale axes
let theoretical_Cu = eff_vu_wrc > 0 ? (visual_jL_wrc / eff_vu_wrc) : X_u;
const max_visible_X = Math.ceil(Math.max(10, Math.min(X_u * 1.2, 25), Math.min(theoretical_Cu * 1.2, 25), X * 1.5));
statePointChart.options.scales.x.max = max_visible_X;
statePointChart.options.scales.y.max = Math.ceil(Math.max(10, visual_jL_wrc * 1.2, jQF * 1.2, 3));
statePointChart.update();
}
[inputQ, inputR, inputMlss, inputSsvi, inputDiameter, inputNumTanks, inputHlrLimit, inputSF, inputSettleableArea].forEach(i => i.addEventListener('input', updateChart));
// Ensure chart builds on initial load
updateChart();
})();
</script>
Verson Control and Updates

Version 1.0 updated on 24 June 2026
Issued for use