Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
redox_state.py14.8 kB
""" RedoxState: Unified DO, ORP, and Eh conversions for corrosion tools. Provides lightweight conversions between: - Dissolved Oxygen (DO) in mg/L - Redox Potential (Eh) in V vs SHE - Oxidation-Reduction Potential (ORP) in mV vs reference electrode Uses Nernst equation for oxygen reduction reaction (ORR): O₂ + 2H₂O + 4e⁻ → 4OH⁻ References: - Pourbaix (1974). Atlas of Electrochemical Equilibria - Revie (2011). Uhlig's Corrosion Handbook, Chapter 6 - Garcia & Gordon (1992). Oxygen solubility (via utils/oxygen_solubility.py) """ import numpy as np from dataclasses import dataclass from typing import Optional, Literal from enum import Enum # Import authoritative DO saturation implementation from project try: from utils.oxygen_solubility import calculate_do_saturation except ImportError: # When running as __main__, adjust import path import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from utils.oxygen_solubility import calculate_do_saturation # ============================================================================== # Constants # ============================================================================== class Constants: """Physical and electrochemical constants.""" R = 8.314 # J/(mol·K) - Gas constant F = 96485 # C/mol - Faraday constant # Reference electrode potentials vs SHE E_SCE_vs_SHE = 0.242 # V - Saturated Calomel Electrode E_AgAgCl_vs_SHE = 0.197 # V - Ag/AgCl (sat'd KCl) # ORR standard potential E0_ORR_SHE = 1.229 # V vs SHE at pH=0, 25°C # Henry's law constant for O₂ at 25°C # Reference: USGS tables show 8.26 mg/L saturation at 25°C, 1 atm # Working backwards: 8.26 mg/L / (32 g/mol * 1000) = 2.58×10⁻⁴ mol/L # K_H = c/p = 2.58×10⁻⁴ / 0.2095 = 1.23×10⁻³ mol/(L·atm) K_H_O2_298K = 1.23e-3 # mol/(L·atm) at 25°C (calibrated to USGS: 8.26 mg/L) # Enthalpy of solution for O₂ (POSITIVE = endothermic, solubility decreases with T) # From Sander (2015): d(ln(k_H))/d(1/T) = -ΔH_soln/R # ΔH_soln ≈ +1700 K * R = +14.1 kJ/mol (endothermic) d_soln_H_O2 = 14100 # J/mol - enthalpy of solution for O₂ (1700 K * 8.314) # Molecular weight of O₂ MW_O2 = 32.0 # g/mol C = Constants() # ============================================================================== # Reference Electrode Types # ============================================================================== class ReferenceElectrode(str, Enum): """Standard reference electrodes.""" SCE = "SCE" # Saturated Calomel Electrode (+0.242 V vs SHE) AgAgCl = "Ag/AgCl" # Silver/Silver Chloride (+0.197 V vs SHE) SHE = "SHE" # Standard Hydrogen Electrode (0.000 V) # ============================================================================== # RedoxState Dataclass # ============================================================================== @dataclass class RedoxState: """ Unified redox state representation. Attributes: eh_VSHE: Redox potential vs SHE (V) pH: Solution pH temperature_C: Temperature (°C) dissolved_oxygen_mg_L: Dissolved oxygen concentration (mg/L) orp_mV: ORP reading vs reference electrode (mV) reference_electrode: Reference electrode type """ eh_VSHE: Optional[float] = None pH: float = 7.0 temperature_C: float = 25.0 dissolved_oxygen_mg_L: Optional[float] = None orp_mV: Optional[float] = None reference_electrode: ReferenceElectrode = ReferenceElectrode.SCE # ============================================================================== # Henry's Law for O₂ Solubility # ============================================================================== def henry_constant_o2(temperature_C: float) -> float: """ Calculate temperature-dependent Henry's law constant for O₂. Derived from Garcia & Gordon (1992) DO saturation model via utils/oxygen_solubility.py (authoritative implementation). Args: temperature_C: Temperature (°C) Returns: Henry's constant (mol/(L·atm)) Reference: Derived from Garcia & Gordon (1992) """ # Get DO saturation from authoritative implementation DO_sat_mg_L = calculate_do_saturation( temperature_C, salinity_psu=0.0, model="garcia-benson" ) # Convert to Henry's constant # DO_sat = K_H * p_O2 * MW_O2 * 1000 # K_H = DO_sat / (p_O2 * MW_O2 * 1000) p_O2 = 0.2095 # atm (air) K_H = DO_sat_mg_L / (p_O2 * C.MW_O2 * 1000) # mol/(L·atm) return K_H def do_saturation(temperature_C: float, pressure_atm: float = 1.0) -> float: """ Calculate DO saturation concentration at air equilibrium. Uses Garcia & Gordon (1992) model via utils/oxygen_solubility.py (authoritative implementation with full provenance). Args: temperature_C: Temperature (°C) pressure_atm: Atmospheric pressure (atm) Returns: DO saturation (mg/L) Note: Assumes air with 20.95% O₂ by volume """ # Use authoritative implementation # Garcia & Gordon is normalized to 1 atm, scale with pressure DO_sat = calculate_do_saturation( temperature_C, salinity_psu=0.0, pressure_mbar=pressure_atm * 1013.25, # Convert atm to mbar model="garcia-benson" ) return DO_sat # ============================================================================== # DO ↔ Eh Conversions # ============================================================================== def do_to_eh( dissolved_oxygen_mg_L: float, pH: float, temperature_C: float = 25.0, ) -> tuple[float, list[str]]: """ Convert dissolved oxygen to redox potential using ORR Nernst equation. Reaction: O₂ + 2H₂O + 4e⁻ → 4OH⁻ Nernst equation: Eh = E⁰ + (RT/4F) * ln(a_O2) - (RT/F) * ln([OH⁻]²) Eh = E⁰ - 0.059*pH + (0.059/4) * log10(p_O2) [at 25°C] Args: dissolved_oxygen_mg_L: Dissolved oxygen (mg/L) pH: Solution pH temperature_C: Temperature (°C) Returns: Tuple of (Eh_VSHE, warnings) - Eh_VSHE: Redox potential vs SHE (V) - warnings: List of warning messages Example: >>> Eh, warnings = do_to_eh(8.0, pH=8.1, temperature_C=25.0) >>> print(f"Eh = {Eh:.3f} V_SHE") Eh = 0.397 V_SHE """ warnings = [] # Convert DO (mg/L) to partial pressure (atm) using Henry's law K_H = henry_constant_o2(temperature_C) c_O2_mol_L = dissolved_oxygen_mg_L / (C.MW_O2 * 1000) # mg/L → mol/L p_O2 = c_O2_mol_L / K_H # atm # Check if DO is at or below detection limit (anaerobic conditions) if dissolved_oxygen_mg_L <= 0.01: warnings.append( "DO <= 0.01 mg/L (anaerobic conditions). Eh calculation assumes ORR " "equilibrium, which may not apply in anaerobic environments where " "hydrogen evolution reaction (HER) or sulfate reduction may dominate." ) # Use epsilon to prevent log(0) p_O2 = max(p_O2, 1e-10) # Check if oversaturated DO_sat = do_saturation(temperature_C) if dissolved_oxygen_mg_L > 1.1 * DO_sat: warnings.append( f"DO ({dissolved_oxygen_mg_L:.1f} mg/L) exceeds saturation " f"({DO_sat:.1f} mg/L) by >10%. This may indicate supersaturation " f"or measurement error." ) # Nernst equation for ORR T_K = temperature_C + 273.15 RT_4F = (C.R * T_K) / (4.0 * C.F) # V # Eh = E⁰ - (RT/F)*ln(10)*pH + (RT/4F)*ln(p_O2) # Using natural log Eh_VSHE = C.E0_ORR_SHE - (2.303 * C.R * T_K / C.F) * pH + RT_4F * np.log(p_O2) return Eh_VSHE, warnings def eh_to_do( eh_VSHE: float, pH: float, temperature_C: float = 25.0, ) -> tuple[float, list[str]]: """ Convert redox potential to dissolved oxygen (inverse of do_to_eh). Solves Nernst equation for p_O2, then converts to DO using Henry's law. Args: eh_VSHE: Redox potential vs SHE (V) pH: Solution pH temperature_C: Temperature (°C) Returns: Tuple of (DO_mg_L, warnings) - DO_mg_L: Dissolved oxygen (mg/L) - warnings: List of warning messages Example: >>> DO, warnings = eh_to_do(0.4, pH=8.1, temperature_C=25.0) >>> print(f"DO = {DO:.1f} mg/L") DO = 8.1 mg/L """ warnings = [] # Solve Nernst equation for p_O2 T_K = temperature_C + 273.15 RT_4F = (C.R * T_K) / (4.0 * C.F) # Eh = E⁰ - (RT/F)*ln(10)*pH + (RT/4F)*ln(p_O2) # ln(p_O2) = (Eh - E⁰ + (RT/F)*ln(10)*pH) / (RT/4F) pH_term = (2.303 * C.R * T_K / C.F) * pH ln_p_O2 = (eh_VSHE - C.E0_ORR_SHE + pH_term) / RT_4F p_O2 = np.exp(ln_p_O2) # Check if p_O2 is physically realistic if p_O2 > 1.0: warnings.append( f"Calculated p_O2 = {p_O2:.2f} atm exceeds atmospheric pressure. " f"This Eh ({eh_VSHE:.3f} V) is too oxidizing for ORR equilibrium." ) p_O2 = min(p_O2, 1.0) # Cap at 1 atm if p_O2 < 1e-10: warnings.append( f"Calculated p_O2 = {p_O2:.2e} atm is negligible. " f"This Eh ({eh_VSHE:.3f} V) indicates anaerobic/reducing conditions." ) # Convert p_O2 to DO using Henry's law K_H = henry_constant_o2(temperature_C) c_O2_mol_L = K_H * p_O2 DO_mg_L = c_O2_mol_L * C.MW_O2 * 1000 return DO_mg_L, warnings # ============================================================================== # ORP ↔ Eh Conversions # ============================================================================== def orp_to_eh( orp_mV: float, reference_electrode: ReferenceElectrode = ReferenceElectrode.SCE, ) -> float: """ Convert ORP reading to Eh (vs SHE). Simple reference electrode offset correction. Args: orp_mV: ORP reading (mV vs reference electrode) reference_electrode: Reference electrode type Returns: Eh_VSHE: Redox potential vs SHE (V) Example: >>> Eh = orp_to_eh(150, reference_electrode=ReferenceElectrode.SCE) >>> print(f"Eh = {Eh:.3f} V_SHE") Eh = 0.392 V_SHE """ orp_V = orp_mV / 1000.0 # mV → V if reference_electrode == ReferenceElectrode.SCE: Eh_VSHE = orp_V + C.E_SCE_vs_SHE elif reference_electrode == ReferenceElectrode.AgAgCl: Eh_VSHE = orp_V + C.E_AgAgCl_vs_SHE elif reference_electrode == ReferenceElectrode.SHE: Eh_VSHE = orp_V # Already vs SHE else: raise ValueError(f"Unknown reference electrode: {reference_electrode}") return Eh_VSHE def eh_to_orp( eh_VSHE: float, reference_electrode: ReferenceElectrode = ReferenceElectrode.SCE, ) -> float: """ Convert Eh (vs SHE) to ORP reading. Args: eh_VSHE: Redox potential vs SHE (V) reference_electrode: Reference electrode type Returns: ORP reading (mV vs reference electrode) Example: >>> orp = eh_to_orp(0.4, reference_electrode=ReferenceElectrode.SCE) >>> print(f"ORP = {orp:.0f} mV_SCE") ORP = 158 mV_SCE """ if reference_electrode == ReferenceElectrode.SCE: orp_V = eh_VSHE - C.E_SCE_vs_SHE elif reference_electrode == ReferenceElectrode.AgAgCl: orp_V = eh_VSHE - C.E_AgAgCl_vs_SHE elif reference_electrode == ReferenceElectrode.SHE: orp_V = eh_VSHE # Already vs SHE else: raise ValueError(f"Unknown reference electrode: {reference_electrode}") return orp_V * 1000.0 # V → mV # ============================================================================== # Convenience Functions # ============================================================================== def create_redox_state_from_do( dissolved_oxygen_mg_L: float, pH: float = 7.0, temperature_C: float = 25.0, ) -> RedoxState: """ Create RedoxState from dissolved oxygen. Args: dissolved_oxygen_mg_L: Dissolved oxygen (mg/L) pH: Solution pH temperature_C: Temperature (°C) Returns: RedoxState object with calculated Eh """ Eh_VSHE, _ = do_to_eh(dissolved_oxygen_mg_L, pH, temperature_C) return RedoxState( eh_VSHE=Eh_VSHE, pH=pH, temperature_C=temperature_C, dissolved_oxygen_mg_L=dissolved_oxygen_mg_L, ) def create_redox_state_from_orp( orp_mV: float, pH: float = 7.0, temperature_C: float = 25.0, reference_electrode: ReferenceElectrode = ReferenceElectrode.SCE, ) -> RedoxState: """ Create RedoxState from ORP reading. Args: orp_mV: ORP reading (mV vs reference electrode) pH: Solution pH temperature_C: Temperature (°C) reference_electrode: Reference electrode type Returns: RedoxState object with calculated Eh and DO """ Eh_VSHE = orp_to_eh(orp_mV, reference_electrode) DO_mg_L, _ = eh_to_do(Eh_VSHE, pH, temperature_C) return RedoxState( eh_VSHE=Eh_VSHE, pH=pH, temperature_C=temperature_C, dissolved_oxygen_mg_L=DO_mg_L, orp_mV=orp_mV, reference_electrode=reference_electrode, ) # ============================================================================== # Example Usage # ============================================================================== if __name__ == "__main__": # Example 1: Aerated seawater print("Example 1: Aerated Seawater") print("-" * 50) DO = 8.0 # mg/L pH = 8.1 T = 25.0 # °C Eh, warnings = do_to_eh(DO, pH, T) print(f"DO = {DO} mg/L, pH = {pH}, T = {T}°C") print(f"Eh = {Eh:.3f} V_SHE = {Eh*1000:.0f} mV_SHE") if warnings: print(f"Warnings: {warnings}") print() # Example 2: Anaerobic digester print("Example 2: Anaerobic Digester") print("-" * 50) DO = 0.01 # mg/L pH = 7.2 T = 35.0 # °C Eh, warnings = do_to_eh(DO, pH, T) print(f"DO = {DO} mg/L, pH = {pH}, T = {T}°C") print(f"Eh = {Eh:.3f} V_SHE = {Eh*1000:.0f} mV_SHE") if warnings: print(f"Warnings: {warnings[0][:80]}...") print() # Example 3: ORP conversion print("Example 3: ORP Reading") print("-" * 50) ORP_mV = 150 # mV vs SCE Eh = orp_to_eh(ORP_mV, ReferenceElectrode.SCE) print(f"ORP = {ORP_mV} mV_SCE") print(f"Eh = {Eh:.3f} V_SHE = {Eh*1000:.0f} mV_SHE") print() # Example 4: DO saturation print("Example 4: DO Saturation") print("-" * 50) for T in [5, 15, 25, 35]: DO_sat = do_saturation(T) print(f"T = {T}°C: DO_sat = {DO_sat:.1f} mg/L")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/puran-water/corrosion-engineering-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server