Module pychnosz.models.dew

DEW (Deep Earth Water) model implementation.

This module implements the Deep Earth Water model for calculating thermodynamic and electrostatic properties of H2O at high pressures and temperatures relevant to deep crustal and mantle conditions.

References: - Sverjensky, D. A., Harrison, B., & Azzolini, D. (2014). Water in the deep Earth: The dielectric constant and the solubilities of quartz and corundum to 60 kb and 1200°C. Geochimica et Cosmochimica Acta, 129, 125-145. - Pan, D., Spanu, L., Harrison, B., Sverjensky, D. A., & Car, R. (2013). Dielectric properties of water under extreme conditions and validation of the corresponding computational approaches. PNAS, 110(17), 6646-6650.

Functions

def water_DEW(properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | str | numpy.ndarray = 1.0,
**kwargs) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def water_DEW(properties: Union[str, List[str]], 
              T: Union[float, np.ndarray] = 298.15,
              P: Union[float, np.ndarray, str] = 1.0,
              **kwargs) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using DEW model.
    
    Parameters
    ----------
    properties : str or list of str
        Property or properties to calculate
    T : float or array
        Temperature in Kelvin  
    P : float, array, or 'Psat'
        Pressure in bar, or 'Psat' for saturation pressure
    **kwargs
        Additional options
    
    Returns
    -------
    float, array, or dict
        Calculated water properties
        
    Examples
    --------
    >>> # High pressure conditions
    >>> epsilon = water_DEW('epsilon', T=873.15, P=10000)  # 600°C, 10 kbar
    >>> 
    >>> # Multiple properties at extreme conditions
    >>> props = water_DEW(['rho', 'epsilon'], T=1073.15, P=30000)  # 800°C, 30 kbar
    >>> 
    >>> # Born functions for electrolyte calculations
    >>> born = water_DEW(['QBorn', 'YBorn'], T=773.15, P=5000)
    """
    return dew_water.calculate(properties, T, P, **kwargs)

Calculate water properties using DEW model.

Parameters

properties : str or list of str
Property or properties to calculate
T : float or array
Temperature in Kelvin
P : float, array, or 'Psat'
Pressure in bar, or 'Psat' for saturation pressure
**kwargs
Additional options

Returns

float, array, or dict
Calculated water properties

Examples

>>> # High pressure conditions
>>> epsilon = water_DEW('epsilon', T=873.15, P=10000)  # 600°C, 10 kbar
>>> 
>>> # Multiple properties at extreme conditions
>>> props = water_DEW(['rho', 'epsilon'], T=1073.15, P=30000)  # 800°C, 30 kbar
>>> 
>>> # Born functions for electrolyte calculations
>>> born = water_DEW(['QBorn', 'YBorn'], T=773.15, P=5000)

Classes

class DEWWater
Expand source code
class DEWWater:
    """
    Deep Earth Water (DEW) model implementation.
    
    This class provides thermodynamic and electrostatic properties of water
    at high pressures and temperatures using the DEW model correlations.
    """
    
    def __init__(self):
        """Initialize DEW water model."""
        # Physical constants  
        self.R = 8.314462618  # J/(mol·K) - Universal gas constant
        self.MW_H2O = 18.0152  # g/mol - Molecular weight of water
        
        # Critical constants for water
        self.Tc = 647.067  # K - Critical temperature
        self.Pc = 220.48   # bar - Critical pressure
        self.rhoc = 0.32174  # g/cm³ - Critical density
        
        # DEW model parameters
        self.a_epsilon = np.array([
            -1.57637700752506e3, -6.97284414953487e4, -3.14058873029023e6,
            1.11926957750896e7, 5.49375634503012e7, -1.33934314022535e8,
            -2.56395839070779e8, 3.73875501063673e8, 4.35976880906701e8,
            -2.11156427436252e8
        ])
        
        self.b_epsilon = np.array([
            5.07722476345932e-1, 1.48046755524790e1, 2.42452179259584e2,
            -1.73986255629880e3, -7.18635413197094e3, 1.21415969235037e4,
            1.92102380413670e4, -1.31967093058141e4, -1.35915853762697e4,
            3.17251296019127e3
        ])
        
        # Pressure and temperature limits for DEW model
        self.T_min = 273.15  # K
        self.T_max = 1473.15  # K (1200°C)
        self.P_min = 1.0      # bar
        self.P_max = 60000.0  # bar (60 kbar)
    
    def available_properties(self) -> List[str]:
        """
        Get list of available water properties.

        Note: DEW model only calculates a limited set of properties.
        This matches the R CHNOSZ implementation which only provides:
        G, epsilon, QBorn, V, rho, beta, A_DH, B_DH

        Other properties requested will return NaN.
        """
        return [
            # Properties actually calculated by DEW
            'G',        # Gibbs energy (J/mol)
            'epsilon',  # Dielectric constant (DEW specialty)
            'QBorn',    # Born Q function (1/bar)
            'V',        # Molar volume (cm³/mol)
            'rho',      # Density (kg/m³)
            'beta',     # Isothermal compressibility (1/bar)
            'A_DH',     # Debye-Hückel A parameter
            'B_DH',     # Debye-Hückel B parameter
        ]
    
    def calculate(self,
                  properties: Union[str, List[str]],
                  T: Union[float, np.ndarray] = 298.15,
                  P: Union[float, np.ndarray, str] = 1.0,
                  **kwargs) -> Union[float, np.ndarray, Dict[str, Any]]:
        """
        Calculate water properties using DEW model.

        Parameters
        ----------
        properties : str or list of str
            Property or properties to calculate
        T : float or array
            Temperature in Kelvin
        P : float, array, or 'Psat'
            Pressure in bar, or 'Psat' for saturation pressure
        **kwargs
            Additional options

        Returns
        -------
        float, array, or dict
            Calculated properties
        """
        # DEBUG
        debug_dew = False
        if debug_dew:
            print(f"\nDEBUG DEW.calculate() called:")
            print(f"  properties: {properties}")
            print(f"  T (input): {T}, type: {type(T)}")
            print(f"  P (input): {P}, type: {type(P)}")

        # Handle input types
        if isinstance(properties, str):
            properties = [properties]
            single_prop = True
        else:
            single_prop = False

        # Convert inputs to arrays
        T = np.atleast_1d(np.asarray(T, dtype=float))
        
        if isinstance(P, str) and P == 'Psat':
            P_is_Psat = True
            P_vals = self._calculate_Psat(T)
        else:
            P_is_Psat = False
            P = np.atleast_1d(np.asarray(P, dtype=float))
            if len(P) < len(T):
                P = np.resize(P, len(T))
            elif len(T) < len(P):
                T = np.resize(T, len(P))
            P_vals = P
        
        # Check validity of conditions
        valid = self._check_validity(T, P_vals)

        # Check for low T or low P conditions (T < 100°C or P < 1000 bar)
        # These should use SUPCRT92 instead of DEW (as in R CHNOSZ water.R line 381)
        ilow = (T < 373.15) | (P_vals < 1000)

        # Initialize results
        results = {}

        # Get list of properties that DEW actually calculates
        supported_props = self.available_properties()

        # For low T or low P conditions, use SUPCRT92 for ALL properties
        if np.any(ilow):
            from .supcrt92_fortran import water_SUPCRT92

            # Get SUPCRT92 results for low conditions
            T_low = T[ilow]
            P_low = P_vals[ilow]

            supcrt_results = water_SUPCRT92(properties, T_low, P_low)

            # Initialize all properties with appropriate array size
            for prop in properties:
                results[prop] = np.full_like(T, np.nan, dtype=float)

            # Fill in SUPCRT92 results for low conditions
            if isinstance(supcrt_results, dict):
                for prop in properties:
                    if prop in supcrt_results:
                        results[prop][ilow] = supcrt_results[prop]
            else:
                # Single property case
                results[properties[0]][ilow] = supcrt_results

            # Special case for Pr,Tr: epsilon should be 78.47 (DEW spreadsheet value)
            iPrTr = (np.abs(T - 298.15) < 0.1) & (np.abs(P_vals - 1.0) < 0.1)
            if 'epsilon' in properties and np.any(iPrTr):
                results['epsilon'][iPrTr] = 78.47

            # If all conditions are low, return SUPCRT results
            if np.all(ilow):
                if single_prop:
                    result = results[properties[0]]
                    return result[0] if len(result) == 1 else result
                else:
                    return results

        # Calculate density first (needed for many DEW properties)
        if any(prop in properties for prop in ['rho', 'V', 'epsilon', 'QBorn', 'beta', 'A_DH', 'B_DH']):
            rho_gcm3 = self._calculate_density(T, P_vals, valid)  # g/cm³
            rho = rho_gcm3 * 1000.0  # Convert to kg/m³ like SUPCRT
            V = self.MW_H2O / rho_gcm3  # cm³/mol (use g/cm³ for volume calculation)
        else:
            rho = None
            V = None

        # Calculate each requested property for high T and high P conditions
        for prop in properties:
            # If property is not supported by DEW, return NaN (like R CHNOSZ)
            if prop not in supported_props:
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
            elif prop == 'rho':
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                results[prop][~ilow] = rho[~ilow]
            elif prop == 'V':
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                results[prop][~ilow] = V[~ilow]
            elif prop == 'epsilon':
                # Use g/cm³ density for dielectric constant calculation
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                if 'rho_gcm3' not in locals():
                    rho_gcm3 = self._calculate_density(T, P_vals, valid)
                epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)
                results[prop][~ilow] = epsilon_vals[~ilow]
            elif prop == 'G':
                # Calculate Gibbs energy using the exact DEW method
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                for i in np.where(~ilow)[0]:
                    T_celsius = T[i] - 273.15  # Convert to Celsius
                    G_cal_per_mol = self._calculate_gibbs_of_water(P_vals[i], T_celsius)  # cal/mol
                    if not np.isnan(G_cal_per_mol):
                        results[prop][i] = G_cal_per_mol * 4.184  # Convert cal/mol to J/mol
            elif prop == 'QBorn':
                # Calculate QBorn using the exact DEW calculateQ function
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                if rho is None or V is None:
                    rho_gcm3 = self._calculate_density(T, P_vals, valid)
                else:
                    rho_gcm3 = rho / 1000.0  # Convert kg/m³ to g/cm³

                for i in np.where(~ilow)[0]:
                    if valid[i]:
                        T_celsius = T[i] - 273.15  # Convert to Celsius
                        results[prop][i] = self._calculate_Q(rho_gcm3[i], T_celsius)
            elif prop == 'beta':
                # Calculate beta (isothermal compressibility)
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                if rho is None or V is None:
                    rho_gcm3 = self._calculate_density(T, P_vals, valid)
                else:
                    rho_gcm3 = rho / 1000.0  # Convert kg/m³ to g/cm³

                for i in np.where(~ilow)[0]:
                    if valid[i]:
                        T_celsius = T[i] - 273.15  # Convert to Celsius
                        # Divide drhodP by rho to get units of bar^-1 (like R code)
                        drhodP = self._calculate_drhodP(rho_gcm3[i], T_celsius)
                        results[prop][i] = drhodP / rho_gcm3[i]
            elif prop in ['A_DH', 'B_DH']:
                # Calculate Debye-Hückel parameters
                if prop not in results:
                    results[prop] = np.full_like(T, np.nan, dtype=float)
                if 'rho_gcm3' not in locals():
                    rho_gcm3 = self._calculate_density(T, P_vals, valid)
                epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)

                if prop == 'A_DH':
                    # A_DH = 1.8246e6 * rho^0.5 / (epsilon * T)^1.5
                    results[prop][~ilow] = 1.8246e6 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**1.5
                else:  # B_DH
                    # B_DH = 50.29e8 * rho^0.5 / (epsilon * T)^0.5
                    results[prop][~ilow] = 50.29e8 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**0.5
        
        # Return results
        if single_prop:
            result = results[properties[0]]
            return result[0] if len(result) == 1 else result
        else:
            # Convert to consistent array lengths
            for key in results:
                if np.isscalar(results[key]):
                    results[key] = np.full_like(T, results[key])
                elif len(results[key]) == 1 and len(T) > 1:
                    results[key] = np.full_like(T, results[key][0])
            return results
    
    def _check_validity(self, T: np.ndarray, P: np.ndarray) -> np.ndarray:
        """Check validity of T-P conditions for DEW model."""
        valid = np.ones_like(T, dtype=bool)
        
        # Temperature limits
        valid &= (T >= self.T_min)
        valid &= (T <= self.T_max) 
        
        # Pressure limits
        valid &= (P >= self.P_min)
        valid &= (P <= self.P_max)
        
        # Avoid near-critical conditions where DEW may be less accurate
        valid &= ~((T > 0.95 * self.Tc) & (P < 2 * self.Pc))
        
        return valid
    
    def _calculate_Psat(self, T: np.ndarray) -> np.ndarray:
        """
        Calculate saturation pressure using Antoine equation.
        
        Valid up to critical point.
        """
        Psat = np.full_like(T, np.nan)
        valid = (T >= 273.16) & (T <= self.Tc)
        
        if np.any(valid):
            T_valid = T[valid]
            
            # Antoine equation coefficients for water (bar, K)
            A = 8.07131
            B = 1730.63
            C = -39.724
            
            # Antoine equation: log10(Psat) = A - B/(T + C)
            log10_Psat = A - B / (T_valid + C)
            Psat[valid] = 10**log10_Psat
        
        return Psat
    
    def _calculate_density(self, T: np.ndarray, P: np.ndarray, valid: np.ndarray) -> np.ndarray:
        """
        Calculate water density using DEW model correlations.
        
        This uses the exact bisection method from the R CHNOSZ DEW implementation
        to find the density that produces the target pressure.
        """
        rho = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            
            # Use bisection method for each T, P pair (as in R DEW.R)
            rho_results = np.full(len(T_valid), np.nan)
            for i, (T_val, P_val) in enumerate(zip(T_valid, P_valid)):
                T_celsius = T_val - 273.15  # Convert to Celsius for DEW equations
                rho_results[i] = self._calculate_density_bisection(P_val, T_celsius)
            
            rho[valid] = rho_results
        
        return rho
    
    def _calculate_density_bisection(self, pressure: float, temperature_celsius: float, error: float = 0.01) -> float:
        """
        Calculate density using bisection method (exact R DEW.R implementation).
        
        Parameters
        ----------
        pressure : float
            Target pressure in bar
        temperature_celsius : float  
            Temperature in Celsius
        error : float
            Pressure error tolerance in bar (default 0.01 as in R code)
            
        Returns
        -------
        float
            Density in g/cm³
        """
        min_guess = 1e-5
        guess = 1e-5
        equation = 1  # The maxGuess is dependent on the value of "equation"
        max_guess = 7.5 * equation - 5.0  # Should be 2.5 for equation=1
        
        # Loop through and find the density (up to 50 iterations as in R)
        for i in range(50):
            # Calculate the pressure using the specified equation
            calc_p = self._calculate_pressure(guess, temperature_celsius)
            
            # If the calculated pressure is not equal to input pressure, 
            # determine a new guess based on bisection method
            if abs(calc_p - pressure) > error:
                if calc_p > pressure:
                    max_guess = guess
                    guess = (guess + min_guess) / 2.0
                elif calc_p < pressure:
                    min_guess = guess  
                    guess = (guess + max_guess) / 2.0
            else:
                return guess
                
        # If we didn't converge, return the last guess
        return guess
    
    def _calculate_pressure(self, density: float, temperature_celsius: float) -> float:
        """
        Calculate pressure from density and temperature using Zhang & Duan (2005) EOS.
        
        This is the exact implementation from R DEW.R calculatePressure function.
        
        Parameters
        ----------
        density : float
            Density in g/cm³
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            Pressure in bar
        """
        # Constants from R DEW.R
        m = 18.01528         # Molar mass of water molecule in g/mol
        ZD05_R = 83.144      # Gas Constant in cm³ bar/mol/K
        ZD05_Vc = 55.9480373 # Critical volume in cm³/mol
        ZD05_Tc = 647.25     # Critical temperature in Kelvin
        
        TK = temperature_celsius + 273.15  # Temperature must be converted to Kelvin
        Vr = m / density / ZD05_Vc
        Tr = TK / ZD05_Tc
        
        B = 0.349824207 - 2.91046273 / (Tr * Tr) + 2.00914688 / (Tr * Tr * Tr)
        C = 0.112819964 + 0.748997714 / (Tr * Tr) - 0.87320704 / (Tr * Tr * Tr)
        D = 0.0170609505 - 0.0146355822 / (Tr * Tr) + 0.0579768283 / (Tr * Tr * Tr)
        E = -0.000841246372 + 0.00495186474 / (Tr * Tr) - 0.00916248538 / (Tr * Tr * Tr)
        f = -0.100358152 / Tr
        g = -0.00182674744 * Tr
        
        delta = (1 + B / Vr + C / (Vr * Vr) + D / (Vr**4) + E / (Vr**5) + 
                 (f / (Vr * Vr) + g / (Vr**4)) * np.exp(-0.0105999998 / (Vr * Vr)))
        
        return ZD05_R * TK * density * delta / m
    
    def _calculate_gibbs_of_water(self, pressure: float, temperature_celsius: float) -> float:
        """
        Calculate Gibbs Free Energy of water using exact R DEW.R implementation.
        
        This is the exact translation of the calculateGibbsOfWater function from R DEW.R
        
        Parameters
        ----------
        pressure : float
            Pressure in bar
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            Gibbs Free Energy in cal/mol
        """
        # Gibbs Free Energy of water at 1 kb. This equation is a polynomial fit to data as a function of temperature.
        # It is valid in the range of 100 to 1000 C.
        GAtOneKb = (2.6880734E-09 * temperature_celsius**4 + 6.3163061E-07 * temperature_celsius**3 - 
                   0.019372355 * temperature_celsius**2 - 16.945093 * temperature_celsius - 55769.287)
        
        if pressure < 1000:  # Simply return zero, this method only works at P >= 1000 bars
            integral = np.nan
        elif pressure == 1000:  # Return the value calculated above from the polynomial fit
            integral = 0.0
        elif pressure > 1000:  # Integrate from 1 kb to P over the volume
            integral = 0.0
            # Integral is sum of rectangles with this width. This function in effect limits the spacing
            # to 20 bars so that very small pressures do not have unreasonably small widths. Otherwise the width
            # is chosen such that there are always 500 steps in the numerical integration. This ensures that for very
            # high pressures, there are not a huge number of steps calculated which is very computationally taxing.
            spacing = max(20.0, (pressure - 1000.0) / 500.0)
            
            # Use numpy arange to exactly match R's seq(1000, pressure, by = spacing) behavior
            # R's seq includes the endpoint, so we need to include pressure in our sequence
            P_values = np.arange(1000.0, pressure + spacing/2, spacing)  # +spacing/2 ensures we include endpoint
            
            for P_current in P_values:
                # This integral determines the density only down to an error of 100 bars
                # rather than the standard of 0.01. This is done to save computational
                # time. Tests indicate this reduces the computation by about a half while
                # introducing little error from the standard of 0.01.
                rho = self._calculate_density_bisection(P_current, temperature_celsius, error=100.0)
                integral += (18.01528 / rho / 41.84) * spacing
        
        return GAtOneKb + integral
    
    def _calculate_depsdrho(self, density: float, temperature_celsius: float) -> float:
        """
        Calculate partial derivative of dielectric constant with respect to density (dε/dρ).
        
        This is the exact implementation from R DEW.R calculate_depsdrho function.
        
        Parameters
        ----------
        density : float
            Density in g/cm³
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            dε/dρ in cm³/g
        """
        # Power Function parameters (same as for epsilon calculation)
        a1 = -0.00157637700752506
        a2 = 0.0681028783422197
        a3 = 0.754875480393944
        b1 = -8.01665106535394E-05
        b2 = -0.0687161761831994
        b3 = 4.74797272182151
        
        A = a1 * temperature_celsius + a2 * np.sqrt(temperature_celsius) + a3
        B = b1 * temperature_celsius + b2 * np.sqrt(temperature_celsius) + b3
        
        # dε/dρ = A * exp(B) * density^(A-1)
        return A * np.exp(B) * (density ** (A - 1))
    
    def _calculate_drhodP(self, density: float, temperature_celsius: float) -> float:
        """
        Calculate partial derivative of density with respect to pressure (dρ/dP).
        
        This is the exact implementation from R DEW.R calculate_drhodP function.
        
        Parameters
        ----------
        density : float
            Density in g/cm³
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            dρ/dP in g/cm³/bar
        """
        # Constants from R DEW.R
        m = 18.01528          # Molar mass of water molecule in g/mol
        ZD05_R = 83.144       # Gas Constant in cm³ bar/mol/K
        ZD05_Vc = 55.9480373  # Critical volume in cm³/mol
        ZD05_Tc = 647.25      # Critical temperature in Kelvin
        
        TK = temperature_celsius + 273.15       # temperature must be converted to Kelvin
        Tr = TK / ZD05_Tc
        cc = ZD05_Vc / m                # This term appears frequently in the equation
        Vr = m / (density * ZD05_Vc)
        
        B = 0.349824207 - 2.91046273 / (Tr * Tr) + 2.00914688 / (Tr * Tr * Tr)
        C = 0.112819964 + 0.748997714 / (Tr * Tr) - 0.87320704 / (Tr * Tr * Tr)
        D = 0.0170609505 - 0.0146355822 / (Tr * Tr) + 0.0579768283 / (Tr * Tr * Tr)
        E = -0.000841246372 + 0.00495186474 / (Tr * Tr) - 0.00916248538 / (Tr * Tr * Tr)
        f = -0.100358152 / Tr
        g = 0.0105999998 * Tr
        
        delta = (1 + B / Vr + C / (Vr**2) + D / (Vr**4) + E / (Vr**5) + 
                 (f / (Vr**2) + g / (Vr**4)) * np.exp(-0.0105999998 / (Vr**2)))
        
        kappa = (B * cc + 2 * C * (cc**2) * density + 4 * D * cc**4 * density**3 + 5 * E * cc**5 * density**4 +
                (2 * f * (cc**2) * density + 4 * g * cc**4 * density**3 - 
                 (f / (Vr**2) + g / (Vr**4)) * (2 * 0.0105999998 * (cc**2) * density)) * 
                np.exp(-0.0105999998 / (Vr**2)))
        
        return m / (ZD05_R * TK * (delta + density * kappa))
    
    def _calculate_Q(self, density: float, temperature_celsius: float) -> float:
        """
        Calculate Born Q function using exact R DEW.R implementation.
        
        This is the exact implementation from R DEW.R calculateQ function.
        
        Parameters
        ----------
        density : float
            Density in g/cm³
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            Q in bar⁻¹
        """
        epsilon = self._calculate_epsilon_single(density, temperature_celsius)
        depsdrho = self._calculate_depsdrho(density, temperature_celsius)
        drhodP = self._calculate_drhodP(density, temperature_celsius)
        
        return depsdrho * drhodP / (epsilon**2)
    
    def _calculate_epsilon_single(self, density: float, temperature_celsius: float) -> float:
        """
        Calculate epsilon for single density and temperature values.
        
        Parameters
        ----------
        density : float
            Density in g/cm³
        temperature_celsius : float
            Temperature in Celsius
            
        Returns
        -------
        float
            Dielectric constant
        """
        # DEW power function parameters (same as in array version)
        a1 = -0.00157637700752506
        a2 = 0.0681028783422197
        a3 = 0.754875480393944
        b1 = -8.01665106535394E-05
        b2 = -0.0687161761831994
        b3 = 4.74797272182151
        
        A = a1 * temperature_celsius + a2 * np.sqrt(temperature_celsius) + a3
        B = b1 * temperature_celsius + b2 * np.sqrt(temperature_celsius) + b3
        
        return np.exp(B) * (density ** A)
    
    def _calculate_dielectric_constant_with_density(self, T: np.ndarray, P: np.ndarray, 
                                                   rho_gcm3: np.ndarray, valid: np.ndarray) -> np.ndarray:
        """
        Calculate dielectric constant using pre-computed density in g/cm³.
        """
        epsilon = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            rho_valid = rho_gcm3[valid]  # Already in g/cm³
            
            # Convert temperature to Celsius for DEW correlation
            T_celsius = T_valid - 273.15
            
            # DEW power function parameters (from R code)
            a1 = -0.00157637700752506
            a2 = 0.0681028783422197
            a3 = 0.754875480393944
            b1 = -8.01665106535394E-5
            b2 = -0.0687161761831994
            b3 = 4.74797272182151
            
            # Calculate A and B
            A = a1 * T_celsius + a2 * np.sqrt(T_celsius) + a3
            B = b1 * T_celsius + b2 * np.sqrt(T_celsius) + b3
            
            # DEW dielectric constant: epsilon = exp(B) * density^A
            epsilon_calc = np.exp(B) * (rho_valid ** A)
            
            # For low T or P conditions, use SUPCRT92 (AW90) as in R version
            low_condition = (T_celsius < 100.0) | (P_valid < 1000.0)
            
            if np.any(low_condition):
                # Use Archer & Wang for low conditions
                from .archer_wang import water_AW90
                
                # Convert density to kg/m³ and pressure to MPa
                rho_kg_m3 = rho_valid[low_condition] * 1000.0  # g/cm³ to kg/m³
                P_MPa = P_valid[low_condition] / 10.0  # bar to MPa
                T_low = T_valid[low_condition]
                
                epsilon_aw90 = water_AW90(T_low, rho_kg_m3, P_MPa)
                epsilon_calc[low_condition] = epsilon_aw90
            
            # Special case: at Pr,Tr use 78.47 as in R code
            prtr_condition = (np.abs(T_celsius - 25.0) < 0.1) & (np.abs(P_valid - 1.0) < 0.1)
            if np.any(prtr_condition):
                epsilon_calc[prtr_condition] = 78.47
            
            # Apply bounds to ensure physical values
            epsilon_calc = np.clip(epsilon_calc, 1.0, 200.0)
            
            epsilon[valid] = epsilon_calc
        
        return epsilon
    
    def _calculate_reference_density(self, T: np.ndarray) -> np.ndarray:
        """Calculate reference density at 1 bar pressure."""
        # Simplified fit to water density at 1 bar
        rho0 = 1.0 - 2.5e-4 * (T - 298.15) - 5e-7 * (T - 298.15)**2
        rho0 = np.maximum(rho0, 0.1)  # Ensure positive
        return rho0
    
    def _calculate_dielectric_constant(self, T: np.ndarray, P: np.ndarray, 
                                     valid: np.ndarray) -> np.ndarray:
        """
        Calculate dielectric constant using DEW model.
        
        This is the key feature of the DEW model - accurate dielectric constants
        at high P-T conditions based on molecular dynamics simulations.
        """
        epsilon = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            
            # DEW dielectric constant correlation
            # Based on the R CHNOSZ implementation which uses the DEW power function
            # for high P-T conditions and falls back to SUPCRT92 (AW90) for low P-T.
            
            from .archer_wang import water_AW90
            
            # Calculate density first
            rho_full = self._calculate_density(T, P, valid)
            rho_valid = rho_full[valid]  # g/cm³
            
            # Convert temperature to Celsius for DEW correlation
            T_celsius = T_valid - 273.15
            
            # DEW power function parameters (from R code)
            a1 = -0.00157637700752506
            a2 = 0.0681028783422197
            a3 = 0.754875480393944
            b1 = -8.01665106535394E-5
            b2 = -0.0687161761831994
            b3 = 4.74797272182151
            
            # Calculate A and B
            A = a1 * T_celsius + a2 * np.sqrt(T_celsius) + a3
            B = b1 * T_celsius + b2 * np.sqrt(T_celsius) + b3
            
            # DEW dielectric constant: epsilon = exp(B) * density^A
            epsilon_calc = np.exp(B) * (rho_valid ** A)
            
            # For low T or P conditions, use SUPCRT92 (AW90) as in R version
            low_condition = (T_celsius < 100.0) | (P_valid < 1000.0)
            
            if np.any(low_condition):
                # Use Archer & Wang for low conditions
                # Convert density to kg/m³ and pressure to MPa
                rho_kg_m3 = rho_valid[low_condition] * 1000.0  # g/cm³ to kg/m³
                P_MPa = P_valid[low_condition] / 10.0  # bar to MPa
                T_low = T_valid[low_condition]
                
                epsilon_aw90 = water_AW90(T_low, rho_kg_m3, P_MPa)
                epsilon_calc[low_condition] = epsilon_aw90
            
            # Special case: at Pr,Tr use 78.47 as in R code
            prtr_condition = (np.abs(T_celsius - 25.0) < 0.1) & (np.abs(P_valid - 1.0) < 0.1)
            if np.any(prtr_condition):
                epsilon_calc[prtr_condition] = 78.47
            
            # Apply bounds to ensure physical values
            epsilon_calc = np.clip(epsilon_calc, 1.0, 200.0)
            
            epsilon[valid] = epsilon_calc
        
        return epsilon
    
    def _calculate_thermodynamic_properties(self, T: np.ndarray, P: np.ndarray,
                                          rho: np.ndarray, valid: np.ndarray) -> Dict[str, np.ndarray]:
        """Calculate thermodynamic properties using DEW model."""
        props = {}
        
        for prop in ['G', 'H', 'S', 'Cp', 'Cv', 'U', 'A']:
            props[prop] = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            
            # Calculate Gibbs energy using the exact DEW method
            G_results = np.full(len(T_valid), np.nan)
            for i, (T_val, P_val) in enumerate(zip(T_valid, P_valid)):
                T_celsius = T_val - 273.15  # Convert to Celsius
                G_cal_per_mol = self._calculate_gibbs_of_water(P_val, T_celsius)  # cal/mol
                G_results[i] = G_cal_per_mol * 4.184  # Convert cal/mol to J/mol
            
            props['G'][valid] = G_results
            
            # For other properties, use simplified approximations (these are not as critical for DEW)
            if any(prop in ['H', 'S', 'Cp', 'Cv', 'U', 'A'] for prop in props.keys()):
                rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
                
                # Reference state properties (liquid water at 25°C, 1 bar)
                H_ref = -285830.0  # J/mol
                S_ref = 69.95      # J/(mol·K)
                Cp_ref = 75.31     # J/(mol·K)
                
                # Temperature effects
                dT = T_valid - 298.15
                
                # Heat capacity (empirical fit for high T-P)
                Cp = Cp_ref + 0.15 * dT - 2e-4 * dT**2 + 1e-7 * dT**3
                
                # Pressure effects on heat capacity
                Cp += 1e-5 * P_valid  # Small pressure dependence
                
                # Entropy (integrate Cp/T)
                S = S_ref + Cp_ref * np.log(T_valid / 298.15) + 0.15 * dT - 1e-4 * dT**2 + (1e-7/2) * dT**3
                
                # Enthalpy (integrate Cp)
                H = H_ref + Cp_ref * dT + 0.075 * dT**2 - (2e-4/3) * dT**3 + (1e-7/4) * dT**4
                
                # Pressure effects on enthalpy (∫V dP)
                V_molar = self.MW_H2O / (rho_valid / 1000.0)  # cm³/mol (convert kg/m³ to g/cm³)
                H += V_molar * (P_valid - 1.0) * 0.01  # Convert bar·cm³/mol to J/mol
                
                # Other properties
                Cv = Cp - self.R  # Simplified relation
                U = H - P_valid * V_molar * 0.01  # Internal energy
                A = U - T_valid * S  # Helmholtz energy
                
                # Store results
                props['H'][valid] = H
                props['S'][valid] = S
                props['Cp'][valid] = Cp
                props['Cv'][valid] = Cv
                props['U'][valid] = U
                props['A'][valid] = A
        
        return props
    
    def _calculate_mechanical_properties(self, T: np.ndarray, P: np.ndarray,
                                       rho: np.ndarray, valid: np.ndarray) -> Dict[str, np.ndarray]:
        """Calculate mechanical properties for high P-T conditions."""
        props = {}
        
        for prop in ['alpha', 'beta', 'kT', 'E']:
            props[prop] = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
            V_valid = self.MW_H2O / rho_valid  # cm³/mol
            
            # Thermal expansion coefficient (modified for high P-T)
            alpha = (2.14e-4 + 1e-6 * (T_valid - 298.15) - 2e-8 * P_valid)
            alpha = np.maximum(alpha, 1e-6)  # Ensure positive
            
            # Isothermal compressibility (decreases with pressure)
            beta = 4.5e-5 * np.exp(-P_valid / 10000.0) * (298.15 / T_valid)**0.5
            beta = np.maximum(beta, 1e-7)  # Ensure positive
            
            # Derived properties
            kT = V_valid * beta  # bar·cm³/mol
            E = V_valid * alpha  # cm³/(mol·K)
            
            props['alpha'][valid] = alpha
            props['beta'][valid] = beta
            props['kT'][valid] = kT
            props['E'][valid] = E
        
        return props
    
    def _calculate_born_functions(self, T: np.ndarray, P: np.ndarray, rho: np.ndarray,
                                valid: np.ndarray) -> Dict[str, np.ndarray]:
        """Calculate Born functions using exact DEW model."""
        props = {}
        
        for prop in ['QBorn', 'YBorn', 'XBorn', 'ZBorn']:
            props[prop] = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            
            # Calculate QBorn using the exact DEW calculateQ function
            # This requires density in g/cm³, not kg/m³
            if rho is not None:
                rho_gcm3_valid = rho[valid] / 1000.0  # Convert kg/m³ to g/cm³
            else:
                rho_gcm3_full = self._calculate_density(T, P, valid)
                rho_gcm3_valid = rho_gcm3_full[valid]
            
            QBorn_results = np.full(len(T_valid), np.nan)
            epsilon_results = np.full(len(T_valid), np.nan)
            
            for i, (T_val, P_val, rho_val) in enumerate(zip(T_valid, P_valid, rho_gcm3_valid)):
                T_celsius = T_val - 273.15  # Convert to Celsius
                # Use exact DEW Q calculation
                QBorn_results[i] = self._calculate_Q(rho_val, T_celsius)
                epsilon_results[i] = self._calculate_epsilon_single(rho_val, T_celsius)
            
            # For other Born functions, use simplified relations (as in R water.R)
            # Get mechanical properties for thermal expansion
            mech_props = self._calculate_mechanical_properties(T, P, rho, valid)
            alpha_valid = mech_props['alpha'][valid]
            
            # Born functions
            YBorn = alpha_valid / epsilon_results  # 1/K
            XBorn = QBorn_results / epsilon_results  # 1/(bar·K) - note: this uses QBorn, not beta
            ZBorn = -1.0 / epsilon_results
            
            props['QBorn'][valid] = QBorn_results
            props['YBorn'][valid] = YBorn
            props['XBorn'][valid] = XBorn
            props['ZBorn'][valid] = ZBorn
        
        return props
    
    def _calculate_debye_huckel(self, T: np.ndarray, P: np.ndarray, rho: np.ndarray,
                              valid: np.ndarray) -> Dict[str, np.ndarray]:
        """Calculate Debye-Hückel parameters using DEW properties."""
        props = {}
        
        for prop in ['A_DH', 'B_DH']:
            props[prop] = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
            epsilon = self._calculate_dielectric_constant(T, P, valid)[valid]
            
            # Debye-Hückel parameters using DEW dielectric constants
            A_DH = 1.8246e6 * rho_valid**0.5 / (epsilon * T_valid)**1.5
            B_DH = 50.29e8 * rho_valid**0.5 / (epsilon * T_valid)**0.5
            
            props['A_DH'][valid] = A_DH
            props['B_DH'][valid] = B_DH
        
        return props
    
    def _calculate_transport_property(self, prop: str, T: np.ndarray, P: np.ndarray,
                                    rho: np.ndarray, valid: np.ndarray) -> np.ndarray:
        """Calculate transport properties (simplified for high P-T)."""
        result = np.full_like(T, np.nan)
        
        if np.any(valid):
            T_valid = T[valid]
            P_valid = P[valid]
            
            if prop == 'Speed':
                # Speed of sound (increases with pressure)
                result[valid] = (1402.7 + 5.0 * (T_valid - 298.15) + 
                               0.5 * np.sqrt(P_valid))
                
            elif prop == 'visc':
                # Viscosity (empirical fit for high P-T)
                result[valid] = (1e-3 * np.exp(-3.0 + 1000.0 / T_valid) * 
                               (1 + P_valid / 5000.0)**0.1)
                
            elif prop == 'tcond':
                # Thermal conductivity (increases with pressure and temperature)
                result[valid] = (0.6 + 0.002 * (T_valid - 298.15) + 
                               0.00005 * P_valid)
        
        return result

Deep Earth Water (DEW) model implementation.

This class provides thermodynamic and electrostatic properties of water at high pressures and temperatures using the DEW model correlations.

Initialize DEW water model.

Methods

def available_properties(self) ‑> List[str]
Expand source code
def available_properties(self) -> List[str]:
    """
    Get list of available water properties.

    Note: DEW model only calculates a limited set of properties.
    This matches the R CHNOSZ implementation which only provides:
    G, epsilon, QBorn, V, rho, beta, A_DH, B_DH

    Other properties requested will return NaN.
    """
    return [
        # Properties actually calculated by DEW
        'G',        # Gibbs energy (J/mol)
        'epsilon',  # Dielectric constant (DEW specialty)
        'QBorn',    # Born Q function (1/bar)
        'V',        # Molar volume (cm³/mol)
        'rho',      # Density (kg/m³)
        'beta',     # Isothermal compressibility (1/bar)
        'A_DH',     # Debye-Hückel A parameter
        'B_DH',     # Debye-Hückel B parameter
    ]

Get list of available water properties.

Note: DEW model only calculates a limited set of properties. This matches the R CHNOSZ implementation which only provides: G, epsilon, QBorn, V, rho, beta, A_DH, B_DH

Other properties requested will return NaN.

def calculate(self,
properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | str | numpy.ndarray = 1.0,
**kwargs) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def calculate(self,
              properties: Union[str, List[str]],
              T: Union[float, np.ndarray] = 298.15,
              P: Union[float, np.ndarray, str] = 1.0,
              **kwargs) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using DEW model.

    Parameters
    ----------
    properties : str or list of str
        Property or properties to calculate
    T : float or array
        Temperature in Kelvin
    P : float, array, or 'Psat'
        Pressure in bar, or 'Psat' for saturation pressure
    **kwargs
        Additional options

    Returns
    -------
    float, array, or dict
        Calculated properties
    """
    # DEBUG
    debug_dew = False
    if debug_dew:
        print(f"\nDEBUG DEW.calculate() called:")
        print(f"  properties: {properties}")
        print(f"  T (input): {T}, type: {type(T)}")
        print(f"  P (input): {P}, type: {type(P)}")

    # Handle input types
    if isinstance(properties, str):
        properties = [properties]
        single_prop = True
    else:
        single_prop = False

    # Convert inputs to arrays
    T = np.atleast_1d(np.asarray(T, dtype=float))
    
    if isinstance(P, str) and P == 'Psat':
        P_is_Psat = True
        P_vals = self._calculate_Psat(T)
    else:
        P_is_Psat = False
        P = np.atleast_1d(np.asarray(P, dtype=float))
        if len(P) < len(T):
            P = np.resize(P, len(T))
        elif len(T) < len(P):
            T = np.resize(T, len(P))
        P_vals = P
    
    # Check validity of conditions
    valid = self._check_validity(T, P_vals)

    # Check for low T or low P conditions (T < 100°C or P < 1000 bar)
    # These should use SUPCRT92 instead of DEW (as in R CHNOSZ water.R line 381)
    ilow = (T < 373.15) | (P_vals < 1000)

    # Initialize results
    results = {}

    # Get list of properties that DEW actually calculates
    supported_props = self.available_properties()

    # For low T or low P conditions, use SUPCRT92 for ALL properties
    if np.any(ilow):
        from .supcrt92_fortran import water_SUPCRT92

        # Get SUPCRT92 results for low conditions
        T_low = T[ilow]
        P_low = P_vals[ilow]

        supcrt_results = water_SUPCRT92(properties, T_low, P_low)

        # Initialize all properties with appropriate array size
        for prop in properties:
            results[prop] = np.full_like(T, np.nan, dtype=float)

        # Fill in SUPCRT92 results for low conditions
        if isinstance(supcrt_results, dict):
            for prop in properties:
                if prop in supcrt_results:
                    results[prop][ilow] = supcrt_results[prop]
        else:
            # Single property case
            results[properties[0]][ilow] = supcrt_results

        # Special case for Pr,Tr: epsilon should be 78.47 (DEW spreadsheet value)
        iPrTr = (np.abs(T - 298.15) < 0.1) & (np.abs(P_vals - 1.0) < 0.1)
        if 'epsilon' in properties and np.any(iPrTr):
            results['epsilon'][iPrTr] = 78.47

        # If all conditions are low, return SUPCRT results
        if np.all(ilow):
            if single_prop:
                result = results[properties[0]]
                return result[0] if len(result) == 1 else result
            else:
                return results

    # Calculate density first (needed for many DEW properties)
    if any(prop in properties for prop in ['rho', 'V', 'epsilon', 'QBorn', 'beta', 'A_DH', 'B_DH']):
        rho_gcm3 = self._calculate_density(T, P_vals, valid)  # g/cm³
        rho = rho_gcm3 * 1000.0  # Convert to kg/m³ like SUPCRT
        V = self.MW_H2O / rho_gcm3  # cm³/mol (use g/cm³ for volume calculation)
    else:
        rho = None
        V = None

    # Calculate each requested property for high T and high P conditions
    for prop in properties:
        # If property is not supported by DEW, return NaN (like R CHNOSZ)
        if prop not in supported_props:
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
        elif prop == 'rho':
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            results[prop][~ilow] = rho[~ilow]
        elif prop == 'V':
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            results[prop][~ilow] = V[~ilow]
        elif prop == 'epsilon':
            # Use g/cm³ density for dielectric constant calculation
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            if 'rho_gcm3' not in locals():
                rho_gcm3 = self._calculate_density(T, P_vals, valid)
            epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)
            results[prop][~ilow] = epsilon_vals[~ilow]
        elif prop == 'G':
            # Calculate Gibbs energy using the exact DEW method
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            for i in np.where(~ilow)[0]:
                T_celsius = T[i] - 273.15  # Convert to Celsius
                G_cal_per_mol = self._calculate_gibbs_of_water(P_vals[i], T_celsius)  # cal/mol
                if not np.isnan(G_cal_per_mol):
                    results[prop][i] = G_cal_per_mol * 4.184  # Convert cal/mol to J/mol
        elif prop == 'QBorn':
            # Calculate QBorn using the exact DEW calculateQ function
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            if rho is None or V is None:
                rho_gcm3 = self._calculate_density(T, P_vals, valid)
            else:
                rho_gcm3 = rho / 1000.0  # Convert kg/m³ to g/cm³

            for i in np.where(~ilow)[0]:
                if valid[i]:
                    T_celsius = T[i] - 273.15  # Convert to Celsius
                    results[prop][i] = self._calculate_Q(rho_gcm3[i], T_celsius)
        elif prop == 'beta':
            # Calculate beta (isothermal compressibility)
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            if rho is None or V is None:
                rho_gcm3 = self._calculate_density(T, P_vals, valid)
            else:
                rho_gcm3 = rho / 1000.0  # Convert kg/m³ to g/cm³

            for i in np.where(~ilow)[0]:
                if valid[i]:
                    T_celsius = T[i] - 273.15  # Convert to Celsius
                    # Divide drhodP by rho to get units of bar^-1 (like R code)
                    drhodP = self._calculate_drhodP(rho_gcm3[i], T_celsius)
                    results[prop][i] = drhodP / rho_gcm3[i]
        elif prop in ['A_DH', 'B_DH']:
            # Calculate Debye-Hückel parameters
            if prop not in results:
                results[prop] = np.full_like(T, np.nan, dtype=float)
            if 'rho_gcm3' not in locals():
                rho_gcm3 = self._calculate_density(T, P_vals, valid)
            epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)

            if prop == 'A_DH':
                # A_DH = 1.8246e6 * rho^0.5 / (epsilon * T)^1.5
                results[prop][~ilow] = 1.8246e6 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**1.5
            else:  # B_DH
                # B_DH = 50.29e8 * rho^0.5 / (epsilon * T)^0.5
                results[prop][~ilow] = 50.29e8 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**0.5
    
    # Return results
    if single_prop:
        result = results[properties[0]]
        return result[0] if len(result) == 1 else result
    else:
        # Convert to consistent array lengths
        for key in results:
            if np.isscalar(results[key]):
                results[key] = np.full_like(T, results[key])
            elif len(results[key]) == 1 and len(T) > 1:
                results[key] = np.full_like(T, results[key][0])
        return results

Calculate water properties using DEW model.

Parameters

properties : str or list of str
Property or properties to calculate
T : float or array
Temperature in Kelvin
P : float, array, or 'Psat'
Pressure in bar, or 'Psat' for saturation pressure
**kwargs
Additional options

Returns

float, array, or dict
Calculated properties