Skip to content

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 ​

ServicePurpose
CarbonateEquilibriumpH calculation from alkalinity/TIC using Henderson-Hasselbalch
AlkalinityTrackingServiceMass balance of alkalinity consumption and production
pHControllerPID control with gain scheduling for acid/base dosing
StruviteMonitorSaturation 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 ​

ConstantValue at 25CDescription
pKa16.35First dissociation (H2CO3 to HCO3-)
pKa210.33Second dissociation (HCO3- to CO3^2-)
KH_CO23.388 x 10^-2 mol/L/atmHenry's law constant
pCO2_atm4.0 x 10^-4 atmAtmospheric 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)pKa1pKa2
106.5110.50
156.4610.45
206.4010.39
256.3510.33
306.3010.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 RangeBuffer IntensityTypical Wastewater
< 5.5Low (H+ dominated)Unstable
5.5 - 7.5High (carbonate)Well buffered
7.5 - 9.5ModerateStable
> 9.5Low (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 ​

ProcessFactor (g CaCO3/g)Notes
Nitrification-7.14 per g NH4-NConsumes alkalinity
Denitrification+3.57 per g NO3-NProduces alkalinity
Biological P Removal-0.5 per g PMinor consumption
Alum P Removal-5.5 per g PMajor consumption
Ferric P Removal-4.9 per g PMajor consumption
Fermentation+0.07 per g CODVFA production
Lime Softening-2.5 per g CaHardness 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 ​

LevelThreshold (mg/L CaCO3)Action
Critical<= 50Immediate supplementation required
Low<= 100Increase chemical dosing
Normal100 - 400No action needed
High>= 400Reduce 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 RangeGain MultiplierReason
0 - 40.1Strong acid region - slow response
4 - 50.3Weak acid region
5 - 60.6Approaching neutral
6 - 6.51.0Moderate buffer
6.5 - 7.52.0Minimum buffer - maximum sensitivity
7.5 - 81.0Moderate buffer
8 - 90.6Approaching basic
9 - 100.3Weak base region
10 - 140.1Strong 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 RangeAcid DoseBase Dose
-100 to -50-100%0%
-5 to +50% (deadband)0%
+5 to +1000%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 ​

ReagentFormulaConcentrationDensity (kg/L)Alkalinity Factor
Sodium HydroxideNaOH50%1.53+1.25 g CaCO3/g
Calcium HydroxideCa(OH)215% slurry1.20+1.35 g CaCO3/g
Sodium CarbonateNa2CO310%1.10+0.94 g CaCO3/g
Magnesium HydroxideMg(OH)260% slurry1.35+1.72 g CaCO3/g
Sulfuric AcidH2SO493%1.83-1.02 g CaCO3/g
Hydrochloric AcidHCl37%1.19-1.37 g CaCO3/g
Carbon DioxideCO2100%0.002-2.27 g CaCO3/g

Tuning Guidelines ​

ParameterTypical RangeNotes
Kp5 - 20Start at 10, adjust based on response
Ki0.5 - 2Keep low to prevent oscillation
Kd0 - 1Often not needed, use sparingly
Deadband0.05 - 0.2Prevents hunting around setpoint
Response Time30 - 120 sSystem 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 ValueConditionRisk Level
< 0UndersaturatedLow
0 - 0.5Near saturationModerate
0.5 - 1.0SupersaturatedHigh
> 1.0Highly supersaturatedCritical

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 ​

StrategyImplementationEffectiveness
pH ControlMaintain pH < 7.0High
P RemovalChemical precipitation upstreamHigh
Mg ReductionLimit Mg sourcesModerate
DilutionIncrease flowLow
Controlled CrystallizationStruvite reactorBest
Anti-scalantsChemical additionModerate

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 ​

  1. Create Chemistry Service

    typescript
    import { ChemistryControlService } from '@/flow/services/simulation/chemistry';
    
    const chemService = new ChemistryControlService();
  2. Register Controllers

    typescript
    chemService.registerController(pHController);
    chemService.registerAlkalinityTracker(alkalinityService);
    chemService.registerStruviteMonitor(struviteMonitor);
  3. Connect to Simulation Engine

    typescript
    simulationEngine.addService('chemistry', chemService);

Measurement Node Configuration ​

pH measurement nodes should be configured with:

PropertyDescriptionTypical Value
locationIdUnique identifier'reactor-effluent'
sampleIntervalMeasurement frequency1 minute
sensorLagSensor response time30 seconds
noiseMeasurement noise0.05 pH units
biasCalibration offset0

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 ​

  1. Tuning Order: Start with Kp only, add Ki slowly, use Kd sparingly
  2. Gain Schedule: Calibrate based on actual titration curve
  3. Sensor Placement: Install pH sensors in well-mixed zones
  4. Redundancy: Use multiple sensors for critical applications
  5. Calibration: Weekly calibration recommended
  6. Alkalinity Monitoring: Track trends, not just absolute values
  7. Struvite Prevention: Monitor SI at high-risk locations daily

Source Files ​

FileDescription
flow/services/simulation/chemistry/types.tsType definitions and constants
flow/services/simulation/chemistry/carbonateEquilibrium.tsCarbonate system calculations
flow/services/simulation/chemistry/alkalinityTrackingService.tsAlkalinity mass balance
flow/services/simulation/chemistry/pHController.tspH control with gain scheduling
flow/services/simulation/chemistry/struviteMonitor.tsStruvite saturation monitoring

References ​

  1. Metcalf & Eddy (2014). "Wastewater Engineering" Chapter 3: Wastewater Chemistry
  2. Snoeyink, V.L. & Jenkins, D. (1980). "Water Chemistry"
  3. WEF MOP 8 (2009). "Design of Municipal Wastewater Treatment Plants"
  4. Ohlinger, K.N. et al. (1998). "Predicting struvite formation in digestion"
  5. Henze, M. et al. (2008). "Biological Wastewater Treatment: Principles, Modelling and Design"