Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
test_chemistry_tools.py15.6 kB
""" Unit tests for Tier 1 Chemistry MCP Tools Tests the three chemistry tools: - run_phreeqc_speciation - predict_scaling_tendency - calculate_langelier_index Target coverage: ≥85% """ import pytest import json from pathlib import Path import sys # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from tools.chemistry.run_speciation import run_phreeqc_speciation from tools.chemistry.predict_scaling import predict_scaling_tendency from tools.chemistry.langelier_index import calculate_langelier_index class TestRunPhreeqcSpeciation: """Test run_phreeqc_speciation MCP tool""" def test_basic_speciation(self): """Test basic speciation with simple NaCl solution""" ions_json = json.dumps({ "Na+": 1000.0, "Cl-": 1545.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0) # Check all required fields assert "pH" in result assert "pe" in result assert "temperature_C" in result assert "ionic_strength_M" in result assert "alkalinity_mg_L_CaCO3" in result assert "species" in result assert "saturation_indices" in result assert "charge_balance_percent" in result assert "interpretation" in result # Check value ranges assert 5.0 <= result["pH"] <= 9.0 assert result["temperature_C"] == 25.0 assert result["ionic_strength_M"] > 0.0 def test_speciation_with_pH(self): """Test speciation with specified pH""" ions_json = json.dumps({ "Na+": 1000.0, "Cl-": 1545.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0, pH=7.5) # pH should be close to specified value assert abs(result["pH"] - 7.5) < 0.2 def test_speciation_seawater(self): """Test speciation for seawater composition""" ions_json = json.dumps({ "Na+": 10770.0, "Mg2+": 1290.0, "Ca2+": 412.0, "K+": 399.0, "Cl-": 19350.0, "SO4-2": 2712.0, "HCO3-": 142.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0) # Seawater pH can vary (~7.0-8.3 without atmosphere equilibration) assert 6.5 <= result["pH"] <= 8.5 # High ionic strength assert result["ionic_strength_M"] > 0.5 # Should have interpretation assert len(result["interpretation"]) > 0 assert "saline" in result["interpretation"].lower() def test_invalid_json(self): """Test error handling for invalid JSON""" with pytest.raises(ValueError, match="Invalid JSON"): run_phreeqc_speciation("not valid json", temperature_C=25.0) def test_non_dict_json(self): """Test error handling for non-dict JSON""" with pytest.raises(ValueError, match="must be a JSON object"): run_phreeqc_speciation("[1, 2, 3]", temperature_C=25.0) def test_charge_balance_validation(self): """Test charge balance validation catches imbalanced water""" ions_json = json.dumps({ "Na+": 10000.0, # Huge excess cations "Cl-": 100.0, }) # Should still run but log warning result = run_phreeqc_speciation( ions_json, temperature_C=25.0, validate_charge_balance=True, max_imbalance=5.0 ) # Check charge balance is reported assert abs(result["charge_balance_percent"]) > 10.0 def test_interpretation_acidic(self): """Test interpretation for acidic water""" ions_json = json.dumps({ "Na+": 100.0, "Cl-": 154.5, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0, pH=4.0) # Should indicate corrosive assert "acidic" in result["interpretation"].lower() or "corrosive" in result["interpretation"].lower() def test_interpretation_scaling(self): """Test interpretation for scaling water""" ions_json = json.dumps({ "Ca2+": 200.0, "Mg2+": 80.0, "HCO3-": 400.0, "Cl-": 100.0, "Na+": 50.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0, pH=8.5) # Should indicate scaling risk assert "scaling" in result["interpretation"].lower() or "alkaline" in result["interpretation"].lower() class TestPredictScalingTendency: """Test predict_scaling_tendency MCP tool""" def test_basic_scaling_prediction(self): """Test basic scaling prediction""" ions_json = json.dumps({ "Ca2+": 120.0, "Mg2+": 30.0, "HCO3-": 250.0, "Cl-": 150.0, "SO4-2": 80.0, "Na+": 100.0, }) result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=7.8) # Check all required fields assert "lsi" in result assert "rsi" in result assert "puckorius_index" in result assert "larson_ratio" in result assert "pH" in result assert "pH_saturation" in result assert "ionic_strength_M" in result assert "interpretation" in result assert "recommendations" in result # Check value ranges assert -3.0 <= result["lsi"] <= 3.0 assert 4.0 <= result["rsi"] <= 10.0 assert result["larson_ratio"] >= 0.0 def test_scaling_water(self): """Test prediction for scaling water (high LSI)""" ions_json = json.dumps({ "Ca2+": 200.0, "Mg2+": 80.0, "HCO3-": 400.0, "Cl-": 100.0, "Na+": 50.0, }) result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=8.5) # Should have positive LSI assert result["lsi"] > 0.0 # Should have recommendations assert len(result["recommendations"]) > 0 # Should mention scaling in interpretation assert "scaling" in result["interpretation"].lower() def test_corrosive_water(self): """Test prediction for corrosive water (negative LSI)""" ions_json = json.dumps({ "Ca2+": 20.0, "HCO3-": 30.0, "Cl-": 200.0, "SO4-2": 150.0, "Na+": 100.0, }) result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=6.5) # Should have negative LSI assert result["lsi"] < 0.0 # Should have high RSI assert result["rsi"] > 7.0 # Should have recommendations assert any("corrosion" in rec.lower() for rec in result["recommendations"]) def test_high_larson_ratio(self): """Test prediction for water with high Larson ratio""" ions_json = json.dumps({ "Cl-": 500.0, # High chloride "SO4-2": 300.0, # High sulfate "HCO3-": 50.0, # Low bicarbonate "Na+": 400.0, "Ca2+": 50.0, }) result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=7.0) # Larson ratio should be high assert result["larson_ratio"] > 1.0 # Should have corrosivity warning assert any("larson" in rec.lower() for rec in result["recommendations"]) def test_balanced_water(self): """Test prediction for near-equilibrium water""" ions_json = json.dumps({ "Ca2+": 80.0, "Mg2+": 20.0, "HCO3-": 180.0, "Cl-": 100.0, "SO4-2": 60.0, "Na+": 80.0, }) result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=7.5) # LSI should be near zero assert -0.5 <= result["lsi"] <= 0.5 # Interpretation should mention equilibrium assert "equilibrium" in result["interpretation"].lower() class TestCalculateLangelierIndex: """Test calculate_langelier_index MCP tool""" def test_basic_lsi_calculation(self): """Test basic LSI calculation""" ions_json = json.dumps({ "Ca2+": 120.0, "HCO3-": 250.0, "Cl-": 150.0, "Na+": 100.0, }) result = calculate_langelier_index(ions_json, temperature_C=25.0, pH=7.8) # Check all required fields assert "lsi" in result assert "pH" in result assert "pH_saturation" in result assert "si_calcite" in result assert "temperature_C" in result assert "interpretation" in result assert "action_required" in result # Check value ranges assert -3.0 <= result["lsi"] <= 3.0 assert result["temperature_C"] == 25.0 def test_lsi_positive_scaling(self): """Test LSI for scaling water""" ions_json = json.dumps({ "Ca2+": 200.0, "HCO3-": 400.0, "Cl-": 100.0, "Na+": 50.0, }) result = calculate_langelier_index(ions_json, temperature_C=25.0, pH=8.5) # Should have positive LSI assert result["lsi"] > 0.0 # Interpretation should mention scaling assert "scaling" in result["interpretation"].lower() # Action should mention treatment assert "action" in result["action_required"].lower() or "monitor" in result["action_required"].lower() def test_lsi_negative_corrosive(self): """Test LSI for corrosive water""" ions_json = json.dumps({ "Ca2+": 30.0, "HCO3-": 50.0, "Cl-": 200.0, "Na+": 100.0, }) result = calculate_langelier_index(ions_json, temperature_C=25.0, pH=6.5) # Should have negative LSI assert result["lsi"] < 0.0 # Interpretation should mention corrosive assert "corrosive" in result["interpretation"].lower() def test_lsi_temperature_effects(self): """Test that temperature affects LSI""" ions_json = json.dumps({ "Ca2+": 120.0, "HCO3-": 250.0, "Cl-": 150.0, "Na+": 100.0, }) result_25C = calculate_langelier_index(ions_json, temperature_C=25.0, pH=7.8) result_40C = calculate_langelier_index(ions_json, temperature_C=40.0, pH=7.8) # Higher temperature should increase scaling tendency # (retrograde solubility of CaCO3) assert result_40C["lsi"] > result_25C["lsi"] # Should have note about elevated temperature if "note" in result_40C: assert "temperature" in result_40C["note"].lower() def test_lsi_missing_calcium(self): """Test error handling when calcium is missing""" ions_json = json.dumps({ "Na+": 1000.0, "Cl-": 1545.0, }) with pytest.raises(ValueError, match="calcium"): calculate_langelier_index(ions_json, temperature_C=25.0) def test_lsi_missing_bicarbonate(self): """Test error handling when bicarbonate is missing""" ions_json = json.dumps({ "Ca2+": 120.0, "Cl-": 200.0, "Na+": 100.0, }) with pytest.raises(ValueError, match="bicarbonate|carbonate"): calculate_langelier_index(ions_json, temperature_C=25.0) def test_lsi_severe_scaling(self): """Test interpretation for severe scaling (LSI > 2.0)""" ions_json = json.dumps({ "Ca2+": 300.0, "Mg2+": 100.0, "HCO3-": 500.0, "Cl-": 100.0, "Na+": 50.0, }) result = calculate_langelier_index(ions_json, temperature_C=25.0, pH=9.0) # Should have very positive LSI assert result["lsi"] > 1.0 # Interpretation should mention severe scaling assert "severe" in result["interpretation"].lower() or "moderate" in result["interpretation"].lower() # Action should be immediate assert "immediate" in result["action_required"].lower() or "action" in result["action_required"].lower() class TestCrossValidation: """Cross-validation tests comparing results between tools""" def test_lsi_consistency(self): """Test that LSI is consistent between predict_scaling and calculate_langelier""" ions_json = json.dumps({ "Ca2+": 120.0, "Mg2+": 30.0, "HCO3-": 250.0, "Cl-": 150.0, "SO4-2": 80.0, "Na+": 100.0, }) lsi_result = calculate_langelier_index(ions_json, temperature_C=25.0, pH=7.8) scaling_result = predict_scaling_tendency(ions_json, temperature_C=25.0, pH=7.8) # LSI should be identical assert abs(lsi_result["lsi"] - scaling_result["lsi"]) < 0.01 # pH should be identical assert abs(lsi_result["pH"] - scaling_result["pH"]) < 0.01 def test_speciation_ph_consistency(self): """Test that pH is consistent when specified vs calculated""" ions_json = json.dumps({ "Na+": 1000.0, "Cl-": 1545.0, }) # First run: let PHREEQC calculate pH result1 = run_phreeqc_speciation(ions_json, temperature_C=25.0) # Second run: specify the calculated pH result2 = run_phreeqc_speciation(ions_json, temperature_C=25.0, pH=result1["pH"]) # pH should be very close assert abs(result1["pH"] - result2["pH"]) < 0.05 class TestRealWorldScenarios: """Test with real-world water compositions""" def test_cooling_tower_water(self): """Test typical cooling tower water""" ions_json = json.dumps({ "Ca2+": 150.0, "Mg2+": 50.0, "Na+": 120.0, "HCO3-": 280.0, "SO4-2": 120.0, "Cl-": 180.0, }) # Run all three tools speciation = run_phreeqc_speciation(ions_json, temperature_C=35.0) scaling = predict_scaling_tendency(ions_json, temperature_C=35.0) lsi = calculate_langelier_index(ions_json, temperature_C=35.0) # All should complete successfully assert speciation["pH"] > 0 assert "lsi" in scaling assert "lsi" in lsi def test_boiler_feedwater(self): """Test typical boiler feedwater (low hardness)""" ions_json = json.dumps({ "Na+": 50.0, "Ca2+": 2.0, # Very low hardness "Mg2+": 0.5, "HCO3-": 30.0, "Cl-": 25.0, "SO4-2": 10.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0) # Should have low ionic strength assert result["ionic_strength_M"] < 0.01 # LSI should be negative (corrosive to CaCO3) lsi_result = calculate_langelier_index(ions_json, temperature_C=25.0) assert lsi_result["lsi"] < 0.0 def test_brackish_groundwater(self): """Test brackish groundwater composition""" ions_json = json.dumps({ "Na+": 800.0, "Ca2+": 150.0, "Mg2+": 80.0, "K+": 20.0, "HCO3-": 350.0, "SO4-2": 250.0, "Cl-": 1200.0, }) result = run_phreeqc_speciation(ions_json, temperature_C=25.0) # Should have moderate ionic strength assert 0.01 < result["ionic_strength_M"] < 0.1 # Should complete without errors assert "interpretation" in result 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