Module pychnosz.models.berman

Berman mineral equations of state implementation.

Calculate thermodynamic properties of minerals using equations from: Berman, R. G. (1988) Internally-consistent thermodynamic data for minerals in the system Na2O-K2O-CaO-MgO-FeO-Fe2O3-Al2O3-SiO2-TiO2-H2O-CO2. J. Petrol. 29, 445-522. https://doi.org/10.1093/petrology/29.2.445

This is a 1:1 Python replica of CHNOSZ-main/R/Berman.R

Functions

def Berman(name: str,
T: float | List[float] = 298.15,
P: float | List[float] = 1,
check_G: bool = False,
calc_transition: bool = True,
calc_disorder: bool = True) ‑> pandas.core.frame.DataFrame
Expand source code
def Berman(name: str, T: Union[float, List[float]] = 298.15, P: Union[float, List[float]] = 1, 
           check_G: bool = False, calc_transition: bool = True, calc_disorder: bool = True) -> pd.DataFrame:
    """
    Calculate thermodynamic properties of minerals using Berman equations.
    
    Parameters
    ----------
    name : str
        Name of the mineral
    T : float or list, optional
        Temperature in Kelvin (default: 298.15)
    P : float or list, optional  
        Pressure in bar (default: 1)
    check_G : bool, optional
        Check consistency of G in data file (default: False)
    calc_transition : bool, optional
        Calculate polymorphic transition contributions (default: True)
    calc_disorder : bool, optional
        Calculate disorder contributions (default: True)
        
    Returns
    -------
    pd.DataFrame
        DataFrame with columns T, P, G, H, S, Cp, V
    """
    
    # Reference temperature and pressure
    Pr = 1
    Tr = 298.15
    
    # Make T and P the same length
    if isinstance(T, (int, float)):
        T = [T]
    if isinstance(P, (int, float)):
        P = [P]

    # Convert to list if numpy array (to avoid element-wise multiplication bug)
    if isinstance(T, np.ndarray):
        T = T.tolist()
    if isinstance(P, np.ndarray):
        P = P.tolist()

    ncond = max(len(T), len(P))
    T = np.array(T * (ncond // len(T) + 1), dtype=float)[:ncond]
    P = np.array(P * (ncond // len(P) + 1), dtype=float)[:ncond]
    
    # Get parameters in the Berman equations
    # Start with thermodynamic parameters provided with CHNOSZ
    thermo_sys = thermo()
    if thermo_sys.Berman is None:
        raise RuntimeError("Berman data not loaded. Please run pychnosz.reset() first.")
    
    dat = thermo_sys.Berman.copy()
    
    # TODO: Handle user-supplied data file (thermo()$opt$Berman)
    # For now, just use the default data
    
    # Remove duplicates (only the first, i.e. most recent entry is kept)
    dat = dat.drop_duplicates(subset=['name'], keep='first')
    
    # Remove the multipliers on volume parameters
    vcols = ['v1', 'v2', 'v3', 'v4']  # columns with v1, v2, v3, v4
    multexp = [5, 5, 5, 8]
    for i, col in enumerate(vcols):
        if col in dat.columns:
            dat[col] = dat[col] / (10 ** multexp[i])
    
    # Which row has data for this mineral?
    matching_rows = dat[dat['name'] == name]
    if len(matching_rows) == 0:
        raise ValueError(f"Data for {name} not available in Berman database")
    
    dat_mineral = matching_rows.iloc[0]
    
    # Extract parameters for easier access
    GfPrTr = dat_mineral['GfPrTr']
    HfPrTr = dat_mineral['HfPrTr'] 
    SPrTr = dat_mineral['SPrTr']
    VPrTr = dat_mineral['VPrTr']
    
    k0 = dat_mineral['k0']
    k1 = dat_mineral['k1'] 
    k2 = dat_mineral['k2']
    k3 = dat_mineral['k3']
    k4 = dat_mineral['k4'] if not pd.isna(dat_mineral['k4']) else 0
    k5 = dat_mineral['k5'] if not pd.isna(dat_mineral['k5']) else 0
    k6 = dat_mineral['k6'] if not pd.isna(dat_mineral['k6']) else 0
    
    v1 = dat_mineral['v1'] if not pd.isna(dat_mineral['v1']) else 0
    v2 = dat_mineral['v2'] if not pd.isna(dat_mineral['v2']) else 0
    v3 = dat_mineral['v3'] if not pd.isna(dat_mineral['v3']) else 0
    v4 = dat_mineral['v4'] if not pd.isna(dat_mineral['v4']) else 0
    
    # Transition parameters
    Tlambda = dat_mineral['Tlambda'] if not pd.isna(dat_mineral['Tlambda']) else None
    Tref = dat_mineral['Tref'] if not pd.isna(dat_mineral['Tref']) else None
    dTdP = dat_mineral['dTdP'] if not pd.isna(dat_mineral['dTdP']) else None
    l1 = dat_mineral['l1'] if not pd.isna(dat_mineral['l1']) else None
    l2 = dat_mineral['l2'] if not pd.isna(dat_mineral['l2']) else None
    
    # Disorder parameters
    Tmin = dat_mineral['Tmin'] if not pd.isna(dat_mineral['Tmin']) else None
    Tmax = dat_mineral['Tmax'] if not pd.isna(dat_mineral['Tmax']) else None
    d0 = dat_mineral['d0'] if not pd.isna(dat_mineral['d0']) else None
    d1 = dat_mineral['d1'] if not pd.isna(dat_mineral['d1']) else None
    d2 = dat_mineral['d2'] if not pd.isna(dat_mineral['d2']) else None
    d3 = dat_mineral['d3'] if not pd.isna(dat_mineral['d3']) else None
    d4 = dat_mineral['d4'] if not pd.isna(dat_mineral['d4']) else None
    Vad = dat_mineral['Vad'] if not pd.isna(dat_mineral['Vad']) else None
    
    # Get the entropy of the elements using the chemical formula
    # Get formula from OBIGT and calculate using entropy() function like in R CHNOSZ
    SPrTr_elements = 0
    if thermo_sys.obigt is not None:
        obigt_match = thermo_sys.obigt[thermo_sys.obigt['name'] == name]
        if len(obigt_match) > 0:
            formula = obigt_match.iloc[0]['formula']
            # Import entropy function and calculate SPrTr_elements properly
            from ..utils.formula import entropy
            SPrTr_elements = entropy(formula)
    
    # Check that G in data file follows Benson-Helgeson convention
    if check_G and not pd.isna(GfPrTr):
        GfPrTr_calc = HfPrTr - Tr * (SPrTr - SPrTr_elements)
        Gdiff = GfPrTr_calc - GfPrTr
        if abs(Gdiff) >= 1000:
            warnings.warn(f"{name}: GfPrTr(calc) - GfPrTr(table) is too big! == {round(Gdiff)} J/mol")
    
    ### Thermodynamic properties ###
    # Calculate Cp and V (Berman, 1988 Eqs. 4 and 5)
    # k4, k5, k6 terms from winTWQ documentation (doi:10.4095/223425)
    Cp = k0 + k1 * T**(-0.5) + k2 * T**(-2) + k3 * T**(-3) + k4 * T**(-1) + k5 * T + k6 * T**2
    
    P_Pr = P - Pr
    T_Tr = T - Tr
    V = VPrTr * (1 + v1 * T_Tr + v2 * T_Tr**2 + v3 * P_Pr + v4 * P_Pr**2)
    
    # Calculate Ha (symbolically integrated using sympy - expressions not simplified)
    intCp = (T*k0 - Tr*k0 + k2/Tr - k2/T + k3/(2*Tr**2) - k3/(2*T**2) + 2.0*k1*T**0.5 - 2.0*k1*Tr**0.5 + 
             k4*np.log(T) - k4*np.log(Tr) + k5*T**2/2 - k5*Tr**2/2 - k6*Tr**3/3 + k6*T**3/3)
    
    intVminusTdVdT = (-VPrTr + P*(VPrTr + VPrTr*v4 - VPrTr*v3 - Tr*VPrTr*v1 + VPrTr*v2*Tr**2 - VPrTr*v2*T**2) +
                      P**2*(VPrTr*v3/2 - VPrTr*v4) + VPrTr*v3/2 - VPrTr*v4/3 + Tr*VPrTr*v1 + 
                      VPrTr*v2*T**2 - VPrTr*v2*Tr**2 + VPrTr*v4*P**3/3)
    
    Ha = HfPrTr + intCp + intVminusTdVdT
    
    # Calculate S (also symbolically integrated)
    intCpoverT = (k0*np.log(T) - k0*np.log(Tr) - k3/(3*T**3) + k3/(3*Tr**3) + k2/(2*Tr**2) - k2/(2*T**2) + 
                  2.0*k1*Tr**(-0.5) - 2.0*k1*T**(-0.5) + k4/Tr - k4/T + T*k5 - Tr*k5 + k6*T**2/2 - k6*Tr**2/2)
    
    intdVdT = -VPrTr*(v1 + v2*(-2*Tr + 2*T)) + P*VPrTr*(v1 + v2*(-2*Tr + 2*T))
    
    S = SPrTr + intCpoverT - intdVdT
    
    # Calculate Ga --> Berman-Brown convention (DG = DH - T*S, no S(element))
    Ga = Ha - T * S
    
    ### Polymorphic transition properties ###
    if (Tlambda is not None and Tref is not None and 
        not pd.isna(Tlambda) and not pd.isna(Tref) and 
        np.any(T > Tref) and calc_transition):
        
        # Starting transition contributions are 0
        Cptr = np.zeros(ncond)
        Htr = np.zeros(ncond)
        Str = np.zeros(ncond)
        
        # Eq. 9: Tlambda at P
        Tlambda_P = Tlambda + dTdP * (P - 1)
        
        # Eq. 8a: Cp at P
        Td = Tlambda - Tlambda_P
        Tprime = T + Td
        
        # With the condition that Tref < Tprime < Tlambda(1bar)
        iTprime = (Tref < Tprime) & (Tprime < Tlambda)
        # Handle NA values
        iTprime = iTprime & ~np.isnan(Tprime)
        
        if np.any(iTprime):
            Tprime_valid = Tprime[iTprime]
            Cptr[iTprime] = Tprime_valid * (l1 + l2 * Tprime_valid)**2
        
        # We got Cp, now calculate the integrations for H and S
        iTtr = T > Tref
        if np.any(iTtr):
            Ttr = T[iTtr].copy()
            Tlambda_P_tr = Tlambda_P[iTtr].copy()
            Td_tr = Td[iTtr] if hasattr(Td, '__len__') else np.full_like(Ttr, Td)
            
            # Handle NA values
            Tlambda_P_tr[np.isnan(Tlambda_P_tr)] = np.inf
            
            # The upper integration limit is Tlambda_P
            Ttr[Ttr >= Tlambda_P_tr] = Tlambda_P_tr[Ttr >= Tlambda_P_tr]
            
            # Derived variables
            tref = Tref - Td_tr
            x1 = l1**2 * Td_tr + 2 * l1 * l2 * Td_tr**2 + l2**2 * Td_tr**3
            x2 = l1**2 + 4 * l1 * l2 * Td_tr + 3 * l2**2 * Td_tr**2
            x3 = 2 * l1 * l2 + 3 * l2**2 * Td_tr
            x4 = l2**2
            
            # Eqs. 10, 11, 12
            Htr[iTtr] = (x1 * (Ttr - tref) + x2/2 * (Ttr**2 - tref**2) + 
                        x3/3 * (Ttr**3 - tref**3) + x4/4 * (Ttr**4 - tref**4))
            Str[iTtr] = (x1 * (np.log(Ttr) - np.log(tref)) + x2 * (Ttr - tref) + 
                        x3/2 * (Ttr**2 - tref**2) + x4/3 * (Ttr**3 - tref**3))
        
        Gtr = Htr - T * Str
        
        # Apply the transition contributions
        Ga = Ga + Gtr
        Ha = Ha + Htr
        S = S + Str
        Cp = Cp + Cptr
    
    ### Disorder thermodynamic properties ###
    if (Tmin is not None and Tmax is not None and 
        not pd.isna(Tmin) and not pd.isna(Tmax) and 
        np.any(T > Tmin) and calc_disorder):
        
        # Starting disorder contributions are 0
        Cpds = np.zeros(ncond)
        Hds = np.zeros(ncond)
        Sds = np.zeros(ncond)
        Vds = np.zeros(ncond)
        
        # The lower integration limit is Tmin
        iTds = T > Tmin
        if np.any(iTds):
            Tds = T[iTds].copy()
            # The upper integration limit is Tmax
            Tds[Tds > Tmax] = Tmax
            
            # Ber88 Eqs. 15, 16, 17
            Cpds[iTds] = d0 + d1*Tds**(-0.5) + d2*Tds**(-2) + d3*Tds + d4*Tds**2
            Hds[iTds] = (d0*(Tds - Tmin) + d1*(Tds**0.5 - Tmin**0.5)/0.5 +
                        d2*(Tds**(-1) - Tmin**(-1))/(-1) + d3*(Tds**2 - Tmin**2)/2 + d4*(Tds**3 - Tmin**3)/3)
            Sds[iTds] = (d0*(np.log(Tds) - np.log(Tmin)) + d1*(Tds**(-0.5) - Tmin**(-0.5))/(-0.5) +
                        d2*(Tds**(-2) - Tmin**(-2))/(-2) + d3*(Tds - Tmin) + d4*(Tds**2 - Tmin**2)/2)
        
        # Eq. 18; we can't do this if Vad == 0 (dolomite and gehlenite)
        if Vad is not None and not pd.isna(Vad) and Vad != 0:
            Vds = Hds / Vad
        
        # Include the Vds term with Hds
        Hds = Hds + Vds * (P - Pr)
        
        # Disordering properties above Tmax (Eq. 20)
        ihigh = T > Tmax
        if np.any(ihigh):
            Hds[ihigh] = Hds[ihigh] - (T[ihigh] - Tmax) * Sds[ihigh]
        
        Gds = Hds - T * Sds
        
        # Apply the disorder contributions
        Ga = Ga + Gds
        Ha = Ha + Hds
        S = S + Sds
        V = V + Vds
        Cp = Cp + Cpds
    
    ### (for testing) Use G = H - TS to check that integrals for H and S are written correctly
    Ga_fromHminusTS = Ha - T * S
    if not np.allclose(Ga_fromHminusTS, Ga, atol=1e-6):
        raise RuntimeError(f"{name}: incorrect integrals detected using DG = DH - T*S")
    
    ### Thermodynamic and unit conventions used in SUPCRT ###
    # Use entropy of the elements in calculation of G --> Benson-Helgeson convention (DG = DH - T*DS)
    Gf = Ga + Tr * SPrTr_elements
    
    # The output will just have "G" and "H"
    G = Gf
    H = Ha
    
    # Convert J/bar to cm^3/mol
    V = V * 10
    
    return pd.DataFrame({
        'T': T,
        'P': P, 
        'G': G,
        'H': H,
        'S': S,
        'Cp': Cp,
        'V': V
    })

Calculate thermodynamic properties of minerals using Berman equations.

Parameters

name : str
Name of the mineral
T : float or list, optional
Temperature in Kelvin (default: 298.15)
P : float or list, optional
Pressure in bar (default: 1)
check_G : bool, optional
Check consistency of G in data file (default: False)
calc_transition : bool, optional
Calculate polymorphic transition contributions (default: True)
calc_disorder : bool, optional
Calculate disorder contributions (default: True)

Returns

pd.DataFrame
DataFrame with columns T, P, G, H, S, Cp, V