Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
test_mass_transfer.py18.2 kB
""" Tests for mass transfer calculations (utils/mass_transfer.py) Validates: - Dimensionless number calculations (Re, Sc) - Sherwood correlations (laminar/turbulent pipe, flat plate) - Mass transfer coefficient - Limiting current density for ORR - End-to-end workflow All tests use textbook benchmark values with ±10% tolerance for empirical correlations. """ import pytest from utils.mass_transfer import ( calculate_reynolds_number, calculate_schmidt_number, calculate_kinematic_viscosity, calculate_sherwood_number_laminar_pipe, calculate_sherwood_number_turbulent_pipe, calculate_sherwood_number_flat_plate, calculate_sherwood_number, calculate_mass_transfer_coefficient, calculate_limiting_current_density, calculate_limiting_current_from_flow, FARADAY_CONSTANT, ) # ============================================================================ # Test Dimensionless Numbers # ============================================================================ class TestReynoldsNumber: """Test Reynolds number calculations""" def test_laminar_flow(self): """Laminar flow in pipe: Re < 2300""" # Water at 20°C in small pipe Re = calculate_reynolds_number( velocity_m_s=0.1, # 10 cm/s length_m=0.01, # 1 cm diameter density_kg_m3=1000.0, viscosity_Pa_s=0.001, ) assert Re == pytest.approx(1000.0, rel=0.01) assert Re < 2300, "Should be laminar" def test_turbulent_flow(self): """Turbulent flow in pipe: Re > 4000""" # Water at 20°C, typical pipe flow Re = calculate_reynolds_number( velocity_m_s=1.0, length_m=0.05, # 5 cm diameter density_kg_m3=1000.0, viscosity_Pa_s=0.001, ) assert Re == pytest.approx(50000.0, rel=0.01) assert Re > 4000, "Should be turbulent" def test_seawater_pipe(self): """Seawater in industrial pipe""" Re = calculate_reynolds_number( velocity_m_s=2.0, length_m=0.10, # 10 cm diameter density_kg_m3=1025.0, # Seawater density viscosity_Pa_s=0.0011, # Seawater at 25°C ) assert Re == pytest.approx(186363.6, rel=0.01) class TestSchmidtNumber: """Test Schmidt number calculations""" def test_oxygen_in_water_25C(self): """Oxygen in water at 25°C: Sc ≈ 500-600""" # Typical values from literature nu_water = 1.0e-6 # m²/s (kinematic viscosity) D_O2 = 2.0e-9 # m²/s (O2 diffusivity at 25°C) Sc = calculate_schmidt_number(nu_water, D_O2) assert Sc == pytest.approx(500.0, rel=0.01) assert 400 < Sc < 700, "Typical range for dissolved gases" def test_gas_phase(self): """Gases have Sc ≈ 1""" Sc = calculate_schmidt_number(1.5e-5, 2.0e-5) assert Sc == pytest.approx(0.75, abs=0.5) def test_high_sc_liquid(self): """High Sc for large molecules in liquid""" Sc = calculate_schmidt_number(1.0e-6, 1.0e-10) assert Sc == pytest.approx(10000.0, rel=0.01) class TestKinematicViscosity: """Test kinematic viscosity calculation""" def test_water_20C(self): """Water at 20°C: ν ≈ 1.0×10⁻⁶ m²/s""" nu = calculate_kinematic_viscosity(0.001, 1000.0) assert nu == pytest.approx(1.0e-6, rel=0.01) def test_seawater_25C(self): """Seawater at 25°C""" nu = calculate_kinematic_viscosity(0.0011, 1025.0) assert nu == pytest.approx(1.073e-6, rel=0.02) # ============================================================================ # Test Sherwood Number Correlations # ============================================================================ class TestSherwoodLaminarPipe: """Test laminar pipe Sherwood correlations""" def test_fully_developed_laminar(self): """Fully developed laminar: Sh = 3.66 (constant wall concentration)""" # Very long pipe to suppress Graetz entrance effects # Gz = (D/L)*Re*Sc = (0.05/5000)*1000*600 = 6 << 10 Sh = calculate_sherwood_number_laminar_pipe( Re=1000, Sc=600, length_m=5000.0, # Very long pipe (5 km) to reach fully developed diameter_m=0.05, entry_effects=True, ) assert Sh == pytest.approx(3.66, abs=0.5) def test_fully_developed_laminar_no_entry(self): """Fully developed laminar with entry_effects=False""" # Short pipe but disable entrance effects Sh = calculate_sherwood_number_laminar_pipe( Re=1000, Sc=600, length_m=10.0, diameter_m=0.05, entry_effects=False, # Force fully developed ) assert Sh == pytest.approx(3.66, abs=0.1) def test_developing_flow_graetz(self): """Developing laminar flow with Graetz effects""" # Use parameters that give Gz in valid range (10 < Gz < 2000) # Target Gz ≈ 1000 for strong entrance effects # Gz = (D/L)*Re*Sc → L = D*Re*Sc/Gz = 0.05*1200*600/1000 = 36 m Sh = calculate_sherwood_number_laminar_pipe( Re=1200, Sc=600, length_m=36.0, # Long enough to keep Gz in valid range diameter_m=0.05, entry_effects=True, ) # Graetz: Gz = (0.05/36) * 1200 * 600 = 1000 (within valid range 10-2000) # Sh ≈ 1.86 * (1000)^(1/3) ≈ 18.6 assert Sh > 10, "Entrance effects should increase Sh significantly" assert Sh == pytest.approx(18.6, rel=0.15) class TestSherwoodTurbulentPipe: """Test turbulent pipe Sherwood correlations""" def test_turbulent_pipe_chilton_colburn(self): """Turbulent pipe: Sh = 0.023 * Re^0.8 * Sc^(1/3) from ht library""" Re = 50000 Sc = 600 Sh = calculate_sherwood_number_turbulent_pipe(Re, Sc) # Expected from ht.turbulent_Colburn(Re=50000, Pr=600) Sh_expected = 1114.18 assert Sh == pytest.approx(Sh_expected, rel=0.01) def test_seawater_turbulent(self): """Seawater turbulent flow (Re=100k, Sc=600)""" Sh = calculate_sherwood_number_turbulent_pipe(100000, 600) # Expected from ht.turbulent_Colburn(Re=100000, Pr=600) assert Sh == pytest.approx(1939.90, rel=0.01) class TestSherwoodFlatPlate: """Test flat plate boundary layer correlations""" def test_laminar_boundary_layer(self): """Laminar flat plate: Sh = 0.664 * Re^0.5 * Sc^(1/3)""" Re = 10000 Sc = 600 Sh = calculate_sherwood_number_flat_plate(Re, Sc, regime="laminar") # Expected: 0.664 * (10000^0.5) * (600^(1/3)) = 560.04 assert Sh == pytest.approx(560.04, rel=0.01) def test_turbulent_boundary_layer(self): """Turbulent flat plate: Sh = 0.037 * Re^0.8 * Sc^(1/3)""" Re = 1e6 Sc = 600 Sh = calculate_sherwood_number_flat_plate(Re, Sc, regime="turbulent") # Expected: 0.037 * (1e6^0.8) * (600^(1/3)) = 19690.29 assert Sh == pytest.approx(19690.29, rel=0.01) class TestSherwoodGeneral: """Test general Sherwood calculator with auto regime detection""" def test_auto_laminar_pipe(self): """Automatic laminar detection for pipe""" Sh = calculate_sherwood_number( Re=1000, Sc=600, geometry="pipe", length_m=5000.0, diameter_m=0.05 ) # Long pipe suppresses Graetz effects: Gz = (0.05/5000)*1000*600 = 6 << 10 assert Sh == pytest.approx(3.66, abs=1.0) def test_auto_turbulent_pipe(self): """Automatic turbulent detection for pipe""" Sh = calculate_sherwood_number(Re=50000, Sc=600, geometry="pipe") # Should use turbulent correlation: ~1114 assert Sh == pytest.approx(1114.18, rel=0.02) def test_auto_laminar_plate(self): """Automatic laminar detection for flat plate""" Sh = calculate_sherwood_number(Re=1e4, Sc=600, geometry="plate") assert Sh == pytest.approx(560.04, rel=0.01) def test_transitional_regime(self): """Transitional regime (2300 < Re < 10000): uses laminar correlation""" Sh = calculate_sherwood_number( Re=5000, Sc=600, geometry="pipe", length_m=1000.0, diameter_m=0.05 ) # Transitional now uses laminar correlation (conservative for corrosion) # turbulent_Colburn not valid below Re=10,000 # Gz = (0.05/1000)*5000*600 = 150 → Sh ≈ 1.86*150^(1/3) ≈ 9.9 assert Sh > 3.66 # Should be > fully developed assert Sh < 20 # But not too high (Graetz limited) # ============================================================================ # Test Mass Transfer Coefficient # ============================================================================ class TestMassTransferCoefficient: """Test mass transfer coefficient calculations""" def test_k_L_from_Sh(self): """Convert Sh → k_L""" Sh = 100 D = 2.0e-9 # m²/s (O2 in water) L = 0.05 # m k_L = calculate_mass_transfer_coefficient(Sh, D, L) expected = 100 * 2.0e-9 / 0.05 # = 4.0e-6 m/s assert k_L == pytest.approx(expected, rel=0.01) assert k_L == pytest.approx(4.0e-6, rel=0.01) def test_typical_seawater_turbulent(self): """Typical k_L for turbulent seawater""" Sh = 500 D = 2.1e-9 # m²/s (O2 at 25°C) L = 0.10 # 10 cm diameter pipe k_L = calculate_mass_transfer_coefficient(Sh, D, L) assert 1e-5 < k_L < 1e-4, "Typical range for turbulent flow" # ============================================================================ # Test Limiting Current Density # ============================================================================ class TestLimitingCurrentDensity: """Test limiting current calculations for ORR""" def test_i_lim_basic(self): """Basic limiting current: i_lim = n*F*k_L*c_O2""" k_L = 5.0e-5 # m/s c_O2 = 0.20 # mol/m³ (typical seawater DO) n = 4 # electrons for ORR i_lim = calculate_limiting_current_density(k_L, c_O2, n_electrons=n) expected = 4 * FARADAY_CONSTANT * 5.0e-5 * 0.20 assert i_lim == pytest.approx(expected, rel=0.01) assert i_lim == pytest.approx(3.86, rel=0.01) # A/m² def test_seawater_typical(self): """Typical seawater: DO=6.5 mg/L ≈ 0.203 mol/m³""" k_L = 3.0e-5 # m/s (moderate turbulence) c_O2 = 0.203 # mol/m³ i_lim = calculate_limiting_current_density(k_L, c_O2) assert i_lim == pytest.approx(2.35, rel=0.10) # A/m² (23.5 mA/dm²) def test_low_do_stagnant(self): """Low DO, stagnant conditions""" k_L = 1.0e-6 # m/s (very low mass transfer) c_O2 = 0.10 # mol/m³ (low DO) i_lim = calculate_limiting_current_density(k_L, c_O2) assert i_lim < 0.1, "Low limiting current for stagnant conditions" # ============================================================================ # Test End-to-End Workflow # ============================================================================ class TestLimitingCurrentFromFlow: """Test integrated workflow: flow → i_lim""" def test_turbulent_seawater_pipe(self): """End-to-end: turbulent seawater in pipe""" result = calculate_limiting_current_from_flow( velocity_m_s=1.0, diameter_m=0.05, length_m=1.0, density_kg_m3=1025.0, viscosity_Pa_s=0.0011, diffusivity_m2_s=2.1e-9, oxygen_concentration_mol_m3=0.20, temperature_C=25.0, geometry="pipe", ) assert result["Re"] > 10000, "Should be turbulent" assert result["regime"] == "turbulent" assert result["Sc"] == pytest.approx(524.0, rel=0.10) assert result["Sh"] > 300, "High Sh for turbulent flow" assert result["k_L_m_s"] > 1e-5, "Significant mass transfer" assert result["i_lim_A_m2"] > 1.0, "Practical limiting current" def test_laminar_pipe(self): """End-to-end: laminar flow""" result = calculate_limiting_current_from_flow( velocity_m_s=0.05, diameter_m=0.01, length_m=1000.0, # Long pipe to suppress Graetz effects density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.25, temperature_C=20.0, ) assert result["Re"] == pytest.approx(500.0, rel=0.01) assert result["regime"] == "laminar" # Gz = (0.01/1000)*500*1000 = 5 << 10, so Sh ≈ 3.66 assert result["Sh"] == pytest.approx(3.66, abs=1.0) def test_output_structure(self): """Verify output dictionary structure""" result = calculate_limiting_current_from_flow( velocity_m_s=1.0, diameter_m=0.05, length_m=1.0, density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.20, ) # Check all required keys present assert "Re" in result assert "Sc" in result assert "Sh" in result assert "k_L_m_s" in result assert "i_lim_A_m2" in result assert "regime" in result assert "temperature_C" in result # Check types assert isinstance(result["Re"], float) assert isinstance(result["regime"], str) def test_flat_plate_laminar(self): """End-to-end: laminar flat plate boundary layer""" result = calculate_limiting_current_from_flow( velocity_m_s=0.1, length_m=0.5, # Plate length (characteristic length for plate) geometry="plate", density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.20, temperature_C=25.0, ) # Re = V*L/nu = 0.1*0.5/(0.001/1000) = 50,000 → laminar plate assert result["Re"] == pytest.approx(50000.0, rel=0.01) assert result["regime"] == "laminar" # Laminar plate: Sh = 0.664*Re^0.5*Sc^(1/3) # Sc = 0.001/1000 / 2e-9 = 500 # Sh = 0.664*50000^0.5*500^(1/3) ≈ 1102 assert result["Sh"] > 1000 assert result["Sh"] < 1200 def test_flat_plate_missing_length_error(self): """Flat plate requires length_m parameter""" with pytest.raises(ValueError, match="length_m is required for geometry='plate'"): calculate_limiting_current_from_flow( velocity_m_s=1.0, diameter_m=0.05, geometry="plate", # Plate geometry but no length_m provided density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.20, ) # ============================================================================ # Test Physical Sanity Checks # ============================================================================ class TestPhysicalSanity: """Sanity checks for physical realism""" def test_increasing_velocity_increases_i_lim(self): """Higher velocity → higher i_lim""" velocities = [0.1, 0.5, 1.0, 2.0] i_lims = [] for v in velocities: result = calculate_limiting_current_from_flow( velocity_m_s=v, diameter_m=0.05, length_m=1.0, density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.20, ) i_lims.append(result["i_lim_A_m2"]) # i_lim should increase monotonically with velocity for i in range(len(i_lims) - 1): assert i_lims[i] < i_lims[i + 1], \ f"i_lim should increase: {i_lims[i]:.2f} < {i_lims[i+1]:.2f}" def test_higher_do_higher_i_lim(self): """Higher DO → higher i_lim (linear relationship)""" result_low = calculate_limiting_current_from_flow( velocity_m_s=1.0, diameter_m=0.05, length_m=1.0, density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.10, # Low DO ) result_high = calculate_limiting_current_from_flow( velocity_m_s=1.0, diameter_m=0.05, length_m=1.0, density_kg_m3=1000.0, viscosity_Pa_s=0.001, diffusivity_m2_s=2.0e-9, oxygen_concentration_mol_m3=0.30, # High DO ) # i_lim should scale linearly with DO ratio = result_high["i_lim_A_m2"] / result_low["i_lim_A_m2"] assert ratio == pytest.approx(3.0, rel=0.01), "i_lim ∝ c_O2" def test_turbulent_higher_than_laminar(self): """Turbulent Sh > Laminar Sh""" Sh_laminar = calculate_sherwood_number(Re=1000, Sc=600, geometry="pipe") Sh_turbulent = calculate_sherwood_number(Re=50000, Sc=600, geometry="pipe") assert Sh_turbulent > Sh_laminar * 10, \ "Turbulent mass transfer should be much higher" # ============================================================================ # Test Error Handling # ============================================================================ class TestErrorHandling: """Test error cases and warnings""" def test_invalid_geometry(self): """Invalid geometry should raise ValueError""" with pytest.raises(ValueError, match="Unknown geometry"): calculate_sherwood_number(Re=5000, Sc=600, geometry="sphere") def test_negative_values_caught(self): """Negative physical values should still compute (though unphysical)""" # Library should not crash, but may produce nonsense results Re = calculate_reynolds_number(-1.0, 0.05, 1000.0, 0.001) assert Re < 0 # Nonsense but computable

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