Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
nrl_materials.py38.5 kB
""" NRL Material Property Classes for Galvanic Corrosion Modeling PROVENANCE: Direct 1:1 translation from MATLAB code by: Steven A. Policastro, Ph.D. Center for Corrosion Science and Engineering U.S. Naval Research Laboratory 4555 Overlook Avenue SW, Washington, DC 20375 Source: USNavalResearchLaboratory/corrosion-modeling-applications Directory: polarization-curve-modeling/ Files: HY80.m, HY100.m, SS316.m, Ti.m, I625.m, CuNi.m License: Public domain (U.S. Federal Government work) Date: 2025-10-19 This module provides material-specific electrochemical properties for: - HY-80 Steel (UNS K31820) - HY-100 Steel (UNS K32045) - SS 316 Stainless Steel (UNS S31600) - Titanium (UNS R50700) - Inconel 625 (UNS N06625) - CuNi 70-30 (UNS C71500) Each material class encapsulates: 1. Physical properties (molar mass, oxidation state) 2. Electrochemical reaction parameters (ΔG, β, diffusion layer thickness) 3. CSV coefficient loading for temperature/chloride-dependent activation energies 4. pH correction for activation energies """ import os import numpy as np import pandas as pd from pathlib import Path from typing import Tuple, Literal, Optional from functools import lru_cache # Path to vendored NRL coefficient CSV files NRL_COEFFICIENTS_DIR = Path(__file__).parent.parent / "external" / "nrl_coefficients" # Module-level CSV cache to avoid redundant pandas reads @lru_cache(maxsize=32) def _load_csv_coefficients_cached(csv_path_str: str) -> np.ndarray: """ Load polynomial coefficients from CSV file with caching. Args: csv_path_str: Full path to CSV file as string (for hashability) Returns: Array of 6 coefficients: [p00, p10, p01, p20, p11, p02] Raises: FileNotFoundError: If CSV file not found ValueError: If CSV format is incorrect """ csv_path = Path(csv_path_str) if not csv_path.exists(): raise FileNotFoundError( f"NRL coefficient file not found: {csv_path}\n" f"Expected in: {NRL_COEFFICIENTS_DIR}" ) # Read CSV (single row, 6 columns, no header) data = pd.read_csv(csv_path, header=None).values[0] if len(data) != 6: raise ValueError( f"Expected 6 coefficients in {csv_path.name}, got {len(data)}" ) return data class CorrodingMetal: """ Base class for corroding metals in NRL galvanic corrosion model. All material classes inherit from this base class and implement the calculate_delta_g() method with material-specific CSV files and pH correction logic. """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): """ Initialize a corroding metal instance. Args: name: Material name (e.g., "HY-80", "SS316") chloride_M: Chloride ion concentration, M (mol/L) temperature_C: Temperature, °C pH: pH value (1-13) velocity_m_s: Liquid velocity, m/s (default 0.0) """ self.name = name self.chloride_M = chloride_M self.temperature_C = temperature_C self.pH = pH self.velocity_m_s = velocity_m_s # Material properties (set by subclasses) self.metal_mass: float = 0.0 # Molar mass, g/mol self.oxidation_level_z: int = 0 # Electrons transferred in oxidation # Electrochemical reaction properties (set by subclasses) # Format: (delta_g_cathodic, delta_g_anodic) in J/mol self.delta_g_orr: Tuple[float, float] = (0.0, 0.0) self.delta_g_her: Tuple[float, float] = (0.0, 0.0) self.delta_g_metal_oxidation: Tuple[float, float] = (0.0, 0.0) self.delta_g_metal_passivation: Tuple[float, float] = (0.0, 0.0) self.delta_g_metal_pitting: Tuple[float, float] = (0.0, 0.0) # Transfer coefficients (β = symmetry factor) self.beta_orr: float = 0.0 self.beta_her: float = 0.0 self.beta_metal_oxidation: float = 0.0 self.beta_metal_passivation: float = 0.0 self.beta_metal_pitting: float = 0.0 # Diffusion layer thicknesses, cm self.del_orr: float = 0.0 self.del_her: float = 0.0 # Oxide film properties self.oxide_mass: float = 0.0 # Molar mass, g/mol self.oxide_density: float = 0.0 # Density, g/cm³ self.resistivity_of_oxide: float = 0.0 # Resistivity, Ω·cm self.passive_current_density: float = 0.0 # A/cm² self.passive_film_thickness: float = 0.0 # cm # Pitting properties (for materials that pit) self.pit_potential: Optional[float] = None # V_SCE @staticmethod def _validate_activation_energy( dg_cathodic: float, dg_anodic: float, reaction_type: str, chloride_M: float, temperature_C: float, pH: float, ) -> Tuple[float, float]: """ Validate activation energies and raise error if negative. CRITICAL SANITY CHECK (Codex recommendation v2): Activation energies must be positive (energy barrier for reaction). Negative dG produces exp(+value) → astronomical exchange currents. Known issue: HY80ORRCoeffs.csv produces negative dG_cathodic at (Cl=0.54 M, T=25C, pH=8), causing i0_cathodic ~ 1e+97 A/cm². Codex guidance: "Clamping to 10 kJ/mol fabricates kinetics with no physical basis. Treat as error path - refuse to return result so consumers know coefficients are out of range." If negative dG detected, raise ValueError with detailed context. Args: dg_cathodic: Cathodic activation energy (J/mol) dg_anodic: Anodic activation energy (J/mol) reaction_type: Reaction name (for error message) chloride_M: Chloride concentration (M) temperature_C: Temperature (°C) pH: pH value Returns: Tuple of (validated_dg_cathodic, validated_dg_anodic) Raises: ValueError: If either activation energy is negative """ errors = [] if dg_cathodic < 0: errors.append( f"Negative cathodic activation energy for {reaction_type}: " f"dG_cathodic = {dg_cathodic:.2e} J/mol" ) if dg_anodic < 0: errors.append( f"Negative anodic activation energy for {reaction_type}: " f"dG_anodic = {dg_anodic:.2e} J/mol" ) if errors: error_msg = ( f"Invalid activation energies detected - polynomial coefficients " f"are out of valid range at (Cl={chloride_M:.3f} M, T={temperature_C:.1f}°C, " f"pH={pH:.1f}).\n\n" f"Errors:\n - " + "\n - ".join(errors) + "\n\n" f"This indicates the NRL polynomial fit is being extrapolated beyond its " f"calibrated range. The resulting exchange currents would be physically " f"meaningless (clamping to arbitrary values would fabricate kinetics with " f"no physical basis).\n\n" f"Recommendations:\n" f"1. Use different material (e.g., SS316, HY100) with valid coefficient sets\n" f"2. Adjust conditions to stay within valid parameter ranges\n" f"3. Contact NRL for revised coefficients or valid range documentation\n\n" f"Known issue: HY80 ORR at seawater conditions (Cl≈0.5 M, T=25°C, pH=8) " f"produces negative dG_cathodic." ) raise ValueError(error_msg) return (dg_cathodic, dg_anodic) def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """ Calculate activation energies from CSV polynomial coefficients. This method must be implemented by each material subclass. Args: reaction_type: Type of electrochemical reaction chloride_M: Chloride ion concentration, M pH: pH value temperature_C: Temperature, °C Returns: Tuple of (delta_g_cathodic, delta_g_anodic) in J/mol """ raise NotImplementedError("Subclasses must implement calculate_delta_g()") def _load_csv_coefficients(self, csv_filename: str) -> np.ndarray: """ Load polynomial coefficients from CSV file (with caching). Args: csv_filename: Name of CSV file (e.g., "HY80ORRCoeffs.csv") Returns: Array of 6 coefficients: [p00, p10, p01, p20, p11, p02] Raises: FileNotFoundError: If CSV file not found in external/nrl_coefficients/ Note: Uses @lru_cache wrapper to avoid redundant pandas CSV reads. Cache hit rate should be high since files are static. """ csv_path = NRL_COEFFICIENTS_DIR / csv_filename # Use cached loader (requires string path for hashability) return _load_csv_coefficients_cached(str(csv_path)) def _apply_polynomial_response_surface( self, coeffs: np.ndarray, chloride_M: float, temperature_C: float ) -> float: """ Apply quadratic response surface polynomial. ΔG(T, Cl⁻) = p00 + p10*Cl⁻ + p01*T + p20*Cl⁻² + p11*Cl⁻*T + p02*T² CRITICAL: NRL polynomials expect temperature in KELVIN, not Celsius. The MATLAB reference uses Kelvin throughout (ElectrochemicalReductionReaction.m:69-108). Failing to convert causes large negative activation energies for HY80. Args: coeffs: Array of [p00, p10, p01, p20, p11, p02] chloride_M: Chloride concentration, M temperature_C: Temperature, °C (will be converted to Kelvin internally) Returns: Activation energy without pH correction, J/mol """ p00, p10, p01, p20, p11, p02 = coeffs # Convert Celsius to Kelvin (CRITICAL FIX per Codex investigation) temperature_K = temperature_C + 273.15 delta_g_no_pH = ( p00 + p10 * chloride_M + p01 * temperature_K + # Use Kelvin, not Celsius p20 * chloride_M**2 + p11 * chloride_M * temperature_K + # Use Kelvin, not Celsius p02 * temperature_K**2 # Use Kelvin, not Celsius ) return delta_g_no_pH class HY80(CorrodingMetal): """ HY-80 High-Yield Steel (UNS K31820) Low-alloy, high-strength steel used in naval applications. Active corrosion material (pitting, not passivating). Reactions: - Cathodic: ORR, HER - Anodic: Fe oxidation (Fe → Fe²⁺ + 2e⁻), Pitting """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 55.845 # Molar mass of Fe, g/mol self.oxidation_level_z = 2 # Fe → Fe²⁺ + 2e⁻ # Pitting properties self.pit_potential = -0.2 # V_SCE self.delta_g_metal_pitting = self.calculate_delta_g( "Pitting", chloride_M, pH, temperature_C ) self.beta_metal_pitting = 0.9999 # Metal oxidation self.delta_g_metal_oxidation = self.calculate_delta_g( "Oxidation", chloride_M, pH, temperature_C ) self.beta_metal_oxidation = 0.3 # Oxygen reduction reaction (cathodic) self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.89 self.del_orr = 0.085 # cm # Hydrogen evolution reaction (cathodic) self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.7 self.del_her = 0.15 # cm # Oxide film properties (Cr₂O₃ - default values) self.oxide_mass = 151.99 # g/mol self.oxide_density = 5.22 # g/cm³ self.resistivity_of_oxide = 5000.0e9 # Ω·cm self.passive_current_density = 1.0e-6 # A/cm² self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for HY-80 steel.""" if reaction_type == "ORR": # Load ORR coefficients coeffs = self._load_csv_coefficients("HY80ORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) # Apply pH correction (linear interpolation from pH 1 to 13) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 800.0e4 # J/mol (high barrier for reverse reaction) elif reaction_type == "HER": # Load HER coefficients coeffs = self._load_csv_coefficients("HY80HERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) # Apply pH correction dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 1000.0e4 # J/mol elif reaction_type == "Oxidation": # Load Fe oxidation coefficients coeffs = self._load_csv_coefficients("HY80FeOxCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) # Apply pH correction dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (13.0 - 1.0) dg_anodic = m * (pH - 13.0) + dg_a_min dg_cathodic = 80.0e4 # J/mol elif reaction_type == "Pitting": # Load pitting coefficients coeffs = self._load_csv_coefficients("HY80PitCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) # Apply pH correction (note: different slope direction for pitting) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_max - dg_a_min) / (13.0 - 1.0) dg_anodic = m * (pH - 1.0) + dg_a_min dg_cathodic = 20.0e4 # J/mol else: raise ValueError( f"Reaction type '{reaction_type}' not supported for HY-80" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) class HY100(CorrodingMetal): """ HY-100 High-Yield Steel (UNS K32045) Similar to HY-80 but higher strength. Active corrosion material (pitting, not passivating). Reactions: - Cathodic: ORR, HER - Anodic: Fe oxidation, Pitting """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 55.845 # Molar mass of Fe, g/mol self.oxidation_level_z = 2 # Fe → Fe²⁺ + 2e⁻ # Pitting properties self.pit_potential = -0.2 # V_SCE self.delta_g_metal_pitting = self.calculate_delta_g( "Pitting", chloride_M, pH, temperature_C ) self.beta_metal_pitting = 0.9999 # Metal oxidation self.delta_g_metal_oxidation = self.calculate_delta_g( "Oxidation", chloride_M, pH, temperature_C ) self.beta_metal_oxidation = 0.3 # ORR self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.89 self.del_orr = 0.085 # cm # HER self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.72 # Slightly different from HY-80 self.del_her = 0.15 # cm # Oxide film properties self.oxide_mass = 151.99 # g/mol self.oxide_density = 5.22 # g/cm³ self.resistivity_of_oxide = 5000.0e9 # Ω·cm self.passive_current_density = 1.0e-6 # A/cm² self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for HY-100 steel.""" if reaction_type == "ORR": coeffs = self._load_csv_coefficients("HY100ORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 800.0e4 elif reaction_type == "HER": coeffs = self._load_csv_coefficients("HY100HERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 1000.0e4 elif reaction_type == "Oxidation": coeffs = self._load_csv_coefficients("HY100FeOxCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (13.0 - 1.0) dg_anodic = m * (pH - 13.0) + dg_a_min dg_cathodic = 80.0e4 elif reaction_type == "Pitting": coeffs = self._load_csv_coefficients("HY100PitCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_max - dg_a_min) / (13.0 - 1.0) dg_anodic = m * (pH - 1.0) + dg_a_min dg_cathodic = 20.0e4 else: raise ValueError( f"Reaction type '{reaction_type}' not supported for HY-100" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) class SS316(CorrodingMetal): """ SS 316 Stainless Steel (UNS S31600) Austenitic chromium-nickel stainless steel. Passivating material with pitting susceptibility. Reactions: - Cathodic: ORR, HER - Anodic: Passivation (Cr₂O₃ film), Pitting """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 51.9961 # Molar mass of Cr, g/mol (rate-limiting element) self.oxidation_level_z = 3 # Cr → Cr³⁺ + 3e⁻ # Pitting properties self.pit_potential = -0.2 # V_SCE self.delta_g_metal_pitting = self.calculate_delta_g( "Pitting", chloride_M, pH, temperature_C ) self.beta_metal_pitting = 0.9999 # Passivation self.delta_g_metal_passivation = self.calculate_delta_g( "Passivation", chloride_M, pH, temperature_C ) self.beta_metal_passivation = 0.6 # ORR self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.89 self.del_orr = 0.085 # cm # HER self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.8 self.del_her = 0.15 # cm # Oxide film properties (Cr₂O₃) self.oxide_mass = 151.99 # g/mol self.oxide_density = 5.22 # g/cm³ self.resistivity_of_oxide = 5000.0e9 # Ω·cm self.passive_current_density = 1.0e-3 # A/cm² (higher than Ti/I625) self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for SS 316.""" pH_max = 13.0 pH_min = 1.0 if reaction_type == "ORR": coeffs = self._load_csv_coefficients("SS316ORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (pH_max - pH_min) dg_cathodic = m * (pH - pH_max) + dg_c_min dg_anodic = 800.0e4 elif reaction_type == "HER": coeffs = self._load_csv_coefficients("SS316HERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (pH_max - pH_min) dg_cathodic = m * (pH - pH_max) + dg_c_min dg_anodic = 1000.0e4 elif reaction_type == "Oxidation": # SS316 does not have active oxidation (only passivation) dg_anodic = 0.0 dg_cathodic = 0.0 elif reaction_type == "Passivation": coeffs = self._load_csv_coefficients("SS316PassCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (pH_max - pH_min) dg_anodic = m * (pH - pH_max) + dg_a_min dg_cathodic = 100.0e4 elif reaction_type == "Pitting": coeffs = self._load_csv_coefficients("SS316PitCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_max - dg_a_min) / (pH_max - pH_min) dg_anodic = m * (pH - pH_min) + dg_a_min dg_cathodic = 20.0e4 else: raise ValueError( f"Reaction type '{reaction_type}' not supported for SS 316" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) class Ti(CorrodingMetal): """ Titanium (UNS R50700) Highly corrosion-resistant passivating metal. Forms protective TiO₂ film. Reactions: - Cathodic: ORR, HER - Anodic: Passivation (TiO₂ film) - Does NOT pit or actively corrode in seawater """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 47.88 # Molar mass of Ti, g/mol self.oxidation_level_z = 3 # Ti → Ti³⁺ + 3e⁻ # Passivation self.delta_g_metal_passivation = self.calculate_delta_g( "Passivation", chloride_M, pH, temperature_C ) self.beta_metal_passivation = 0.3 # ORR self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.65 self.del_orr = 0.085 # cm # HER self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.75 self.del_her = 0.15 # cm # Oxide film properties (TiO₂) self.oxide_mass = 143.76 # g/mol self.oxide_density = 4.49 # g/cm³ self.resistivity_of_oxide = 50000.0e9 # Ω·cm (very resistive) self.passive_current_density = 1.0e-6 # A/cm² self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for titanium.""" if reaction_type == "ORR": coeffs = self._load_csv_coefficients("TiORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 800.0e4 elif reaction_type == "HER": coeffs = self._load_csv_coefficients("TiHERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 1000.0e4 elif reaction_type == "Oxidation": # Ti does not have active oxidation (only passivation) dg_anodic = 0.0 dg_cathodic = 0.0 elif reaction_type == "Passivation": coeffs = self._load_csv_coefficients("TiPassCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (13.0 - 1.0) dg_anodic = m * (pH - 13.0) + dg_a_min dg_cathodic = 80.0e4 else: raise ValueError( f"Reaction type '{reaction_type}' not supported for titanium" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) class I625(CorrodingMetal): """ Inconel 625 (UNS N06625) Nickel-chromium-molybdenum superalloy. Highly corrosion-resistant passivating material. Reactions: - Cathodic: ORR, HER - Anodic: Passivation (Cr₂O₃ + NiO film) - Does NOT pit in seawater Note: Includes velocity-dependent diffusion layer for ORR. """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 58.6934 # Molar mass of Ni, g/mol (primary element) self.oxidation_level_z = 3 # Ni → Ni³⁺ + 3e⁻ (in passive film) # Passivation self.delta_g_metal_passivation = self.calculate_delta_g( "Passivation", chloride_M, pH, temperature_C ) self.beta_metal_passivation = 0.21 # ORR self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.89 # Velocity-dependent diffusion layer thickness liq_v0 = 50.0 # Reference velocity, m/s self.del_orr = 0.085 * (1.0 - (velocity_m_s / liq_v0)) # cm # HER self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.7 self.del_her = 0.15 # cm # Oxide film properties (Cr₂O₃ + NiO) self.oxide_mass = 165.39 # g/mol self.oxide_density = 4.84 # g/cm³ self.resistivity_of_oxide = 50000.0e9 # Ω·cm self.passive_current_density = 1.0e-6 # A/cm² self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for Inconel 625.""" if reaction_type == "ORR": coeffs = self._load_csv_coefficients("I625ORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 800.0e4 elif reaction_type == "HER": coeffs = self._load_csv_coefficients("I625HERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 1000.0e4 elif reaction_type == "Oxidation": # I625 does not have active oxidation (only passivation) dg_anodic = 0.0 dg_cathodic = 0.0 elif reaction_type == "Passivation": coeffs = self._load_csv_coefficients("I625PassCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (13.0 - 1.0) dg_anodic = m * (pH - 13.0) + dg_a_min dg_cathodic = 80.0e4 else: raise ValueError( f"Reaction type '{reaction_type}' not supported for Inconel 625" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) class CuNi(CorrodingMetal): """ CuNi 70-30 (Copper-Nickel Alloy, UNS C71500) 70% Cu, 30% Ni alloy used in marine applications. Active corrosion material (Cu oxidation). Reactions: - Cathodic: ORR, HER - Anodic: Cu oxidation (Cu → Cu⁺ + e⁻) - Does NOT form passive film Note: Includes velocity-dependent diffusion layer for ORR. """ def __init__( self, name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ): super().__init__(name, chloride_M, temperature_C, pH, velocity_m_s) # Material properties self.metal_mass = 63.546 # Molar mass of Cu, g/mol self.oxidation_level_z = 1 # Cu → Cu⁺ + e⁻ # Metal oxidation (Cu) self.delta_g_metal_oxidation = self.calculate_delta_g( "Oxidation", chloride_M, pH, temperature_C ) self.beta_metal_oxidation = 0.7 # ORR self.delta_g_orr = self.calculate_delta_g( "ORR", chloride_M, pH, temperature_C ) self.beta_orr = 0.72 # Velocity-dependent diffusion layer thickness liq_v0 = 7.5 # Reference velocity, m/s (lower than I625) self.del_orr = 0.085 * (1.0 - (velocity_m_s / liq_v0)) # cm # HER self.delta_g_her = self.calculate_delta_g( "HER", chloride_M, pH, temperature_C ) self.beta_her = 0.6 self.del_her = 0.15 # cm # Oxide film properties (not used for CuNi, but set for consistency) self.oxide_mass = 151.99 # g/mol self.oxide_density = 5.22 # g/cm³ self.resistivity_of_oxide = 5000.0e9 # Ω·cm self.passive_current_density = 1.0e-6 # A/cm² self.passive_film_thickness = 2.5e-7 # cm def calculate_delta_g( self, reaction_type: Literal["ORR", "HER", "Oxidation", "Passivation", "Pitting"], chloride_M: float, pH: float, temperature_C: float ) -> Tuple[float, float]: """Calculate activation energies for CuNi 70-30.""" if reaction_type == "ORR": coeffs = self._load_csv_coefficients("cuniORRCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 800.0e4 elif reaction_type == "HER": coeffs = self._load_csv_coefficients("cuniHERCoeffs.csv") dg_cathodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_c_max = 1.1 * dg_cathodic_no_pH dg_c_min = 0.9 * dg_cathodic_no_pH m = (dg_c_min - dg_c_max) / (13.0 - 1.0) dg_cathodic = m * (pH - 13.0) + dg_c_min dg_anodic = 1000.0e4 elif reaction_type == "Oxidation": coeffs = self._load_csv_coefficients("cuniCuOxCoeffs.csv") dg_anodic_no_pH = self._apply_polynomial_response_surface( coeffs, chloride_M, temperature_C ) dg_a_max = 1.1 * dg_anodic_no_pH dg_a_min = 0.9 * dg_anodic_no_pH m = (dg_a_min - dg_a_max) / (13.0 - 1.0) dg_anodic = m * (pH - 13.0) + dg_a_min dg_cathodic = 25.0e4 else: raise ValueError( f"Reaction type '{reaction_type}' not supported for CuNi 70-30" ) # Validate and clamp negative activation energies (Codex recommendation) return self._validate_activation_energy( dg_cathodic, dg_anodic, reaction_type, chloride_M, temperature_C, pH ) # Material factory function def create_material( material_name: str, chloride_M: float, temperature_C: float, pH: float, velocity_m_s: float = 0.0 ) -> CorrodingMetal: """ Factory function to create material instances by name. Args: material_name: Material identifier (case-insensitive) chloride_M: Chloride ion concentration, M temperature_C: Temperature, °C pH: pH value (1-13) velocity_m_s: Liquid velocity, m/s (default 0.0) Returns: Instance of appropriate material class Raises: ValueError: If material_name not recognized Supported Materials: - "HY80", "HY-80", "HY_80" → HY80 - "HY100", "HY-100", "HY_100" → HY100 - "SS316", "316", "SS_316", "316L" → SS316 - "Ti", "Titanium" → Ti - "I625", "Inconel625", "Inconel_625" → I625 - "CuNi", "CuNi7030", "CuNi_70_30" → CuNi """ material_map = { "HY80": HY80, "HY-80": HY80, "HY_80": HY80, "HY100": HY100, "HY-100": HY100, "HY_100": HY100, "SS316": SS316, "316": SS316, "SS_316": SS316, "316L": SS316, "TI": Ti, "TITANIUM": Ti, "I625": I625, "INCONEL625": I625, "INCONEL_625": I625, "CUNI": CuNi, "CUNI7030": CuNi, "CUNI_70_30": CuNi, } material_key = material_name.upper().replace(" ", "_") if material_key not in material_map: supported = list(set([k.split("_")[0] for k in material_map.keys()])) raise ValueError( f"Unknown material: '{material_name}'\n" f"Supported materials: {supported}" ) material_class = material_map[material_key] return material_class( name=material_name, chloride_M=chloride_M, temperature_C=temperature_C, pH=pH, velocity_m_s=velocity_m_s ) __all__ = [ "CorrodingMetal", "HY80", "HY100", "SS316", "Ti", "I625", "CuNi", "create_material", ]

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