pychnosz.models.dew API documentation
Description: DEW (Deep Earth Water) model implementation …

# 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