Module pychnosz.geochemistry

Geochemistry package for CHNOSZ.

This package provides specialized geochemical calculations including mineral equilibria, redox reactions, and environmental applications.

Sub-modules

pychnosz.geochemistry.minerals

Mineral equilibria and phase diagram calculations for CHNOSZ …

pychnosz.geochemistry.redox

Redox reactions and Eh-pH diagram calculations for CHNOSZ …

Functions

def eh(pe_value: float, T: float = 298.15) ‑> float
Expand source code
def eh(pe_value: float, T: float = 298.15) -> float:
    """
    Convert pe to Eh.
    
    Parameters
    ----------
    pe_value : float
        Electron activity
    T : float, default 298.15
        Temperature in Kelvin
        
    Returns
    -------
    float
        Eh in Volts
    """
    
    return _redox_calculator.eh_from_pe(pe_value, T)

Convert pe to Eh.

Parameters

pe_value : float
Electron activity
T : float, default 298.15
Temperature in Kelvin

Returns

float
Eh in Volts
def eh_ph(element: str, **kwargs) ‑> Dict[str, Any]
Expand source code
def eh_ph(element: str, **kwargs) -> Dict[str, Any]:
    """
    Create Eh-pH diagram for an element.
    
    Parameters
    ----------
    element : str
        Element symbol
    **kwargs
        Additional parameters for eh_ph_diagram()
        
    Returns
    -------
    dict
        Eh-pH diagram data
    """
    
    return _redox_calculator.eh_ph_diagram(element, **kwargs)

Create Eh-pH diagram for an element.

Parameters

element : str
Element symbol
**kwargs
Additional parameters for eh_ph_diagram()

Returns

dict
Eh-pH diagram data
def logfO2(pe_value: float, pH: float = 7.0, **kwargs) ‑> float
Expand source code
def logfO2(pe_value: float, pH: float = 7.0, **kwargs) -> float:
    """
    Calculate log oxygen fugacity.
    
    Parameters
    ----------
    pe_value : float
        Electron activity
    pH : float, default 7.0
        Solution pH
    **kwargs
        Additional parameters
        
    Returns
    -------
    float
        log fO2
    """
    
    return _redox_calculator.oxygen_fugacity(pe_value, pH, **kwargs)

Calculate log oxygen fugacity.

Parameters

pe_value : float
Electron activity
pH : float, default 7.0
Solution pH
**kwargs
Additional parameters

Returns

float
log fO2
def mineral_solubility(mineral: str,
pH_range: Tuple[float, float] = (0, 14),
T: float = 298.15,
P: float = 1.0) ‑> Dict[str, Any]
Expand source code
def mineral_solubility(mineral: str, pH_range: Tuple[float, float] = (0, 14),
                      T: float = 298.15, P: float = 1.0) -> Dict[str, Any]:
    """
    Calculate mineral solubility vs pH.
    
    Parameters
    ----------
    mineral : str
        Mineral name
    pH_range : tuple, default (0, 14)
        pH range for calculation
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
        
    Returns
    -------
    dict
        Solubility data
    """
    
    return _mineral_calculator.mineral_solubility(mineral, T, P, pH_range)

Calculate mineral solubility vs pH.

Parameters

mineral : str
Mineral name
pH_range : tuple, default (0, 14)
pH range for calculation
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar

Returns

dict
Solubility data
def pe(redox_couple: str | Dict[str, float],
concentrations: Dict[str, float],
**kwargs) ‑> float
Expand source code
def pe(redox_couple: Union[str, Dict[str, float]], 
      concentrations: Dict[str, float], **kwargs) -> float:
    """
    Calculate pe for a redox couple.
    
    Parameters
    ----------
    redox_couple : str or dict
        Redox couple or reaction
    concentrations : dict
        Species concentrations
    **kwargs
        Additional parameters
        
    Returns
    -------
    float
        pe value
    """
    
    return _redox_calculator.pe_calculation(redox_couple, concentrations, **kwargs)

Calculate pe for a redox couple.

Parameters

redox_couple : str or dict
Redox couple or reaction
concentrations : dict
Species concentrations
**kwargs
Additional parameters

Returns

float
pe value
def phase_boundary(reaction: Dict[str, float], T_range: Tuple[float, float] = (273.15, 673.15)) ‑> Dict[str, Any]
Expand source code
def phase_boundary(reaction: Dict[str, float],
                  T_range: Tuple[float, float] = (273.15, 673.15)) -> Dict[str, Any]:
    """
    Calculate phase boundary for a reaction.
    
    Parameters
    ----------
    reaction : dict
        Reaction stoichiometry {species: coefficient}
    T_range : tuple, default (273.15, 673.15)
        Temperature range in Kelvin
        
    Returns
    -------
    dict
        Phase boundary data
    """
    
    return _mineral_calculator.phase_equilibrium(reaction, T_range=T_range)

Calculate phase boundary for a reaction.

Parameters

reaction : dict
Reaction stoichiometry {species: coefficient}
T_range : tuple, default (273.15, 673.15)
Temperature range in Kelvin

Returns

dict
Phase boundary data
def stability_field(minerals: List[str],
variables: Tuple[str, str],
ranges: Tuple[Tuple[float, float], Tuple[float, float]],
T: float = 298.15,
P: float = 1.0) ‑> Dict[str, Any]
Expand source code
def stability_field(minerals: List[str], 
                   variables: Tuple[str, str],
                   ranges: Tuple[Tuple[float, float], Tuple[float, float]],
                   T: float = 298.15, P: float = 1.0) -> Dict[str, Any]:
    """
    Calculate mineral stability fields.
    
    Parameters
    ----------
    minerals : list of str
        Minerals to consider
    variables : tuple of str
        (x_variable, y_variable) for diagram
    ranges : tuple of tuples
        ((x_min, x_max), (y_min, y_max))
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
        
    Returns
    -------
    dict
        Stability diagram data
    """
    
    x_var, y_var = variables
    x_range, y_range = ranges
    
    return _mineral_calculator.stability_diagram(
        minerals, x_var, x_range, y_var, y_range, T, P)

Calculate mineral stability fields.

Parameters

minerals : list of str
Minerals to consider
variables : tuple of str
(x_variable, y_variable) for diagram
ranges : tuple of tuples
((x_min, x_max), (y_min, y_max))
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar

Returns

dict
Stability diagram data

Classes

class MineralEquilibria
Expand source code
class MineralEquilibria:
    """
    Mineral equilibria calculator for geological systems.
    
    This class handles mineral stability calculations, including:
    - Mineral solubility equilibria
    - Phase stability diagrams
    - Mineral-water reactions
    - Solid solution equilibria
    """
    
    def __init__(self):
        """Initialize the mineral equilibria calculator."""
        self.equilibrium_solver = EquilibriumSolver()
        self.speciation_engine = SpeciationEngine()
        
        # Common mineral dissolution reactions (simplified)
        self.dissolution_reactions = {
            'quartz': {'SiO2': 1, 'H2O': 0, 'H4SiO4': -1},
            'calcite': {'CaCO3': 1, 'H+': 2, 'Ca+2': -1, 'HCO3-': -1, 'H2O': -1},
            'pyrite': {'FeS2': 1, 'H+': 8, 'SO4-2': -2, 'Fe+2': -1, 'H2O': -4},
            'hematite': {'Fe2O3': 1, 'H+': 6, 'Fe+3': -2, 'H2O': -3},
            'magnetite': {'Fe3O4': 1, 'H+': 8, 'Fe+2': -1, 'Fe+3': -2, 'H2O': -4},
            'albite': {'NaAlSi3O8': 1, 'H+': 1, 'H2O': 4, 'Na+': -1, 'Al+3': -1, 'H4SiO4': -3},
            'anorthite': {'CaAl2Si2O8': 1, 'H+': 8, 'H2O': 0, 'Ca+2': -1, 'Al+3': -2, 'H4SiO4': -2},
            'kaolinite': {'Al2Si2O5(OH)4': 1, 'H+': 6, 'Al+3': -2, 'H4SiO4': -2, 'H2O': -1}
        }
    
    def mineral_solubility(self, mineral: str, 
                          T: float = 298.15, P: float = 1.0,
                          pH_range: Optional[Tuple[float, float]] = None,
                          ionic_strength: float = 0.0) -> Dict[str, Any]:
        """
        Calculate mineral solubility as a function of pH.
        
        Parameters
        ----------
        mineral : str
            Mineral name
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
        pH_range : tuple, optional
            pH range (min, max). Default: (0, 14)
        ionic_strength : float, default 0.0
            Solution ionic strength
            
        Returns
        -------
        dict
            Solubility data vs pH
        """
        
        if pH_range is None:
            pH_range = (0, 14)
        
        pH_values = np.linspace(pH_range[0], pH_range[1], 100)
        
        if mineral.lower() not in self.dissolution_reactions:
            warnings.warn(f"Dissolution reaction not defined for {mineral}")
            return {'pH': pH_values, 'solubility': np.zeros_like(pH_values)}
        
        reaction = self.dissolution_reactions[mineral.lower()]
        
        try:
            # Get equilibrium constant for dissolution
            species_names = list(reaction.keys())
            coefficients = list(reaction.values())
            
            # Calculate logK at T, P
            result = subcrt(species_names, coefficients, T=T, P=P, show=False)
            if result.out is not None and 'logK' in result.out.columns:
                logK = result.out['logK'].iloc[0]
            else:
                logK = 0.0
                warnings.warn(f"Could not calculate logK for {mineral} dissolution")
            
        except Exception as e:
            warnings.warn(f"Error calculating equilibrium constant: {e}")
            logK = 0.0
        
        # Calculate solubility at each pH
        solubility = np.zeros_like(pH_values)
        
        for i, pH in enumerate(pH_values):
            try:
                # Simplified solubility calculation
                # This would be more complex in a full implementation
                
                if 'H+' in reaction:
                    h_coeff = reaction['H+']
                    # Basic pH dependence
                    log_solubility = logK - h_coeff * pH
                else:
                    log_solubility = logK
                
                # Convert to molality
                solubility[i] = 10**log_solubility
                
                # Apply ionic strength corrections if needed
                if ionic_strength > 0:
                    # Simple activity coefficient correction
                    gamma = self._estimate_activity_coefficient(ionic_strength)
                    solubility[i] *= gamma
                
            except:
                solubility[i] = 1e-20  # Very low solubility
        
        return {
            'pH': pH_values,
            'solubility': solubility,
            'logK': logK,
            'mineral': mineral,
            'T': T,
            'P': P
        }
    
    def stability_diagram(self, minerals: List[str],
                         x_axis: str, x_range: Tuple[float, float],
                         y_axis: str, y_range: Tuple[float, float],
                         T: float = 298.15, P: float = 1.0,
                         resolution: int = 50) -> Dict[str, Any]:
        """
        Create mineral stability diagram.
        
        Parameters
        ----------
        minerals : list of str
            Mineral names to consider
        x_axis : str
            X-axis variable ('pH', 'log_activity_SiO2', etc.)
        x_range : tuple
            X-axis range (min, max)
        y_axis : str
            Y-axis variable
        y_range : tuple
            Y-axis range (min, max)
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
        resolution : int, default 50
            Grid resolution
            
        Returns
        -------
        dict
            Stability diagram data
        """
        
        x_values = np.linspace(x_range[0], x_range[1], resolution)
        y_values = np.linspace(y_range[0], y_range[1], resolution)
        X, Y = np.meshgrid(x_values, y_values)
        
        # Initialize stability field array
        stable_mineral = np.zeros_like(X, dtype=int)
        
        # Calculate stability at each grid point
        for i in range(len(y_values)):
            for j in range(len(x_values)):
                x_val, y_val = X[i, j], Y[i, j]
                
                # Find most stable mineral at this point
                min_energy = np.inf
                stable_idx = 0
                
                for k, mineral in enumerate(minerals):
                    try:
                        # Calculate relative stability
                        # This would involve full equilibrium calculations
                        # For now, use simplified energy estimates
                        
                        energy = self._calculate_stability_energy(
                            mineral, x_axis, x_val, y_axis, y_val, T, P)
                        
                        if energy < min_energy:
                            min_energy = energy
                            stable_idx = k
                        
                    except Exception as e:
                        warnings.warn(f"Error calculating stability for {mineral}: {e}")
                        continue
                
                stable_mineral[i, j] = stable_idx
        
        return {
            x_axis: x_values,
            y_axis: y_values,
            'stable_mineral': stable_mineral,
            'mineral_names': minerals,
            'T': T,
            'P': P
        }
    
    def phase_equilibrium(self, reaction: Dict[str, float],
                         T_range: Optional[Tuple[float, float]] = None,
                         P_range: Optional[Tuple[float, float]] = None) -> Dict[str, Any]:
        """
        Calculate phase equilibrium curve.
        
        Parameters
        ----------
        reaction : dict
            Reaction stoichiometry {species: coefficient}
        T_range : tuple, optional
            Temperature range (min, max) in K
        P_range : tuple, optional
            Pressure range (min, max) in bar
            
        Returns
        -------
        dict
            Equilibrium curve data
        """
        
        if T_range is None and P_range is None:
            T_range = (273.15, 673.15)  # 0-400°C
        
        if T_range is not None:
            # T-P curve at equilibrium
            T_values = np.linspace(T_range[0], T_range[1], 50)
            P_equilibrium = np.zeros_like(T_values)
            
            for i, T in enumerate(T_values):
                try:
                    # Calculate equilibrium pressure
                    # This would involve iterative solution
                    # For now, use simplified correlation
                    P_equilibrium[i] = self._calculate_equilibrium_pressure(reaction, T)
                    
                except:
                    P_equilibrium[i] = 1.0  # Default
            
            return {
                'T': T_values,
                'P': P_equilibrium,
                'reaction': reaction
            }
        
        else:
            # P-T curve - similar logic
            P_values = np.linspace(P_range[0], P_range[1], 50)
            T_equilibrium = np.zeros_like(P_values)
            
            for i, P in enumerate(P_values):
                try:
                    T_equilibrium[i] = self._calculate_equilibrium_temperature(reaction, P)
                except:
                    T_equilibrium[i] = 298.15
            
            return {
                'P': P_values,
                'T': T_equilibrium,
                'reaction': reaction
            }
    
    def solid_solution(self, end_members: List[str], compositions: List[float],
                      T: float = 298.15, P: float = 1.0,
                      model: str = 'ideal') -> Dict[str, Any]:
        """
        Calculate solid solution thermodynamics.
        
        Parameters
        ----------
        end_members : list of str
            End-member mineral names
        compositions : list of float
            Mole fractions of end-members (must sum to 1)
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
        model : str, default 'ideal'
            Mixing model ('ideal', 'regular', 'margules')
            
        Returns
        -------
        dict
            Solid solution properties
        """
        
        if len(end_members) != len(compositions):
            raise ValueError("Number of end-members must match number of compositions")
        
        if abs(sum(compositions) - 1.0) > 1e-6:
            raise ValueError("Compositions must sum to 1.0")
        
        # Calculate end-member properties
        end_member_props = []
        for mineral in end_members:
            try:
                # This would get mineral properties from database
                props = self._get_mineral_properties(mineral, T, P)
                end_member_props.append(props)
            except:
                # Default properties
                props = {'G': -1000000, 'H': -1200000, 'S': 100, 'V': 50, 'Cp': 100}
                end_member_props.append(props)
        
        # Calculate mixing properties
        if model == 'ideal':
            # Ideal mixing
            G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
            H_mix = 0.0
            S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
            
        elif model == 'regular':
            # Regular solution (symmetric)
            W = 10000  # Interaction parameter (J/mol) - would be mineral-specific
            G_mix = W * compositions[0] * compositions[1]  # Binary case
            H_mix = G_mix
            S_mix = 0.0
            
        else:
            # Default to ideal
            G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
            H_mix = 0.0
            S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
        
        # Total properties
        total_props = {}
        for prop in ['G', 'H', 'S', 'V', 'Cp']:
            total_props[prop] = sum(compositions[i] * end_member_props[i][prop] 
                                  for i in range(len(end_members)))
        
        # Add mixing contributions
        total_props['G'] += G_mix
        total_props['H'] += H_mix
        total_props['S'] += S_mix
        
        return {
            'properties': total_props,
            'mixing': {'G': G_mix, 'H': H_mix, 'S': S_mix},
            'end_members': end_members,
            'compositions': compositions,
            'model': model
        }
    
    def _estimate_activity_coefficient(self, ionic_strength: float) -> float:
        """Estimate activity coefficient from ionic strength."""
        if ionic_strength <= 0:
            return 1.0
        else:
            # Simple Debye-Hückel approximation
            A = 0.509
            return 10**(-A * np.sqrt(ionic_strength))
    
    def _calculate_stability_energy(self, mineral: str, x_axis: str, x_val: float,
                                  y_axis: str, y_val: float, T: float, P: float) -> float:
        """Calculate relative stability energy for mineral."""
        
        # Simplified stability calculation
        # Would involve full thermodynamic analysis
        
        base_energy = np.random.random() * 1000  # Placeholder
        
        # Add dependencies on axes
        if 'pH' in x_axis:
            base_energy += abs(x_val - 7) * 100  # pH dependence
        
        if 'log' in y_axis:
            base_energy += abs(y_val + 3) * 50  # Activity dependence
        
        return base_energy
    
    def _calculate_equilibrium_pressure(self, reaction: Dict[str, float], T: float) -> float:
        """Calculate equilibrium pressure for reaction at given T."""
        
        # Simplified using Clapeyron equation approximation
        # dP/dT = ΔS/ΔV
        
        # Estimate volume and entropy changes
        delta_V = 5.0e-6  # m³/mol (typical for mineral reactions)
        delta_S = 20.0    # J/(mol·K) (typical)
        
        # Integrate from reference conditions
        T0, P0 = 298.15, 1.0
        dP_dT = delta_S / delta_V * 1e-5  # Convert to bar/K
        
        P_eq = P0 + dP_dT * (T - T0)
        
        return max(P_eq, 0.1)  # Minimum 0.1 bar
    
    def _calculate_equilibrium_temperature(self, reaction: Dict[str, float], P: float) -> float:
        """Calculate equilibrium temperature for reaction at given P."""
        
        # Inverse of pressure calculation
        delta_V = 5.0e-6
        delta_S = 20.0
        
        T0, P0 = 298.15, 1.0
        dT_dP = delta_V / delta_S * 1e5  # K/bar
        
        T_eq = T0 + dT_dP * (P - P0)
        
        return max(T_eq, 273.15)  # Minimum 0°C
    
    def _get_mineral_properties(self, mineral: str, T: float, P: float) -> Dict[str, float]:
        """Get thermodynamic properties of a mineral."""
        
        # This would look up mineral in database
        # For now, return typical values
        
        typical_props = {
            'quartz': {'G': -856300, 'H': -910700, 'S': 41.5, 'V': 22.7, 'Cp': 44.6},
            'calcite': {'G': -1128800, 'H': -1207400, 'S': 91.7, 'V': 36.9, 'Cp': 83.5},
            'pyrite': {'G': -160200, 'H': -171500, 'S': 52.9, 'V': 23.9, 'Cp': 62.2},
            'hematite': {'G': -744400, 'H': -824200, 'S': 87.4, 'V': 30.3, 'Cp': 103.9}
        }
        
        return typical_props.get(mineral.lower(), {
            'G': -500000, 'H': -600000, 'S': 50, 'V': 30, 'Cp': 80
        })

Mineral equilibria calculator for geological systems.

This class handles mineral stability calculations, including: - Mineral solubility equilibria - Phase stability diagrams - Mineral-water reactions - Solid solution equilibria

Initialize the mineral equilibria calculator.

Methods

def mineral_solubility(self,
mineral: str,
T: float = 298.15,
P: float = 1.0,
pH_range: Tuple[float, float] | None = None,
ionic_strength: float = 0.0) ‑> Dict[str, Any]
Expand source code
def mineral_solubility(self, mineral: str, 
                      T: float = 298.15, P: float = 1.0,
                      pH_range: Optional[Tuple[float, float]] = None,
                      ionic_strength: float = 0.0) -> Dict[str, Any]:
    """
    Calculate mineral solubility as a function of pH.
    
    Parameters
    ----------
    mineral : str
        Mineral name
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
    pH_range : tuple, optional
        pH range (min, max). Default: (0, 14)
    ionic_strength : float, default 0.0
        Solution ionic strength
        
    Returns
    -------
    dict
        Solubility data vs pH
    """
    
    if pH_range is None:
        pH_range = (0, 14)
    
    pH_values = np.linspace(pH_range[0], pH_range[1], 100)
    
    if mineral.lower() not in self.dissolution_reactions:
        warnings.warn(f"Dissolution reaction not defined for {mineral}")
        return {'pH': pH_values, 'solubility': np.zeros_like(pH_values)}
    
    reaction = self.dissolution_reactions[mineral.lower()]
    
    try:
        # Get equilibrium constant for dissolution
        species_names = list(reaction.keys())
        coefficients = list(reaction.values())
        
        # Calculate logK at T, P
        result = subcrt(species_names, coefficients, T=T, P=P, show=False)
        if result.out is not None and 'logK' in result.out.columns:
            logK = result.out['logK'].iloc[0]
        else:
            logK = 0.0
            warnings.warn(f"Could not calculate logK for {mineral} dissolution")
        
    except Exception as e:
        warnings.warn(f"Error calculating equilibrium constant: {e}")
        logK = 0.0
    
    # Calculate solubility at each pH
    solubility = np.zeros_like(pH_values)
    
    for i, pH in enumerate(pH_values):
        try:
            # Simplified solubility calculation
            # This would be more complex in a full implementation
            
            if 'H+' in reaction:
                h_coeff = reaction['H+']
                # Basic pH dependence
                log_solubility = logK - h_coeff * pH
            else:
                log_solubility = logK
            
            # Convert to molality
            solubility[i] = 10**log_solubility
            
            # Apply ionic strength corrections if needed
            if ionic_strength > 0:
                # Simple activity coefficient correction
                gamma = self._estimate_activity_coefficient(ionic_strength)
                solubility[i] *= gamma
            
        except:
            solubility[i] = 1e-20  # Very low solubility
    
    return {
        'pH': pH_values,
        'solubility': solubility,
        'logK': logK,
        'mineral': mineral,
        'T': T,
        'P': P
    }

Calculate mineral solubility as a function of pH.

Parameters

mineral : str
Mineral name
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar
pH_range : tuple, optional
pH range (min, max). Default: (0, 14)
ionic_strength : float, default 0.0
Solution ionic strength

Returns

dict
Solubility data vs pH
def phase_equilibrium(self,
reaction: Dict[str, float],
T_range: Tuple[float, float] | None = None,
P_range: Tuple[float, float] | None = None) ‑> Dict[str, Any]
Expand source code
def phase_equilibrium(self, reaction: Dict[str, float],
                     T_range: Optional[Tuple[float, float]] = None,
                     P_range: Optional[Tuple[float, float]] = None) -> Dict[str, Any]:
    """
    Calculate phase equilibrium curve.
    
    Parameters
    ----------
    reaction : dict
        Reaction stoichiometry {species: coefficient}
    T_range : tuple, optional
        Temperature range (min, max) in K
    P_range : tuple, optional
        Pressure range (min, max) in bar
        
    Returns
    -------
    dict
        Equilibrium curve data
    """
    
    if T_range is None and P_range is None:
        T_range = (273.15, 673.15)  # 0-400°C
    
    if T_range is not None:
        # T-P curve at equilibrium
        T_values = np.linspace(T_range[0], T_range[1], 50)
        P_equilibrium = np.zeros_like(T_values)
        
        for i, T in enumerate(T_values):
            try:
                # Calculate equilibrium pressure
                # This would involve iterative solution
                # For now, use simplified correlation
                P_equilibrium[i] = self._calculate_equilibrium_pressure(reaction, T)
                
            except:
                P_equilibrium[i] = 1.0  # Default
        
        return {
            'T': T_values,
            'P': P_equilibrium,
            'reaction': reaction
        }
    
    else:
        # P-T curve - similar logic
        P_values = np.linspace(P_range[0], P_range[1], 50)
        T_equilibrium = np.zeros_like(P_values)
        
        for i, P in enumerate(P_values):
            try:
                T_equilibrium[i] = self._calculate_equilibrium_temperature(reaction, P)
            except:
                T_equilibrium[i] = 298.15
        
        return {
            'P': P_values,
            'T': T_equilibrium,
            'reaction': reaction
        }

Calculate phase equilibrium curve.

Parameters

reaction : dict
Reaction stoichiometry {species: coefficient}
T_range : tuple, optional
Temperature range (min, max) in K
P_range : tuple, optional
Pressure range (min, max) in bar

Returns

dict
Equilibrium curve data
def solid_solution(self,
end_members: List[str],
compositions: List[float],
T: float = 298.15,
P: float = 1.0,
model: str = 'ideal') ‑> Dict[str, Any]
Expand source code
def solid_solution(self, end_members: List[str], compositions: List[float],
                  T: float = 298.15, P: float = 1.0,
                  model: str = 'ideal') -> Dict[str, Any]:
    """
    Calculate solid solution thermodynamics.
    
    Parameters
    ----------
    end_members : list of str
        End-member mineral names
    compositions : list of float
        Mole fractions of end-members (must sum to 1)
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
    model : str, default 'ideal'
        Mixing model ('ideal', 'regular', 'margules')
        
    Returns
    -------
    dict
        Solid solution properties
    """
    
    if len(end_members) != len(compositions):
        raise ValueError("Number of end-members must match number of compositions")
    
    if abs(sum(compositions) - 1.0) > 1e-6:
        raise ValueError("Compositions must sum to 1.0")
    
    # Calculate end-member properties
    end_member_props = []
    for mineral in end_members:
        try:
            # This would get mineral properties from database
            props = self._get_mineral_properties(mineral, T, P)
            end_member_props.append(props)
        except:
            # Default properties
            props = {'G': -1000000, 'H': -1200000, 'S': 100, 'V': 50, 'Cp': 100}
            end_member_props.append(props)
    
    # Calculate mixing properties
    if model == 'ideal':
        # Ideal mixing
        G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
        H_mix = 0.0
        S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
        
    elif model == 'regular':
        # Regular solution (symmetric)
        W = 10000  # Interaction parameter (J/mol) - would be mineral-specific
        G_mix = W * compositions[0] * compositions[1]  # Binary case
        H_mix = G_mix
        S_mix = 0.0
        
    else:
        # Default to ideal
        G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
        H_mix = 0.0
        S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
    
    # Total properties
    total_props = {}
    for prop in ['G', 'H', 'S', 'V', 'Cp']:
        total_props[prop] = sum(compositions[i] * end_member_props[i][prop] 
                              for i in range(len(end_members)))
    
    # Add mixing contributions
    total_props['G'] += G_mix
    total_props['H'] += H_mix
    total_props['S'] += S_mix
    
    return {
        'properties': total_props,
        'mixing': {'G': G_mix, 'H': H_mix, 'S': S_mix},
        'end_members': end_members,
        'compositions': compositions,
        'model': model
    }

Calculate solid solution thermodynamics.

Parameters

end_members : list of str
End-member mineral names
compositions : list of float
Mole fractions of end-members (must sum to 1)
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar
model : str, default 'ideal'
Mixing model ('ideal', 'regular', 'margules')

Returns

dict
Solid solution properties
def stability_diagram(self,
minerals: List[str],
x_axis: str,
x_range: Tuple[float, float],
y_axis: str,
y_range: Tuple[float, float],
T: float = 298.15,
P: float = 1.0,
resolution: int = 50) ‑> Dict[str, Any]
Expand source code
def stability_diagram(self, minerals: List[str],
                     x_axis: str, x_range: Tuple[float, float],
                     y_axis: str, y_range: Tuple[float, float],
                     T: float = 298.15, P: float = 1.0,
                     resolution: int = 50) -> Dict[str, Any]:
    """
    Create mineral stability diagram.
    
    Parameters
    ----------
    minerals : list of str
        Mineral names to consider
    x_axis : str
        X-axis variable ('pH', 'log_activity_SiO2', etc.)
    x_range : tuple
        X-axis range (min, max)
    y_axis : str
        Y-axis variable
    y_range : tuple
        Y-axis range (min, max)
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
    resolution : int, default 50
        Grid resolution
        
    Returns
    -------
    dict
        Stability diagram data
    """
    
    x_values = np.linspace(x_range[0], x_range[1], resolution)
    y_values = np.linspace(y_range[0], y_range[1], resolution)
    X, Y = np.meshgrid(x_values, y_values)
    
    # Initialize stability field array
    stable_mineral = np.zeros_like(X, dtype=int)
    
    # Calculate stability at each grid point
    for i in range(len(y_values)):
        for j in range(len(x_values)):
            x_val, y_val = X[i, j], Y[i, j]
            
            # Find most stable mineral at this point
            min_energy = np.inf
            stable_idx = 0
            
            for k, mineral in enumerate(minerals):
                try:
                    # Calculate relative stability
                    # This would involve full equilibrium calculations
                    # For now, use simplified energy estimates
                    
                    energy = self._calculate_stability_energy(
                        mineral, x_axis, x_val, y_axis, y_val, T, P)
                    
                    if energy < min_energy:
                        min_energy = energy
                        stable_idx = k
                    
                except Exception as e:
                    warnings.warn(f"Error calculating stability for {mineral}: {e}")
                    continue
            
            stable_mineral[i, j] = stable_idx
    
    return {
        x_axis: x_values,
        y_axis: y_values,
        'stable_mineral': stable_mineral,
        'mineral_names': minerals,
        'T': T,
        'P': P
    }

Create mineral stability diagram.

Parameters

minerals : list of str
Mineral names to consider
x_axis : str
X-axis variable ('pH', 'log_activity_SiO2', etc.)
x_range : tuple
X-axis range (min, max)
y_axis : str
Y-axis variable
y_range : tuple
Y-axis range (min, max)
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar
resolution : int, default 50
Grid resolution

Returns

dict
Stability diagram data
class RedoxCalculator
Expand source code
class RedoxCalculator:
    """
    Redox equilibria calculator for geochemical systems.
    
    This class handles redox reactions, pe-pH diagrams, and
    electron activity calculations.
    """
    
    def __init__(self):
        """Initialize the redox calculator."""
        self.equilibrium_solver = EquilibriumSolver()
        
        # Standard electrode potentials (V) at 25°C
        self.standard_potentials = {
            'O2/H2O': 1.229,
            'H+/H2': 0.000,
            'Fe+3/Fe+2': 0.771,
            'NO3-/NO2-': 0.835,
            'SO4-2/HS-': -0.217,
            'CO2/CH4': -0.244,
            'N2/NH4+': -0.277,
            'Fe+2/Fe': -0.447,
            'S/HS-': -0.065,
        }
        
        # Common redox couples and their reactions
        self.redox_reactions = {
            # Oxygen reduction
            'O2_H2O': {'O2': 1, 'H+': 4, 'e-': 4, 'H2O': -2},
            'H2O_H2': {'H2O': 2, 'e-': 2, 'H2': -1, 'OH-': -2},
            
            # Iron redox
            'Fe3_Fe2': {'Fe+3': 1, 'e-': 1, 'Fe+2': -1},
            'Fe2_Fe': {'Fe+2': 1, 'e-': 2, 'Fe': -1},
            'Fe2O3_Fe2': {'Fe2O3': 1, 'H+': 6, 'e-': 2, 'Fe+2': -2, 'H2O': -3},
            
            # Nitrogen redox
            'NO3_NO2': {'NO3-': 1, 'H+': 2, 'e-': 2, 'NO2-': -1, 'H2O': -1},
            'NO2_NH4': {'NO2-': 1, 'H+': 8, 'e-': 6, 'NH4+': -1, 'H2O': -2},
            
            # Sulfur redox
            'SO4_HS': {'SO4-2': 1, 'H+': 9, 'e-': 8, 'HS-': -1, 'H2O': -4},
            'S_HS': {'S': 1, 'H+': 1, 'e-': 2, 'HS-': -1},
            
            # Carbon redox
            'CO2_CH4': {'CO2': 1, 'H+': 8, 'e-': 8, 'CH4': -1, 'H2O': -2},
            'HCO3_CH4': {'HCO3-': 1, 'H+': 9, 'e-': 8, 'CH4': -1, 'H2O': -3}
        }
    
    def eh_ph_diagram(self, element: str, 
                     pH_range: Tuple[float, float] = (0, 14),
                     pe_range: Tuple[float, float] = (-10, 15),
                     T: float = 298.15, P: float = 1.0,
                     total_concentration: float = 1e-6,
                     resolution: int = 100) -> Dict[str, Any]:
        """
        Create Eh-pH (pe-pH) diagram for an element.
        
        Parameters
        ----------
        element : str
            Element symbol (e.g., 'Fe', 'S', 'N', 'C')
        pH_range : tuple, default (0, 14)
            pH range for diagram
        pe_range : tuple, default (-10, 15)
            pe (electron activity) range
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
        total_concentration : float, default 1e-6
            Total element concentration (molal)
        resolution : int, default 100
            Grid resolution
            
        Returns
        -------
        dict
            Eh-pH diagram data
        """
        
        pH_grid = np.linspace(pH_range[0], pH_range[1], resolution)
        pe_grid = np.linspace(pe_range[0], pe_range[1], resolution)
        pH_mesh, pe_mesh = np.meshgrid(pH_grid, pe_grid)
        
        # Get species for this element
        element_species = self._get_element_species(element)
        
        if not element_species:
            raise ValueError(f"No species found for element {element}")
        
        # Calculate predominance at each point
        predominant = np.zeros_like(pH_mesh, dtype=int)
        activities = {species: np.zeros_like(pH_mesh) for species in element_species}
        
        for i in range(len(pe_grid)):
            for j in range(len(pH_grid)):
                pH, pe = pH_mesh[i, j], pe_mesh[i, j]
                
                # Calculate speciation at this point
                spec_result = self._calculate_redox_speciation(
                    element_species, pH, pe, T, P, total_concentration)
                
                # Find predominant species
                max_activity = -np.inf
                max_idx = 0
                
                for k, species in enumerate(element_species):
                    activity = spec_result.get(species, 1e-20)
                    activities[species][i, j] = activity
                    
                    if np.log10(activity) > max_activity:
                        max_activity = np.log10(activity)
                        max_idx = k
                
                predominant[i, j] = max_idx
        
        # Add water stability limits
        water_limits = self._water_stability_lines(pH_grid, T, P)
        
        return {
            'pH': pH_grid,
            'pe': pe_grid,
            'pH_mesh': pH_mesh,
            'pe_mesh': pe_mesh,
            'predominant': predominant,
            'activities': activities,
            'species_names': element_species,
            'water_limits': water_limits,
            'element': element,
            'T': T,
            'P': P,
            'total_concentration': total_concentration
        }
    
    def pe_calculation(self, redox_couple: Union[str, Dict[str, float]],
                      concentrations: Dict[str, float],
                      pH: float = 7.0, T: float = 298.15, P: float = 1.0) -> float:
        """
        Calculate pe (electron activity) for a redox couple.
        
        Parameters
        ----------
        redox_couple : str or dict
            Redox couple name or reaction dictionary
        concentrations : dict
            Species concentrations {species: concentration}
        pH : float, default 7.0
            Solution pH
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
            
        Returns
        -------
        float
            pe value
        """
        
        if isinstance(redox_couple, str):
            if redox_couple not in self.redox_reactions:
                raise ValueError(f"Unknown redox couple: {redox_couple}")
            reaction = self.redox_reactions[redox_couple]
        else:
            reaction = redox_couple
        
        # Calculate equilibrium constant
        try:
            species_names = [sp for sp in reaction.keys() if sp != 'e-']
            coefficients = [reaction[sp] for sp in species_names]
            
            result = subcrt(species_names, coefficients, T=T, P=P, show=False)
            if result.out is not None and 'logK' in result.out.columns:
                logK = result.out['logK'].iloc[0]
            else:
                logK = 0.0
        except Exception as e:
            warnings.warn(f"Could not calculate logK: {e}")
            logK = 0.0
        
        # Apply Nernst equation
        n_electrons = abs(reaction.get('e-', 1))  # Number of electrons
        
        # Calculate activity quotient
        log_Q = 0.0
        for species, coeff in reaction.items():
            if species == 'e-':
                continue
            elif species == 'H+':
                activity = 10**(-pH)
            elif species in concentrations:
                activity = concentrations[species]
                # Apply activity coefficients if needed
                gamma = self._get_activity_coefficient(species, concentrations, T)
                activity *= gamma
            else:
                activity = 1.0  # Default for species not specified
            
            if activity > 0:
                log_Q += coeff * np.log10(activity)
        
        # Nernst equation: pe = pe° + (1/n) * log(Q)
        # where pe° = logK/n for the half-reaction
        pe = logK / n_electrons + log_Q / n_electrons
        
        return pe
    
    def eh_from_pe(self, pe: float, T: float = 298.15) -> float:
        """
        Convert pe to Eh (redox potential).
        
        Parameters
        ----------
        pe : float
            Electron activity (pe)
        T : float, default 298.15
            Temperature in Kelvin
            
        Returns
        -------
        float
            Redox potential (Eh) in Volts
        """
        
        # Eh = (RT/F) * ln(10) * pe
        # where R = 8.314 J/(mol·K), F = 96485 C/mol
        RT_F = 8.314 * T / 96485
        Eh = RT_F * 2.302585 * pe  # ln(10) = 2.302585
        
        return Eh
    
    def pe_from_eh(self, eh: float, T: float = 298.15) -> float:
        """
        Convert Eh (redox potential) to pe.
        
        Parameters
        ----------
        eh : float
            Redox potential (Eh) in Volts
        T : float, default 298.15
            Temperature in Kelvin
            
        Returns
        -------
        float
            Electron activity (pe)
        """
        
        # pe = F * Eh / (RT * ln(10))
        RT_F = 8.314 * T / 96485
        pe = eh / (RT_F * 2.302585)
        
        return pe
    
    def oxygen_fugacity(self, pe: float, pH: float = 7.0, 
                       T: float = 298.15, P: float = 1.0) -> float:
        """
        Calculate oxygen fugacity from pe and pH.
        
        Parameters
        ----------
        pe : float
            Electron activity
        pH : float, default 7.0
            Solution pH
        T : float, default 298.15
            Temperature in Kelvin
        P : float, default 1.0
            Pressure in bar
            
        Returns
        -------
        float
            log fO2 (log oxygen fugacity)
        """
        
        # O2 + 4H+ + 4e- = 2H2O
        # At equilibrium: log fO2 = 4*pe + 4*pH - logK
        
        try:
            # Calculate logK for oxygen-water reaction
            result = subcrt(['O2', 'H+', 'H2O'], [1, 4, -2], T=T, P=P, show=False)
            if result.out is not None and 'logK' in result.out.columns:
                logK = result.out['logK'].iloc[0]
            else:
                logK = 83.1  # Approximate value at 25°C
        except:
            logK = 83.1
        
        log_fO2 = 4 * pe + 4 * pH - logK
        
        return log_fO2
    
    def _get_element_species(self, element: str) -> List[str]:
        """Get list of species containing the specified element."""
        
        # Simplified species lists for common elements
        element_species = {
            'Fe': ['Fe+2', 'Fe+3', 'Fe2O3', 'FeOH+', 'Fe(OH)2', 'Fe(OH)3'],
            'S': ['SO4-2', 'SO3-2', 'S2O3-2', 'HS-', 'S0', 'S-2'],
            'N': ['NO3-', 'NO2-', 'NH4+', 'NH3', 'N2O', 'N2'],
            'C': ['CO2', 'HCO3-', 'CO3-2', 'CH4', 'HCOOH', 'CH3COO-'],
            'Mn': ['Mn+2', 'MnO4-', 'MnO2', 'Mn+3', 'MnOH+'],
            'As': ['AsO4-3', 'AsO3-3', 'H3AsO4', 'H3AsO3', 'As0']
        }
        
        return element_species.get(element, [f'{element}+2', f'{element}+3'])
    
    def _calculate_redox_speciation(self, species: List[str], pH: float, pe: float,
                                  T: float, P: float, total_conc: float) -> Dict[str, float]:
        """Calculate speciation at given pH and pe."""
        
        # Simplified speciation calculation
        # Full implementation would solve equilibrium system
        
        activities = {}
        
        for sp in species:
            # Simplified pe-pH dependence
            if '+' in sp:  # Cations (more stable at low pH, high pe)
                charge = self._extract_charge(sp)
                log_activity = -6 + charge * (14 - pH) / 14 + pe / 10
            elif '-' in sp:  # Anions (more stable at high pH, high pe)  
                charge = abs(self._extract_charge(sp))
                log_activity = -6 + charge * pH / 14 + pe / 10
            else:  # Neutral (less pH/pe dependence)
                log_activity = -6 + (pe - pH) / 20
            
            activities[sp] = 10**log_activity
        
        # Normalize to total concentration
        total_calculated = sum(activities.values())
        if total_calculated > 0:
            factor = total_conc / total_calculated
            activities = {sp: act * factor for sp, act in activities.items()}
        
        return activities
    
    def _extract_charge(self, species: str) -> int:
        """Extract charge from species name."""
        
        if '+' in species:
            parts = species.split('+')
            if len(parts) > 1 and parts[-1].isdigit():
                return int(parts[-1])
            else:
                return 1
        elif '-' in species:
            parts = species.split('-')
            if len(parts) > 1 and parts[-1].isdigit():
                return -int(parts[-1])
            else:
                return -1
        else:
            return 0
    
    def _water_stability_lines(self, pH_values: np.ndarray, 
                              T: float, P: float) -> Dict[str, np.ndarray]:
        """Calculate water stability limits (H2O/H2 and O2/H2O)."""
        
        # Upper limit: O2/H2O
        # O2 + 4H+ + 4e- = 2H2O
        # pe = 20.75 - pH (at 25°C, 1 atm O2)
        pe_upper = 20.75 - pH_values
        
        # Lower limit: H2O/H2
        # 2H2O + 2e- = H2 + 2OH-
        # pe = -pH (at 25°C, 1 atm H2)
        pe_lower = -pH_values
        
        # Temperature corrections (simplified)
        if T != 298.15:
            dT = T - 298.15
            pe_upper += dT * 0.001  # Small temperature dependence
            pe_lower -= dT * 0.001
        
        return {
            'upper': pe_upper,  # O2/H2O line
            'lower': pe_lower   # H2O/H2 line
        }
    
    def _get_activity_coefficient(self, species: str, composition: Dict[str, float],
                                 T: float) -> float:
        """Get activity coefficient for species."""
        
        # Simplified - would use proper activity models
        charge = abs(self._extract_charge(species))
        
        if charge == 0:
            return 1.0
        else:
            # Simple ionic strength correction
            I = 0.001  # Assume low ionic strength
            return 10**(-0.509 * charge**2 * np.sqrt(I))

Redox equilibria calculator for geochemical systems.

This class handles redox reactions, pe-pH diagrams, and electron activity calculations.

Initialize the redox calculator.

Methods

def eh_from_pe(self, pe: float, T: float = 298.15) ‑> float
Expand source code
def eh_from_pe(self, pe: float, T: float = 298.15) -> float:
    """
    Convert pe to Eh (redox potential).
    
    Parameters
    ----------
    pe : float
        Electron activity (pe)
    T : float, default 298.15
        Temperature in Kelvin
        
    Returns
    -------
    float
        Redox potential (Eh) in Volts
    """
    
    # Eh = (RT/F) * ln(10) * pe
    # where R = 8.314 J/(mol·K), F = 96485 C/mol
    RT_F = 8.314 * T / 96485
    Eh = RT_F * 2.302585 * pe  # ln(10) = 2.302585
    
    return Eh

Convert pe to Eh (redox potential).

Parameters

pe : float
Electron activity (pe)
T : float, default 298.15
Temperature in Kelvin

Returns

float
Redox potential (Eh) in Volts
def eh_ph_diagram(self,
element: str,
pH_range: Tuple[float, float] = (0, 14),
pe_range: Tuple[float, float] = (-10, 15),
T: float = 298.15,
P: float = 1.0,
total_concentration: float = 1e-06,
resolution: int = 100) ‑> Dict[str, Any]
Expand source code
def eh_ph_diagram(self, element: str, 
                 pH_range: Tuple[float, float] = (0, 14),
                 pe_range: Tuple[float, float] = (-10, 15),
                 T: float = 298.15, P: float = 1.0,
                 total_concentration: float = 1e-6,
                 resolution: int = 100) -> Dict[str, Any]:
    """
    Create Eh-pH (pe-pH) diagram for an element.
    
    Parameters
    ----------
    element : str
        Element symbol (e.g., 'Fe', 'S', 'N', 'C')
    pH_range : tuple, default (0, 14)
        pH range for diagram
    pe_range : tuple, default (-10, 15)
        pe (electron activity) range
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
    total_concentration : float, default 1e-6
        Total element concentration (molal)
    resolution : int, default 100
        Grid resolution
        
    Returns
    -------
    dict
        Eh-pH diagram data
    """
    
    pH_grid = np.linspace(pH_range[0], pH_range[1], resolution)
    pe_grid = np.linspace(pe_range[0], pe_range[1], resolution)
    pH_mesh, pe_mesh = np.meshgrid(pH_grid, pe_grid)
    
    # Get species for this element
    element_species = self._get_element_species(element)
    
    if not element_species:
        raise ValueError(f"No species found for element {element}")
    
    # Calculate predominance at each point
    predominant = np.zeros_like(pH_mesh, dtype=int)
    activities = {species: np.zeros_like(pH_mesh) for species in element_species}
    
    for i in range(len(pe_grid)):
        for j in range(len(pH_grid)):
            pH, pe = pH_mesh[i, j], pe_mesh[i, j]
            
            # Calculate speciation at this point
            spec_result = self._calculate_redox_speciation(
                element_species, pH, pe, T, P, total_concentration)
            
            # Find predominant species
            max_activity = -np.inf
            max_idx = 0
            
            for k, species in enumerate(element_species):
                activity = spec_result.get(species, 1e-20)
                activities[species][i, j] = activity
                
                if np.log10(activity) > max_activity:
                    max_activity = np.log10(activity)
                    max_idx = k
            
            predominant[i, j] = max_idx
    
    # Add water stability limits
    water_limits = self._water_stability_lines(pH_grid, T, P)
    
    return {
        'pH': pH_grid,
        'pe': pe_grid,
        'pH_mesh': pH_mesh,
        'pe_mesh': pe_mesh,
        'predominant': predominant,
        'activities': activities,
        'species_names': element_species,
        'water_limits': water_limits,
        'element': element,
        'T': T,
        'P': P,
        'total_concentration': total_concentration
    }

Create Eh-pH (pe-pH) diagram for an element.

Parameters

element : str
Element symbol (e.g., 'Fe', 'S', 'N', 'C')
pH_range : tuple, default (0, 14)
pH range for diagram
pe_range : tuple, default (-10, 15)
pe (electron activity) range
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar
total_concentration : float, default 1e-6
Total element concentration (molal)
resolution : int, default 100
Grid resolution

Returns

dict
Eh-pH diagram data
def oxygen_fugacity(self, pe: float, pH: float = 7.0, T: float = 298.15, P: float = 1.0) ‑> float
Expand source code
def oxygen_fugacity(self, pe: float, pH: float = 7.0, 
                   T: float = 298.15, P: float = 1.0) -> float:
    """
    Calculate oxygen fugacity from pe and pH.
    
    Parameters
    ----------
    pe : float
        Electron activity
    pH : float, default 7.0
        Solution pH
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
        
    Returns
    -------
    float
        log fO2 (log oxygen fugacity)
    """
    
    # O2 + 4H+ + 4e- = 2H2O
    # At equilibrium: log fO2 = 4*pe + 4*pH - logK
    
    try:
        # Calculate logK for oxygen-water reaction
        result = subcrt(['O2', 'H+', 'H2O'], [1, 4, -2], T=T, P=P, show=False)
        if result.out is not None and 'logK' in result.out.columns:
            logK = result.out['logK'].iloc[0]
        else:
            logK = 83.1  # Approximate value at 25°C
    except:
        logK = 83.1
    
    log_fO2 = 4 * pe + 4 * pH - logK
    
    return log_fO2

Calculate oxygen fugacity from pe and pH.

Parameters

pe : float
Electron activity
pH : float, default 7.0
Solution pH
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar

Returns

float
log fO2 (log oxygen fugacity)
def pe_calculation(self,
redox_couple: str | Dict[str, float],
concentrations: Dict[str, float],
pH: float = 7.0,
T: float = 298.15,
P: float = 1.0) ‑> float
Expand source code
def pe_calculation(self, redox_couple: Union[str, Dict[str, float]],
                  concentrations: Dict[str, float],
                  pH: float = 7.0, T: float = 298.15, P: float = 1.0) -> float:
    """
    Calculate pe (electron activity) for a redox couple.
    
    Parameters
    ----------
    redox_couple : str or dict
        Redox couple name or reaction dictionary
    concentrations : dict
        Species concentrations {species: concentration}
    pH : float, default 7.0
        Solution pH
    T : float, default 298.15
        Temperature in Kelvin
    P : float, default 1.0
        Pressure in bar
        
    Returns
    -------
    float
        pe value
    """
    
    if isinstance(redox_couple, str):
        if redox_couple not in self.redox_reactions:
            raise ValueError(f"Unknown redox couple: {redox_couple}")
        reaction = self.redox_reactions[redox_couple]
    else:
        reaction = redox_couple
    
    # Calculate equilibrium constant
    try:
        species_names = [sp for sp in reaction.keys() if sp != 'e-']
        coefficients = [reaction[sp] for sp in species_names]
        
        result = subcrt(species_names, coefficients, T=T, P=P, show=False)
        if result.out is not None and 'logK' in result.out.columns:
            logK = result.out['logK'].iloc[0]
        else:
            logK = 0.0
    except Exception as e:
        warnings.warn(f"Could not calculate logK: {e}")
        logK = 0.0
    
    # Apply Nernst equation
    n_electrons = abs(reaction.get('e-', 1))  # Number of electrons
    
    # Calculate activity quotient
    log_Q = 0.0
    for species, coeff in reaction.items():
        if species == 'e-':
            continue
        elif species == 'H+':
            activity = 10**(-pH)
        elif species in concentrations:
            activity = concentrations[species]
            # Apply activity coefficients if needed
            gamma = self._get_activity_coefficient(species, concentrations, T)
            activity *= gamma
        else:
            activity = 1.0  # Default for species not specified
        
        if activity > 0:
            log_Q += coeff * np.log10(activity)
    
    # Nernst equation: pe = pe° + (1/n) * log(Q)
    # where pe° = logK/n for the half-reaction
    pe = logK / n_electrons + log_Q / n_electrons
    
    return pe

Calculate pe (electron activity) for a redox couple.

Parameters

redox_couple : str or dict
Redox couple name or reaction dictionary
concentrations : dict
Species concentrations {species: concentration}
pH : float, default 7.0
Solution pH
T : float, default 298.15
Temperature in Kelvin
P : float, default 1.0
Pressure in bar

Returns

float
pe value
def pe_from_eh(self, eh: float, T: float = 298.15) ‑> float
Expand source code
def pe_from_eh(self, eh: float, T: float = 298.15) -> float:
    """
    Convert Eh (redox potential) to pe.
    
    Parameters
    ----------
    eh : float
        Redox potential (Eh) in Volts
    T : float, default 298.15
        Temperature in Kelvin
        
    Returns
    -------
    float
        Electron activity (pe)
    """
    
    # pe = F * Eh / (RT * ln(10))
    RT_F = 8.314 * T / 96485
    pe = eh / (RT_F * 2.302585)
    
    return pe

Convert Eh (redox potential) to pe.

Parameters

eh : float
Redox potential (Eh) in Volts
T : float, default 298.15
Temperature in Kelvin

Returns

float
Electron activity (pe)