Appearance
pH and Alkalinity Control System
Comprehensive pH control with gain scheduling, carbonate equilibrium, alkalinity tracking, and struvite monitoring (Task 189)
Overview
The pH and Alkalinity Control System provides advanced chemistry control for wastewater treatment, enabling precise pH management while tracking alkalinity consumption and monitoring struvite precipitation risk. The system is critical for:
- Maintaining optimal conditions for biological treatment (pH 6.5-8.5)
- Preventing nitrification inhibition from low alkalinity
- Avoiding struvite scaling in anaerobic digestion and sidestream treatment
- Optimizing chemical dosing for cost-effective operation
Services
| Service | Purpose |
|---|---|
CarbonateEquilibrium | pH calculation from alkalinity/TIC using Henderson-Hasselbalch |
AlkalinityTrackingService | Mass balance of alkalinity consumption and production |
pHController | PID control with gain scheduling for acid/base dosing |
StruviteMonitor | Saturation index calculation and precipitation risk assessment |
Carbonate Equilibrium System
Henderson-Hasselbalch Equation
The carbonate system is the primary buffer in wastewater. The pH is calculated from the equilibrium:
$$\text{H}_2\text{CO}_3^* \rightleftharpoons \text{HCO}3^- + \text{H}^+ \quad (pK)$$
$$\text{HCO}_3^- \rightleftharpoons \text{CO}3^{2-} + \text{H}^+ \quad (pK)$$
Where H2CO3* represents dissolved CO2 plus carbonic acid.
Equilibrium Constants
| Constant | Value at 25C | Description |
|---|---|---|
| pKa1 | 6.35 | First dissociation (H2CO3 to HCO3-) |
| pKa2 | 10.33 | Second dissociation (HCO3- to CO3^2-) |
| KH_CO2 | 3.388 x 10^-2 mol/L/atm | Henry's law constant |
| pCO2_atm | 4.0 x 10^-4 atm | Atmospheric CO2 |
Temperature Corrections
Equilibrium constants are temperature-dependent:
pKa1(T) = pKa1(25) + (-0.0108) x (T - 25)
pKa2(T) = pKa2(25) + (-0.0116) x (T - 25)| Temperature (C) | pKa1 | pKa2 |
|---|---|---|
| 10 | 6.51 | 10.50 |
| 15 | 6.46 | 10.45 |
| 20 | 6.40 | 10.39 |
| 25 | 6.35 | 10.33 |
| 30 | 6.30 | 10.27 |
Alpha Values (Carbonate Speciation)
The distribution of carbonate species is described by alpha values:
$$\alpha_0 = \frac{[\text{H}^+]^2}{[\text{H}^+]^2 + [\text{H}^+]K_1 + K_1K_2}$$
$$\alpha_1 = \frac{[\text{H}^+]K_1}{[\text{H}^+]^2 + [\text{H}^+]K_1 + K_1K_2}$$
$$\alpha_2 = \frac{K_1K_2}{[\text{H}^+]^2 + [\text{H}^+]K_1 + K_1K_2}$$
Where:
- alpha_0 = fraction as H2CO3* (dissolved CO2)
- alpha_1 = fraction as HCO3- (bicarbonate)
- alpha_2 = fraction as CO3^2- (carbonate)
mermaid
xychart-beta
title "Carbonate Speciation vs pH"
x-axis "pH" 4 --> 12
y-axis "Fraction (alpha)" 0 --> 1
line "H2CO3* (alpha_0)" [1.0, 0.98, 0.88, 0.50, 0.10, 0.01, 0.001, 0.0001, 0.0]
line "HCO3- (alpha_1)" [0.0, 0.02, 0.12, 0.50, 0.90, 0.98, 0.88, 0.50, 0.10]
line "CO3^2- (alpha_2)" [0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.12, 0.50, 0.90]Buffer Intensity
Buffer intensity (beta) measures resistance to pH change:
$$\beta = 2.303 \times \left( C_T \times (\alpha_0\alpha_1 + 4\alpha_1\alpha_2) + [\text{H}^+] + [\text{OH}^-] \right)$$
| pH Range | Buffer Intensity | Typical Wastewater |
|---|---|---|
| < 5.5 | Low (H+ dominated) | Unstable |
| 5.5 - 7.5 | High (carbonate) | Well buffered |
| 7.5 - 9.5 | Moderate | Stable |
| > 9.5 | Low (OH- dominated) | Unstable |
Usage
typescript
import {
calculateCarbonateEquilibrium,
calculateAlphaValues,
calculateBufferIntensity,
calculatepKa1,
calculatepKa2,
} from '@/flow/services/simulation/chemistry/carbonateEquilibrium';
// Calculate pH from alkalinity and TIC
const result = calculateCarbonateEquilibrium({
totalAlkalinity: 200, // mg/L as CaCO3
totalInorganicCarbon: 50, // mg/L as C
temperature: 20, // degrees C
ionicStrength: 0.01, // mol/L
atmosphericCO2: 4.0e-4, // atm
closedSystem: false, // open to atmosphere
});
console.log(result.pH); // Calculated pH
console.log(result.speciation); // H2CO3, HCO3, CO3 concentrations
console.log(result.bufferIntensity); // meq/L/pH
console.log(result.LSI); // Langelier Saturation Index
// Get alpha values directly
const [alpha0, alpha1, alpha2] = calculateAlphaValues(7.0, 6.35, 10.33);Alkalinity Tracking
Mass Balance Approach
Alkalinity changes through biological and chemical processes are tracked using stoichiometric factors:
mermaid
flowchart LR
INF[Influent<br>Alkalinity] --> BAL((Mass<br>Balance))
NITR[Nitrification<br>-7.14 g/g N] --> BAL
DENITR[Denitrification<br>+3.57 g/g N] --> BAL
CHEM[Chemical<br>Dosing] --> BAL
PREM[P Removal<br>-0.5 to 5.5 g/g P] --> BAL
FERM[Fermentation<br>+0.07 g/g COD] --> BAL
BAL --> EFF[Effluent<br>Alkalinity]Stoichiometric Factors
| Process | Factor (g CaCO3/g) | Notes |
|---|---|---|
| Nitrification | -7.14 per g NH4-N | Consumes alkalinity |
| Denitrification | +3.57 per g NO3-N | Produces alkalinity |
| Biological P Removal | -0.5 per g P | Minor consumption |
| Alum P Removal | -5.5 per g P | Major consumption |
| Ferric P Removal | -4.9 per g P | Major consumption |
| Fermentation | +0.07 per g COD | VFA production |
| Lime Softening | -2.5 per g Ca | Hardness removal |
Alkalinity Balance Calculation
typescript
import {
AlkalinityTrackingService,
calculateAlkalinityBalance,
} from '@/flow/services/simulation/chemistry/alkalinityTrackingService';
const service = new AlkalinityTrackingService({
influentAlkalinity: 250, // mg/L as CaCO3
nitrificationRate: 50, // kg N/d
denitrificationRate: 30, // kg N/d
chemicalDosing: [
{
reagent: 'NaOH',
doseRate: 10, // L/h
unit: 'L/h',
location: 'secondary',
},
],
biologicalPRemoval: 5, // kg P/d
chemicalPRemoval: 0, // kg P/d
fermentationRate: 100, // kg COD/d
effluentFlow: 10000, // m3/d
}, {
setpoint: 150, // mg/L target
lowAlarm: 100, // mg/L low alarm
highAlarm: 300, // mg/L high alarm
});
// Update with process data
const result = service.update({
nitrificationRate: 55, // Changed nitrification
});
console.log(result.alkalinity); // Effluent alkalinity (mg/L)
console.log(result.balance); // Detailed mass balance
console.log(result.trend); // 'increasing', 'stable', 'decreasing'
console.log(result.alarmState); // 'normal', 'low', 'high', 'critical'
console.log(result.timeToCritical); // Hours until critical (if decreasing)
console.log(result.doseAdjustment); // Recommended dose change (%)Alarm Thresholds
| Level | Threshold (mg/L CaCO3) | Action |
|---|---|---|
| Critical | <= 50 | Immediate supplementation required |
| Low | <= 100 | Increase chemical dosing |
| Normal | 100 - 400 | No action needed |
| High | >= 400 | Reduce chemical dosing |
Required NaOH Dose Calculation
typescript
import { calculateRequiredNaOHDose } from '@/flow/services/simulation/chemistry/alkalinityTrackingService';
const naohDose = calculateRequiredNaOHDose(
50, // nitrificationRate (kg N/d)
30, // denitrificationRate (kg N/d)
150, // targetAlkalinity (mg/L)
200, // influentAlkalinity (mg/L)
10000 // effluentFlow (m3/d)
);
console.log(`Required 50% NaOH: ${naohDose.toFixed(1)} L/h`);pH Control with Gain Scheduling
Why Gain Scheduling?
The titration curve for wastewater is highly nonlinear. Near neutral pH, small reagent additions cause large pH changes, while at extreme pH values, much larger doses are needed. Gain scheduling compensates for this nonlinearity.
mermaid
xychart-beta
title "pH Titration Curve (Typical Wastewater)"
x-axis "Base Added (meq/L)" -10 --> 10
y-axis "pH" 2 --> 12
line "Titration Curve" [2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5]PID Controller with Anti-Windup
The controller uses a PID algorithm with back-calculation anti-windup:
$$u(t) = K_p \cdot e(t) + K_i \int e(t)dt + K_d \frac{de(t)}{dt}$$
Where:
- u(t) = controller output
- e(t) = pH error (setpoint - measurement)
- Kp, Ki, Kd = proportional, integral, derivative gains
Gain Scheduling Table
| pH Range | Gain Multiplier | Reason |
|---|---|---|
| 0 - 4 | 0.1 | Strong acid region - slow response |
| 4 - 5 | 0.3 | Weak acid region |
| 5 - 6 | 0.6 | Approaching neutral |
| 6 - 6.5 | 1.0 | Moderate buffer |
| 6.5 - 7.5 | 2.0 | Minimum buffer - maximum sensitivity |
| 7.5 - 8 | 1.0 | Moderate buffer |
| 8 - 9 | 0.6 | Approaching basic |
| 9 - 10 | 0.3 | Weak base region |
| 10 - 14 | 0.1 | Strong base region - slow response |
Control Cascade Structure
mermaid
flowchart LR
SP[pH Setpoint] --> E1((+/-))
PV[pH Measured] --> E1
E1 --> ERROR[Error]
ERROR --> SCHED[Gain<br>Scheduler]
SCHED --> MULT[Multiplier]
MULT --> PID[PID<br>Controller]
PID --> OUTPUT[Output<br>-100 to +100]
OUTPUT --> SPLIT{Split<br>Range}
SPLIT -->|< 0| ACID[Acid<br>Pump]
SPLIT -->|> 0| BASE[Base<br>Pump]Split-Range Control
Controller output is split between acid and base:
| Output Range | Acid Dose | Base Dose |
|---|---|---|
| -100 to -5 | 0-100% | 0% |
| -5 to +5 | 0% (deadband) | 0% |
| +5 to +100 | 0% | 0-100% |
Configuration
typescript
import {
pHController,
createpHController,
createDefaultGainSchedule,
} from '@/flow/services/simulation/chemistry/pHController';
const controller = createpHController({
id: 'ph-control-1',
name: 'Neutralization pH Control',
enabled: true,
pHsetpoint: 7.0,
pHdeadband: 0.1,
controllerGains: {
Kp: 10,
Ki: 1,
Kd: 0.5,
},
gainScheduling: createDefaultGainSchedule(),
acidReagent: 'H2SO4', // 93% sulfuric acid
baseReagent: 'NaOH', // 50% sodium hydroxide
neutralizationVolume: 100, // m3
mixingEfficiency: 0.85,
maxAcidDose: 100, // L/h
maxBaseDose: 100, // L/h
responseTime: 60, // seconds
measurementNodeId: 'effluent-ph',
actuatorNodeId: 'reagent-pump',
});
// Execute control calculation
const measurement = {
locationId: 'effluent-ph',
pH: 6.5,
temperature: 20,
timestamp: Date.now(),
quality: 'good',
};
const output = controller.calculate(measurement, 0.001, 50); // dt in days, TIC in mg/L
console.log(output.output); // Controller output (-100 to +100)
console.log(output.acidDoseRate); // L/h acid
console.log(output.baseDoseRate); // L/h base
console.log(output.gainMultiplier); // Current gain multiplier
console.log(output.mode); // 'auto', 'manual', 'cascade'Reagent Properties
| Reagent | Formula | Concentration | Density (kg/L) | Alkalinity Factor |
|---|---|---|---|---|
| Sodium Hydroxide | NaOH | 50% | 1.53 | +1.25 g CaCO3/g |
| Calcium Hydroxide | Ca(OH)2 | 15% slurry | 1.20 | +1.35 g CaCO3/g |
| Sodium Carbonate | Na2CO3 | 10% | 1.10 | +0.94 g CaCO3/g |
| Magnesium Hydroxide | Mg(OH)2 | 60% slurry | 1.35 | +1.72 g CaCO3/g |
| Sulfuric Acid | H2SO4 | 93% | 1.83 | -1.02 g CaCO3/g |
| Hydrochloric Acid | HCl | 37% | 1.19 | -1.37 g CaCO3/g |
| Carbon Dioxide | CO2 | 100% | 0.002 | -2.27 g CaCO3/g |
Tuning Guidelines
| Parameter | Typical Range | Notes |
|---|---|---|
| Kp | 5 - 20 | Start at 10, adjust based on response |
| Ki | 0.5 - 2 | Keep low to prevent oscillation |
| Kd | 0 - 1 | Often not needed, use sparingly |
| Deadband | 0.05 - 0.2 | Prevents hunting around setpoint |
| Response Time | 30 - 120 s | System lag compensation |
Struvite Saturation Monitoring
Precipitation Chemistry
Struvite (MgNH4PO4 . 6H2O) precipitates when the solution is supersaturated:
$$\text{Mg}^{2+} + \text{NH}_4^+ + \text{PO}_4^{3-} + 6\text{H}_2\text{O} \rightarrow \text{MgNH}_4\text{PO}_4 \cdot 6\text{H}_2\text{O}$$
Saturation Index Calculation
$$\text{SI} = \log_{10}\left(\frac{\text{IAP}}{K_{sp}}\right)$$
Where:
- IAP = Ion Activity Product = {Mg2+} x {NH4+} x
- Ksp = Solubility product (10^-13.26 at 25C)
| SI Value | Condition | Risk Level |
|---|---|---|
| < 0 | Undersaturated | Low |
| 0 - 0.5 | Near saturation | Moderate |
| 0.5 - 1.0 | Supersaturated | High |
| > 1.0 | Highly supersaturated | Critical |
Risk Zones Diagram
mermaid
quadrantChart
title Struvite Precipitation Risk vs pH and PO4
x-axis "Low PO4" --> "High PO4"
y-axis "Low pH (6)" --> "High pH (9)"
quadrant-1 "HIGH RISK"
quadrant-2 "MODERATE RISK"
quadrant-3 "LOW RISK"
quadrant-4 "MODERATE RISK"Ion Activity Calculations
Activity coefficients are calculated using the Davies equation:
$$\log(\gamma) = -A \times z^2 \times \left(\frac{\sqrt{I}}{1 + \sqrt{I}} - 0.3I\right)$$
Where:
- A approximately equals 0.5 at 25C
- z = ion charge
- I = ionic strength (mol/L)
Struvite Monitor Usage
typescript
import {
StruviteMonitor,
calculateStruviteSaturation,
calculateSaturationIndex,
estimateMinimumpH,
} from '@/flow/services/simulation/chemistry/struviteMonitor';
const monitor = new StruviteMonitor({
magnesium: 25, // mg/L as Mg
ammonia: 100, // mg/L as N
phosphate: 20, // mg/L as P
temperature: 35, // degrees C (typical digester)
pH: 7.5,
ionicStrength: 0.02,
}, {
alarmThreshold: 0.5,
monitoringLocations: ['digester_effluent', 'centrate'],
});
// Update with new measurements
const result = monitor.update({
pH: 7.8, // pH increased
phosphate: 25, // PO4 increased
});
console.log(result.saturationIndex); // SI value
console.log(result.riskLevel); // 'low', 'moderate', 'high', 'critical'
console.log(result.supersaturationRatio); // IAP/Ksp
console.log(result.limitingIon); // 'Mg', 'NH4', or 'PO4'
console.log(result.precipitationPotential); // mg/L as struvite
console.log(result.recommendations); // Array of action items
// Calculate safe pH
const safepH = estimateMinimumpH(25, 100, 20, 35, 0.02);
console.log(`Keep pH below ${safepH.toFixed(1)} to prevent struvite`);
// Calculate target pH for SI = 0
const targetpH = monitor.calculateRequiredpHForTargetSI(0);
console.log(`Target pH: ${targetpH.toFixed(2)}`);Prevention Strategies
| Strategy | Implementation | Effectiveness |
|---|---|---|
| pH Control | Maintain pH < 7.0 | High |
| P Removal | Chemical precipitation upstream | High |
| Mg Reduction | Limit Mg sources | Moderate |
| Dilution | Increase flow | Low |
| Controlled Crystallization | Struvite reactor | Best |
| Anti-scalants | Chemical addition | Moderate |
Struvite Recovery Potential
typescript
import { calculateStruviteRecoveryPotential } from '@/flow/services/simulation/chemistry/struviteMonitor';
const params = {
magnesium: 25,
ammonia: 100,
phosphate: 20,
temperature: 35,
pH: 8.0,
ionicStrength: 0.02,
};
const recoveryKgPerDay = calculateStruviteRecoveryPotential(params, 500); // 500 m3/d
console.log(`Potential struvite recovery: ${recoveryKgPerDay.toFixed(1)} kg/d`);Configuration Examples
Complete pH Control Configuration
json
{
"id": "neutralization-tank-1",
"name": "Primary Neutralization",
"enabled": true,
"pHsetpoint": 7.2,
"pHdeadband": 0.15,
"controllerGains": {
"Kp": 12,
"Ki": 0.8,
"Kd": 0.3
},
"gainScheduling": [
{ "pHmin": 0, "pHmax": 5, "gainMultiplier": 0.2 },
{ "pHmin": 5, "pHmax": 6, "gainMultiplier": 0.5 },
{ "pHmin": 6, "pHmax": 6.5, "gainMultiplier": 1.0 },
{ "pHmin": 6.5, "pHmax": 7.5, "gainMultiplier": 2.0 },
{ "pHmin": 7.5, "pHmax": 8, "gainMultiplier": 1.0 },
{ "pHmin": 8, "pHmax": 9, "gainMultiplier": 0.5 },
{ "pHmin": 9, "pHmax": 14, "gainMultiplier": 0.2 }
],
"acidReagent": "H2SO4",
"baseReagent": "NaOH",
"neutralizationVolume": 150,
"mixingEfficiency": 0.9,
"maxAcidDose": 150,
"maxBaseDose": 200,
"responseTime": 45,
"measurementNodeId": "tank1-ph-sensor",
"actuatorNodeId": "reagent-pumps"
}Alkalinity Tracking Configuration
json
{
"influentAlkalinity": 280,
"nitrificationRate": 75,
"denitrificationRate": 45,
"chemicalDosing": [
{
"reagent": "NaOH",
"doseRate": 15,
"unit": "L/h",
"location": "activated_sludge"
},
{
"reagent": "Na2CO3",
"doseRate": 50,
"unit": "kg/h",
"location": "primary"
}
],
"biologicalPRemoval": 8,
"chemicalPRemoval": 12,
"fermentationRate": 200,
"effluentFlow": 25000,
"setpoint": 180,
"lowAlarm": 100,
"highAlarm": 350
}Struvite Monitor Configuration
json
{
"magnesium": 30,
"ammonia": 150,
"phosphate": 35,
"temperature": 37,
"pH": 7.3,
"ionicStrength": 0.025,
"alarmThreshold": 0.5,
"monitoringLocations": [
"digester_effluent",
"centrate_return",
"thickener_overflow"
]
}Integration with ErgoPlanner
Adding pH Controllers to Simulations
Create Chemistry Service
typescriptimport { ChemistryControlService } from '@/flow/services/simulation/chemistry'; const chemService = new ChemistryControlService();Register Controllers
typescriptchemService.registerController(pHController); chemService.registerAlkalinityTracker(alkalinityService); chemService.registerStruviteMonitor(struviteMonitor);Connect to Simulation Engine
typescriptsimulationEngine.addService('chemistry', chemService);
Measurement Node Configuration
pH measurement nodes should be configured with:
| Property | Description | Typical Value |
|---|---|---|
| locationId | Unique identifier | 'reactor-effluent' |
| sampleInterval | Measurement frequency | 1 minute |
| sensorLag | Sensor response time | 30 seconds |
| noise | Measurement noise | 0.05 pH units |
| bias | Calibration offset | 0 |
Controller Output Integration
Controller outputs are applied to dosing pumps:
typescript
// In simulation step
const chemResult = chemService.execute(context);
for (const output of chemResult.pHControlOutputs) {
const pump = nodes.get(output.targetNodeId);
if (pump) {
pump.setAcidFlow(output.acidDoseRate);
pump.setBaseFlow(output.baseDoseRate);
}
}Best Practices
- Tuning Order: Start with Kp only, add Ki slowly, use Kd sparingly
- Gain Schedule: Calibrate based on actual titration curve
- Sensor Placement: Install pH sensors in well-mixed zones
- Redundancy: Use multiple sensors for critical applications
- Calibration: Weekly calibration recommended
- Alkalinity Monitoring: Track trends, not just absolute values
- Struvite Prevention: Monitor SI at high-risk locations daily
Related Topics
- Cascade-Controller - Multi-loop control strategies
- Aeration-Control - DO control integration
- Overview - Dosing pump configuration
- Nitrogen-Balance - Nitrification alkalinity consumption
Source Files
| File | Description |
|---|---|
flow/services/simulation/chemistry/types.ts | Type definitions and constants |
flow/services/simulation/chemistry/carbonateEquilibrium.ts | Carbonate system calculations |
flow/services/simulation/chemistry/alkalinityTrackingService.ts | Alkalinity mass balance |
flow/services/simulation/chemistry/pHController.ts | pH control with gain scheduling |
flow/services/simulation/chemistry/struviteMonitor.ts | Struvite saturation monitoring |
References
- Metcalf & Eddy (2014). "Wastewater Engineering" Chapter 3: Wastewater Chemistry
- Snoeyink, V.L. & Jenkins, D. (1980). "Water Chemistry"
- WEF MOP 8 (2009). "Design of Municipal Wastewater Treatment Plants"
- Ohlinger, K.N. et al. (1998). "Predicting struvite formation in digestion"
- Henze, M. et al. (2008). "Biological Wastewater Treatment: Principles, Modelling and Design"