Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
test_redox_state.py12.6 kB
""" Validation tests for RedoxState module. Tests against authoritative sources: 1. USGS DO saturation tables 2. Pourbaix Atlas O2/H2O equilibrium line 3. Standard reference electrode potentials 4. Literature Eh-DO correlations """ import pytest import numpy as np from utils.redox_state import ( do_to_eh, eh_to_do, orp_to_eh, eh_to_orp, do_saturation, henry_constant_o2, ReferenceElectrode, create_redox_state_from_do, create_redox_state_from_orp, ) # ============================================================================== # Test DO Saturation vs USGS Tables # ============================================================================== class TestDOSaturation: """Validate DO saturation against USGS tables.""" @pytest.mark.parametrize("temp_C,expected_mg_L,tolerance", [ (0, 14.6, 0.3), # USGS: 14.6 mg/L (5, 12.8, 0.2), # USGS: 12.8 mg/L (10, 11.3, 0.2), # USGS: 11.3 mg/L (15, 10.1, 0.2), # USGS: 10.1 mg/L (20, 9.1, 0.2), # USGS: 9.1 mg/L (25, 8.3, 0.2), # USGS: 8.26 mg/L (30, 7.6, 0.2), # USGS: 7.56 mg/L (35, 7.0, 0.2), # USGS: 6.95 mg/L ]) def test_do_saturation_vs_usgs(self, temp_C, expected_mg_L, tolerance): """Test DO saturation against USGS tables (freshwater, 1 atm).""" calculated = do_saturation(temp_C, pressure_atm=1.0) assert abs(calculated - expected_mg_L) < tolerance, \ f"At {temp_C}°C: calculated {calculated:.2f} mg/L vs USGS {expected_mg_L} mg/L" def test_do_decreases_with_temperature(self): """DO solubility should decrease with increasing temperature.""" temps = [5, 15, 25, 35] saturations = [do_saturation(T) for T in temps] for i in range(len(temps) - 1): assert saturations[i] > saturations[i+1], \ f"DO should decrease with T: {saturations[i]:.1f} > {saturations[i+1]:.1f}" def test_altitude_effect(self): """DO saturation should decrease with altitude (lower pressure).""" # Sea level (1 atm) vs Denver (0.83 atm) DO_sea_level = do_saturation(25.0, pressure_atm=1.0) DO_denver = do_saturation(25.0, pressure_atm=0.83) assert DO_denver < DO_sea_level assert abs(DO_denver / DO_sea_level - 0.83) < 0.02 # Proportional to pressure # ============================================================================== # Test DO → Eh Conversion vs Pourbaix Atlas # ============================================================================== class TestDOtoEh: """Validate DO→Eh conversion against Pourbaix thermodynamics.""" def test_pourbaix_o2_line_pH7(self): """ Test against Pourbaix Atlas O2/H2O line at pH 7. Pourbaix (1974) shows O2/H2O line at p_O2=0.21 atm, pH=7: Eh ≈ +0.82 V vs SHE """ DO_sat = do_saturation(25.0) # ~8.3 mg/L at 1 atm Eh, warnings = do_to_eh(DO_sat, pH=7.0, temperature_C=25.0) # Expected: 1.229 - 0.059*7 + (RT/4F)*ln(0.21) # = 1.229 - 0.413 - 0.010 = 0.806 V assert abs(Eh - 0.806) < 0.02, f"Eh = {Eh:.3f} V, expected ~0.806 V" def test_pourbaix_o2_line_pH0(self): """Test against standard O2/H2O potential at pH 0.""" # At pH 0, p_O2 = 1 atm: E⁰ = +1.229 V DO_at_1atm = do_saturation(25.0, pressure_atm=1.0/0.21) # Pure O2 Eh, warnings = do_to_eh(DO_at_1atm, pH=0.0, temperature_C=25.0) assert abs(Eh - 1.229) < 0.02, f"Eh = {Eh:.3f} V, expected ~1.229 V" def test_eh_decreases_with_ph(self): """Eh should decrease ~59 mV per pH unit (Nernst slope).""" DO = 8.0 # mg/L pHs = [6.0, 7.0, 8.0, 9.0] Ehs = [do_to_eh(DO, pH, 25.0)[0] for pH in pHs] # Check slope ~-0.059 V/pH for i in range(len(pHs) - 1): dEh = Ehs[i+1] - Ehs[i] dpH = pHs[i+1] - pHs[i] slope = dEh / dpH assert abs(slope - (-0.059)) < 0.005, \ f"Nernst slope = {slope:.3f} V/pH, expected -0.059 V/pH" def test_eh_increases_with_do(self): """Eh should increase with DO (more oxidizing).""" pH = 7.0 DOs = [0.5, 2.0, 5.0, 8.0] Ehs = [do_to_eh(DO, pH, 25.0)[0] for DO in DOs] for i in range(len(DOs) - 1): assert Ehs[i] < Ehs[i+1], \ f"Eh should increase with DO: {Ehs[i]:.3f} < {Ehs[i+1]:.3f}" def test_anaerobic_warning(self): """Should warn when DO < 0.01 mg/L (anaerobic).""" Eh, warnings = do_to_eh(0.005, pH=7.0, temperature_C=25.0) assert len(warnings) > 0 assert "anaerobic" in warnings[0].lower() def test_oversaturation_warning(self): """Should warn when DO exceeds saturation by >10%.""" DO_sat = do_saturation(25.0) DO_super = DO_sat * 1.2 # 20% oversaturation Eh, warnings = do_to_eh(DO_super, pH=7.0, temperature_C=25.0) assert len(warnings) > 0 assert "saturation" in warnings[0].lower() or "supersaturation" in warnings[0].lower() # ============================================================================== # Test Eh → DO Conversion (Inverse) # ============================================================================== class TestEhtoDO: """Validate Eh→DO conversion (inverse of do_to_eh).""" def test_roundtrip_conversion(self): """DO → Eh → DO should recover original value.""" DO_original = 8.0 # mg/L pH = 7.5 T = 25.0 Eh, _ = do_to_eh(DO_original, pH, T) DO_recovered, _ = eh_to_do(Eh, pH, T) assert abs(DO_recovered - DO_original) < 0.01, \ f"Roundtrip: {DO_original} → {Eh:.3f} V → {DO_recovered:.2f} mg/L" def test_oxidizing_conditions(self): """High Eh (oxidizing) should give high DO.""" Eh = 0.8 # V (highly oxidizing) pH = 7.0 T = 25.0 DO, warnings = eh_to_do(Eh, pH, T) # Should be near saturation (Eh=0.8V is ~15mV below air-equilibrium ~0.805V, # so expect ~47% sat per Nernst equation, not quite 50%) DO_sat = do_saturation(T) assert DO > 0.45 * DO_sat, \ f"High Eh ({Eh} V) should give significant DO: {DO:.2f} mg/L" def test_reducing_conditions(self): """Low Eh (reducing) should give negligible DO.""" Eh = 0.0 # V (neutral, but below O2/H2O line) pH = 7.0 T = 25.0 DO, warnings = eh_to_do(Eh, pH, T) assert DO < 0.1, \ f"Low Eh ({Eh} V) should give near-zero DO: {DO:.4f} mg/L" assert len(warnings) > 0 # Should warn about anaerobic # ============================================================================== # Test ORP Conversions # ============================================================================== class TestORPConversions: """Validate ORP ↔ Eh conversions.""" def test_sce_reference(self): """Test SCE reference electrode (+0.242 V vs SHE).""" ORP_mV = 150 # mV vs SCE Eh = orp_to_eh(ORP_mV, ReferenceElectrode.SCE) expected_Eh = (150/1000) + 0.242 # = 0.392 V assert abs(Eh - expected_Eh) < 0.001, \ f"SCE: {ORP_mV} mV → {Eh:.3f} V, expected {expected_Eh:.3f} V" def test_agagcl_reference(self): """Test Ag/AgCl reference electrode (+0.197 V vs SHE).""" ORP_mV = 200 # mV vs Ag/AgCl Eh = orp_to_eh(ORP_mV, ReferenceElectrode.AgAgCl) expected_Eh = (200/1000) + 0.197 # = 0.397 V assert abs(Eh - expected_Eh) < 0.001 def test_she_reference(self): """Test SHE reference (no offset).""" ORP_mV = 500 # mV vs SHE Eh = orp_to_eh(ORP_mV, ReferenceElectrode.SHE) assert abs(Eh - 0.500) < 0.001 def test_orp_roundtrip(self): """Eh → ORP → Eh should recover original.""" Eh_original = 0.4 # V ref = ReferenceElectrode.SCE ORP = eh_to_orp(Eh_original, ref) Eh_recovered = orp_to_eh(ORP, ref) assert abs(Eh_recovered - Eh_original) < 0.001 # ============================================================================== # Test Literature Validation # ============================================================================== class TestLiteratureValidation: """Validate against published Eh-pH-DO data.""" def test_revie_aerated_wastewater(self): """ Revie (2011), Table 6.3: Aerated wastewater DO = 6-8 mg/L, pH = 7.5, Eh = +400 to +600 mV (MEASURED) Note: Measured Eh is LOWER than thermodynamic due to kinetics. Our calculation gives thermodynamic Eh. """ DO = 7.0 # mg/L pH = 7.5 T = 25.0 Eh_calc, _ = do_to_eh(DO, pH, T) # Thermodynamic Eh should be HIGHER than measured # Expected: ~750 mV (thermodynamic) vs 400-600 mV (measured) assert Eh_calc > 0.6, \ f"Thermodynamic Eh ({Eh_calc*1000:.0f} mV) should exceed measured range (400-600 mV)" def test_anaerobic_digester_low_do(self): """ Anaerobic digester: DO ~ 0.01 mg/L, pH 7.2, T=35°C Measured Eh: -200 to -400 mV (sulfate reduction, not ORR-controlled) """ DO = 0.01 # mg/L (trace) pH = 7.2 T = 35.0 Eh_calc, warnings = do_to_eh(DO, pH, T) # Should warn about anaerobic conditions assert len(warnings) > 0 assert "anaerobic" in warnings[0].lower() # Calculated Eh will be high (ORR equilibrium with trace O2) # but warning tells user this is NOT valid for anaerobic systems assert "HER" in warnings[0] or "hydrogen evolution" in warnings[0].lower() # ============================================================================== # Test RedoxState Dataclass # ============================================================================== class TestRedoxStateDataclass: """Test RedoxState convenience functions.""" def test_create_from_do(self): """Create RedoxState from DO.""" state = create_redox_state_from_do(8.0, pH=7.5, temperature_C=25.0) assert state.dissolved_oxygen_mg_L == 8.0 assert state.pH == 7.5 assert state.temperature_C == 25.0 assert state.eh_VSHE is not None assert state.eh_VSHE > 0.7 # Oxidizing def test_create_from_orp(self): """Create RedoxState from ORP reading.""" state = create_redox_state_from_orp( 150.0, pH=7.0, temperature_C=25.0, reference_electrode=ReferenceElectrode.SCE ) assert state.orp_mV == 150.0 assert state.reference_electrode == ReferenceElectrode.SCE assert state.eh_VSHE is not None assert abs(state.eh_VSHE - 0.392) < 0.01 # 150 mV + 242 mV = 392 mV assert state.dissolved_oxygen_mg_L is not None # ============================================================================== # Test Temperature Effects # ============================================================================== class TestTemperatureEffects: """Test temperature-dependent calculations.""" def test_henry_constant_temperature_dependence(self): """Henry's constant (K_H = c/p, mol·L⁻¹·atm⁻¹) should DECREASE with T. Gas solubility decreases with temperature, so the solubility constant K_H = c/p decreases. This is distinct from the "inverse Henry constant" (p/c) which would increase with temperature. """ temps = [5, 15, 25, 35] K_Hs = [henry_constant_o2(T) for T in temps] for i in range(len(temps) - 1): assert K_Hs[i] > K_Hs[i+1], \ f"K_H should decrease with T (less soluble): {K_Hs[i]:.2e} > {K_Hs[i+1]:.2e}" def test_nernst_temperature_correction(self): """Nernst equation slope should change with temperature.""" DO = 8.0 pH = 7.0 Eh_25, _ = do_to_eh(DO, pH, 25.0) Eh_35, _ = do_to_eh(DO, pH, 35.0) # Eh changes slightly with T due to RT/F term # At higher T, Nernst slope increases: (RT/F) ∝ T assert abs(Eh_35 - Eh_25) < 0.05, \ f"Eh change with T should be modest: ΔEh = {abs(Eh_35 - Eh_25):.3f} V" # ============================================================================== # Run Tests # ============================================================================== if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])

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