Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
test_electrochemistry_db.py12.8 kB
""" Unit tests for ElectrochemistryDatabase Tests Butler-Volmer calculations, Arrhenius temperature correction, Tafel slope parsing, and hybrid YAML/semantic search. """ import pytest from unittest.mock import Mock, patch, mock_open from pathlib import Path import sys import yaml import math # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from utils.electrochemistry_db import ElectrochemistryDatabase # Mock YAML data MOCK_YAML = """ metadata: extraction_method: "Semantic search + Butler-Volmer calculation" extraction_date: "2025-10-18" base_parameters: fe_oxidation: reaction: "Fe → Fe²⁺ + 2e⁻" n_electrons: 2 alpha: 0.5 i0_A_per_m2_25C: 1.0e-5 activation_energy_kJ_per_mol: 40.0 source: "Handbook of Corrosion Engineering" reactions: carbon_steel_fe_oxidation_seawater: material: "Carbon Steel" reaction: "Fe_oxidation" electrolyte: "seawater" ba_V_per_decade: 0.060 bc_V_per_decade: -0.120 i0_A_per_m2: 1.0e-5 alpha: 0.5 n_electrons: 2 temperature_C: 25 pH: 8.2 source: "Handbook of Corrosion Engineering, Fig 3.14" source_document: "Handbook of Corrosion Engineering" extraction_date: "2025-10-18" """ class TestElectrochemistryDatabase: """Test suite for ElectrochemistryDatabase""" def test_yaml_lookup_carbon_steel(self): """Test YAML lookup for carbon steel Fe oxidation""" with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase(semantic_search_function=None) result = db.get_tafel_slopes( material="Carbon Steel", reaction="Fe_oxidation", electrolyte="seawater", temperature_C=25.0, ) assert result["ba_V_per_decade"] == 0.060 assert result["bc_V_per_decade"] == -0.120 assert result["i0_A_per_m2"] == 1.0e-5 assert result["provenance"]["method"] == "yaml_lookup" assert result["provenance"]["confidence"] == "high" def test_butler_volmer_calculation_25C(self): """Test Butler-Volmer Tafel slope calculation at 25°C""" R = 8.314 # J/mol·K F = 96485 # C/mol T = 298.15 # 25°C in K alpha = 0.5 n = 2 # Expected Tafel slope RT_over_alphaF = (R * T) / (alpha * F) expected_ba = 2.303 * RT_over_alphaF with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase() # Calculate via Butler-Volmer result = db._calculate_from_butler_volmer( material="Test Material", reaction="fe_oxidation", electrolyte="seawater", temperature_C=25.0, pH=7.0, ) assert result is not None assert abs(result["ba_V_per_decade"] - expected_ba) < 0.001 assert result["bc_V_per_decade"] < 0 # Cathodic is negative assert result["provenance"]["method"] == "calculated" def test_arrhenius_temperature_correction(self): """Test Arrhenius correction for temperature dependency of i0""" with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase() # Calculate at 25°C (reference) result_25C = db._calculate_from_butler_volmer( material="Test", reaction="fe_oxidation", electrolyte="seawater", temperature_C=25.0, pH=7.0, ) # Calculate at 60°C (higher temperature) result_60C = db._calculate_from_butler_volmer( material="Test", reaction="fe_oxidation", electrolyte="seawater", temperature_C=60.0, pH=7.0, ) # i0 should increase with temperature (Arrhenius) assert result_60C["i0_A_per_m2"] > result_25C["i0_A_per_m2"] # Tafel slopes should also change with temperature (RT/F term) assert result_60C["ba_V_per_decade"] > result_25C["ba_V_per_decade"] def test_reaction_key_normalization(self): """Test normalization of material/reaction/electrolyte keys""" db = ElectrochemistryDatabase() # Test various input formats key1 = db._normalize_reaction_key("Carbon Steel", "Fe Oxidation", "seawater") key2 = db._normalize_reaction_key("carbon steel", "fe_oxidation", "seawater") assert key1 == key2 # Should normalize to same key def test_regex_parsing_tafel_slopes(self): """Test regex parsing of Tafel slopes from text""" db = ElectrochemistryDatabase() text = ( "For this material, anodic Tafel slope ba = 0.060 V/decade " "and cathodic Tafel slope bc = -0.120 V/decade" ) results = [{"text": text, "source": "Test", "path": "test.pdf", "id": "001"}] parsed = db._parse_electrochemistry_from_text(results) assert parsed["ba"] is not None assert abs(parsed["ba"] - 0.060) < 0.001 assert parsed["bc"] is not None assert abs(parsed["bc"] - (-0.120)) < 0.001 def test_regex_parsing_exchange_current_density(self): """Test regex parsing of exchange current density""" db = ElectrochemistryDatabase() text = "exchange current density i0 = 1.0e-5 A/m²" results = [{"text": text, "source": "Test", "path": "test.pdf", "id": "001"}] parsed = db._parse_electrochemistry_from_text(results) assert parsed["i0"] is not None assert abs(parsed["i0"] - 1.0e-5) < 1e-6 def test_confidence_scoring_electrochemistry(self): """Test confidence scoring based on found parameters""" db = ElectrochemistryDatabase() # Low confidence: only ba found text_low = "anodic Tafel slope: 0.060 V/decade" results_low = [{"text": text_low, "source": "Test", "path": "test.pdf", "id": "001"}] parsed_low = db._parse_electrochemistry_from_text(results_low) assert parsed_low["confidence"] == "medium" # High confidence: ba, bc, i0 found text_high = ( "anodic Tafel slope ba = 0.060 V/decade, " "cathodic Tafel slope bc = -0.120 V/decade, " "exchange current density i0 = 1.0e-5 A/m²" ) results_high = [{"text": text_high, "source": "Test", "path": "test.pdf", "id": "001"}] parsed_high = db._parse_electrochemistry_from_text(results_high) assert parsed_high["confidence"] == "high" def test_semantic_search_fallback(self): """Test fallback to semantic search when YAML misses""" def mock_search(query, top_k=5): return [ { "text": "For this reaction, anodic Tafel slope ba = 0.070 V/decade", "source": "Handbook", "path": "handbook.pdf", "id": "chunk_001", } ] with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase(semantic_search_function=mock_search) # Request unknown reaction result = db.get_tafel_slopes( material="Unknown Material", reaction="unknown_reaction", electrolyte="brine", ) assert result["provenance"]["method"] == "semantic_search" assert result["ba_V_per_decade"] is not None def test_empty_result_handling(self): """Test handling when no data is found""" def mock_search_empty(query, top_k=5): return [] with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase(semantic_search_function=mock_search_empty) result = db.get_tafel_slopes( material="Unknown", reaction="unknown", electrolyte="unknown", ) assert result["ba_V_per_decade"] is None assert result["bc_V_per_decade"] is None assert result["i0_A_per_m2"] is None assert result["provenance"]["method"] == "none" def test_three_tier_lookup_priority(self): """Test that lookup priority is: YAML → Butler-Volmer → Semantic Search""" def mock_search(query, top_k=5): # Should not be called if YAML or Butler-Volmer succeed pytest.fail("Semantic search should not be called") with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase(semantic_search_function=mock_search) # This should hit YAML (highest priority) result = db.get_tafel_slopes( material="Carbon Steel", reaction="Fe_oxidation", electrolyte="seawater", ) assert result["provenance"]["method"] == "yaml_lookup" def test_cache_behavior_electrochemistry(self): """Test that results are cached""" with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase() # First call result1 = db.get_tafel_slopes( material="Carbon Steel", reaction="Fe_oxidation", electrolyte="seawater", ) # Second call should hit cache result2 = db.get_tafel_slopes( material="Carbon Steel", reaction="Fe_oxidation", electrolyte="seawater", ) assert result1 is result2 def test_list_available_reactions(self): """Test listing all available reactions in YAML""" with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase() available = db.list_available_reactions() assert "carbon_steel_fe_oxidation_seawater" in available def test_provenance_metadata_structure_electrochemistry(self): """Test that provenance metadata has all required fields""" with patch('builtins.open', mock_open(read_data=MOCK_YAML)): with patch('pathlib.Path.exists', return_value=True): db = ElectrochemistryDatabase() result = db.get_tafel_slopes( material="Carbon Steel", reaction="Fe_oxidation", electrolyte="seawater", ) provenance = result["provenance"] # Check all required fields assert "method" in provenance assert "confidence" in provenance assert "extraction_date" in provenance assert "source_document" in provenance assert "source_repo" in provenance assert "quality_note" in provenance class TestButlerVolmerPhysics: """Test Butler-Volmer physics calculations""" def test_tafel_slope_temperature_dependency(self): """Test that Tafel slopes increase with temperature""" R = 8.314 F = 96485 alpha = 0.5 n = 2 # Calculate at two temperatures T1 = 298.15 # 25°C T2 = 333.15 # 60°C ba1 = 2.303 * (R * T1) / (alpha * n * F) ba2 = 2.303 * (R * T2) / (alpha * n * F) # Higher temperature should give higher Tafel slope assert ba2 > ba1 def test_exchange_current_arrhenius(self): """Test Arrhenius temperature correction for i0""" i0_ref = 1.0e-5 # A/m² at 25°C Ea = 40000 # J/mol R = 8.314 # J/mol·K T_ref = 298.15 # 25°C T_test = 333.15 # 60°C # Arrhenius: i0(T) = i0_ref × exp(-Ea/R × (1/T - 1/T_ref)) i0_test = i0_ref * math.exp(-(Ea / R) * (1 / T_test - 1 / T_ref)) # i0 at 60°C should be higher than at 25°C assert i0_test > i0_ref if __name__ == "__main__": pytest.main([__file__, "-v"])

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