Module pychnosz.models.iapws95

IAPWS-95 water model implementation.

This module implements the IAPWS-95 formulation for the thermodynamic properties of ordinary water substance for general and scientific use. This is the international standard for water properties.

This implementation exactly matches the R CHNOSZ package, with identical coefficients and derivative calculations. No shortcuts or approximations - full fidelity to Wagner & Pruss (2002).

References: - Wagner, W., & Pruß, A. (2002). The IAPWS formulation 1995 for the thermodynamic properties of ordinary water substance for general and scientific use. Journal of Physical and Chemical Reference Data, 31(2), 387-535. - Fernández, D. P., et al. (1997). A formulation for the static permittivity of water and steam at temperatures from 238 K to 873 K at pressures up to 1200 MPa. Journal of Physical and Chemical Reference Data, 26(4), 1125-1166. - R CHNOSZ package IAPWS95.R implementation

Functions

def rho_IAPWS95_accurate(T: float | numpy.ndarray,
P: float | numpy.ndarray,
state: str = '',
trace: int = 0) ‑> numpy.ndarray
Expand source code
def rho_IAPWS95_accurate(T: Union[float, np.ndarray], P: Union[float, np.ndarray], 
                        state: str = "", trace: int = 0) -> np.ndarray:
    """
    Return density in kg/m³ corresponding to given pressure (bar) and temperature (K).
    
    Exact implementation matching R CHNOSZ rho.IAPWS95 function with numerical root finding.
    """
    T = np.atleast_1d(np.asarray(T, dtype=float))
    P = np.atleast_1d(np.asarray(P, dtype=float))
    
    # Ensure T and P have same length
    if len(P) < len(T):
        P = np.resize(P, len(T))
    elif len(T) < len(P):
        T = np.resize(T, len(P))
        
    rho = np.full_like(T, np.nan)
    
    # Critical point constants
    T_critical = 647.096  # K
    P_critical = 22.064   # MPa
    
    # Convert pressure from bar to MPa (matching R code line 60)
    P_MPa = P / 10.0
    
    for i in range(len(T)):
        if np.isnan(T[i]) or np.isnan(P[i]) or T[i] <= 0 or P[i] <= 0:
            continue
            
        # Function to find zero: P_calculated - P_target = 0
        def dP(rho_guess):
            if rho_guess <= 0:
                return float('inf')
            try:
                # Use the accurate IAPWS95 pressure calculation
                P_calc_MPa = accurate_iapws95.calculate_IAPWS95_property('P', T[i], rho_guess)
                return P_calc_MPa - P_MPa[i]
            except:
                return float('inf')
        
        # Phase identification and initial guess setup (matching R logic)
        try:
            Psat = _WP02_auxiliary_accurate("P.sigma", T[i])[0]  # This is in MPa
            
            if T[i] > T_critical:
                # Above critical temperature - supercritical
                interval = [0.1, 1000.0]
                
            elif P_MPa[i] > P_critical:
                # Above critical pressure - supercritical  
                rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
                # For high pressures, we need much higher densities
                # Estimate upper bound based on pressure scaling
                rho_upper = rho_sat + (P_MPa[i] - P_critical) * 4.0  # Rough scaling
                interval = [rho_sat, min(rho_upper, 1500.0)]  # Cap at reasonable max density
                
            elif P_MPa[i] <= 0.9999 * Psat:
                # Steam region
                rho_sat = _WP02_auxiliary_accurate("rho.vapor", T[i])[0]
                interval = [rho_sat * 0.1, rho_sat * 2.0]
                
            elif P_MPa[i] >= 1.00005 * Psat:
                # Liquid water region
                rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
                interval = [rho_sat * 0.9, rho_sat * 1.1]
                
            else:
                # Close to saturation - use liquid estimate
                rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
                interval = [rho_sat * 0.95, rho_sat * 1.05]
            
            # Numerical root finding using Brent's method
            try:
                rho[i] = brentq(dP, interval[0], interval[1], xtol=1e-10, rtol=1e-10)
            except Exception as e:
                if trace > 0:
                    print(f"Warning: rho_IAPWS95_accurate problems finding density at {T[i]} K and {P[i]} bar: {e}")
                rho[i] = np.nan
                
        except Exception as e:
            if trace > 0:
                print(f"Warning: rho_IAPWS95_accurate problems with phase identification at {T[i]} K and {P[i]} bar: {e}")
            rho[i] = np.nan
            
    return rho

Return density in kg/m³ corresponding to given pressure (bar) and temperature (K).

Exact implementation matching R CHNOSZ rho.IAPWS95 function with numerical root finding.

def water_IAPWS95(properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | numpy.ndarray = 100.0) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def water_IAPWS95(properties: Union[str, List[str]],
                  T: Union[float, np.ndarray] = 298.15,
                  P: Union[float, np.ndarray] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using IAPWS-95.

    This function provides the main interface to IAPWS-95 water properties,
    using the accurate implementation that matches R CHNOSZ exactly.

    Parameters
    ----------
    properties : str or list of str
        Property or properties to calculate:
        - 'P': Pressure in MPa
        - 'S': Entropy in kJ/(kg·K)
        - 'U': Internal energy in kJ/kg
        - 'H': Enthalpy in kJ/kg
        - 'G': Gibbs free energy in kJ/kg
        - 'Cv': Isochoric heat capacity in kJ/(kg·K)
        - 'Cp': Isobaric heat capacity in kJ/(kg·K)
        - 'w': Speed of sound in m/s
        - 'rho': Density in kg/m³
    T : float or array
        Temperature in Kelvin
    P : float or array
        Pressure in kPa (note: different from other modules that use bar)

    Returns
    -------
    float, array, or dict
        Calculated water properties

    Examples
    --------
    >>> import numpy as np
    >>>
    >>> # Single property at standard conditions
    >>> rho = water_IAPWS95('rho', T=298.15, P=100.0)  # 100 kPa = 1 bar
    >>> print(f"Density: {rho:.3f} kg/m³")
    >>>
    >>> # Multiple properties
    >>> props = water_IAPWS95(['rho', 'Cp'], T=298.15, P=100.0)
    >>> print(f"Density: {props['rho']:.3f} kg/m³")
    >>> print(f"Heat capacity: {props['Cp']:.3f} kJ/(kg·K)")
    >>>
    >>> # Array calculations
    >>> T_array = np.array([273.15, 298.15, 373.15])
    >>> densities = water_IAPWS95('rho', T=T_array, P=100.0)
    """
    # Convert pressure from kPa to bar for the accurate implementation
    if isinstance(P, (int, float)):
        P_bar = P / 100.0
    else:
        P_bar = np.asarray(P) / 100.0

    # Use the accurate implementation
    return water_IAPWS95_accurate(properties, T=T, P=P_bar)

Calculate water properties using IAPWS-95.

This function provides the main interface to IAPWS-95 water properties, using the accurate implementation that matches R CHNOSZ exactly.

Parameters

properties : str or list of str
Property or properties to calculate: - 'P': Pressure in MPa - 'S': Entropy in kJ/(kg·K) - 'U': Internal energy in kJ/kg - 'H': Enthalpy in kJ/kg - 'G': Gibbs free energy in kJ/kg - 'Cv': Isochoric heat capacity in kJ/(kg·K) - 'Cp': Isobaric heat capacity in kJ/(kg·K) - 'w': Speed of sound in m/s - 'rho': Density in kg/m³
T : float or array
Temperature in Kelvin
P : float or array
Pressure in kPa (note: different from other modules that use bar)

Returns

float, array, or dict
Calculated water properties

Examples

>>> import numpy as np
>>>
>>> # Single property at standard conditions
>>> rho = water_IAPWS95('rho', T=298.15, P=100.0)  # 100 kPa = 1 bar
>>> print(f"Density: {rho:.3f} kg/m³")
>>>
>>> # Multiple properties
>>> props = water_IAPWS95(['rho', 'Cp'], T=298.15, P=100.0)
>>> print(f"Density: {props['rho']:.3f} kg/m³")
>>> print(f"Heat capacity: {props['Cp']:.3f} kJ/(kg·K)")
>>>
>>> # Array calculations
>>> T_array = np.array([273.15, 298.15, 373.15])
>>> densities = water_IAPWS95('rho', T=T_array, P=100.0)
def water_IAPWS95_accurate(properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | numpy.ndarray = 1.0,
rho: float | numpy.ndarray | None = None) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def water_IAPWS95_accurate(properties: Union[str, List[str]], 
                          T: Union[float, np.ndarray] = 298.15,
                          P: Union[float, np.ndarray] = 1.0,
                          rho: Optional[Union[float, np.ndarray]] = None) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using accurate IAPWS-95 implementation.
    
    This function provides an accurate implementation of IAPWS-95 that matches
    the R CHNOSZ package exactly, with no shortcuts or approximations.
    
    Parameters
    ----------
    properties : str or list of str
        Property or properties to calculate ('P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho')
    T : float or array
        Temperature in Kelvin
    P : float or array
        Pressure in bar (used to calculate density if rho not provided)
    rho : float or array, optional
        Density in kg/m³. If provided, used directly; if not, calculated from T,P
        
    Returns
    -------
    float, array, or dict
        Calculated water properties
        
    Examples
    --------
    >>> # Single property with T,P
    >>> p = water_IAPWS95_accurate('P', T=298.15, P=1.0)
    >>> 
    >>> # Single property with T,rho
    >>> p = water_IAPWS95_accurate('P', T=298.15, rho=997.0)
    >>> 
    >>> # Multiple properties
    >>> props = water_IAPWS95_accurate(['rho', 'Cp'], T=298.15, P=1.0)
    """
    # Handle input types
    if isinstance(properties, str):
        properties = [properties]
        single_prop = True
    else:
        single_prop = False
    
    # Convert inputs
    T = np.atleast_1d(np.asarray(T, dtype=float))
    
    if rho is None:
        # Calculate density from T,P
        P = np.atleast_1d(np.asarray(P, dtype=float))
        rho_calc = rho_IAPWS95_accurate(T, P)
    else:
        # Use provided density
        rho_calc = np.atleast_1d(np.asarray(rho, dtype=float))
        P = np.atleast_1d(np.asarray(P, dtype=float))
    
    # Ensure same length
    max_len = max(len(T), len(rho_calc))
    if len(T) < max_len:
        T = np.resize(T, max_len)
    if len(rho_calc) < max_len:
        rho_calc = np.resize(rho_calc, max_len)
    
    # Calculate properties
    results = {}
    
    # Reference state correction constants (from R water.R lines 187-194)
    # Convert to SUPCRT reference state at the triple point
    # difference = SUPCRT - IAPWS ( + entropy in G )
    M = 18.015268  # g/mol, molar mass of water
    Tr = 298.15    # Reference temperature
    cal_to_J = 4.184  # Conversion factor from cal to J
    
    # Pre-calculate reference corrections (from R)
    dH = (-68316.76 - 451.75437) * cal_to_J  # J/mol
    dS = (16.7123 - 1.581072) * cal_to_J     # J/mol/K  
    dU = (-67434.5 - 451.3229) * cal_to_J    # J/mol
    dA_base = (-55814.06 + 20.07376) * cal_to_J  # J/mol
    
    for prop in properties:
        if prop.lower() == 'rho':
            # Return density
            results[prop] = rho_calc if len(rho_calc) > 1 else rho_calc[0]
        else:
            # Calculate other properties
            prop_values = np.full(max_len, np.nan)
            
            for i in range(max_len):
                if not (np.isnan(T[i]) or np.isnan(rho_calc[i]) or T[i] <= 0 or rho_calc[i] <= 0):
                    try:
                        # Get raw IAPWS95 value in kJ/kg (specific units)
                        raw_value = accurate_iapws95.calculate_IAPWS95_property(prop, T[i], rho_calc[i])
                        
                        # Convert to J/mol (molar units) and apply reference state corrections
                        if prop.lower() == 'g':
                            # Gibbs energy: IAPWS95("g")*M + dG
                            dG = (-56687.71 + 19.64228 - dS * (T[i] - Tr)) * cal_to_J
                            prop_values[i] = raw_value * M + dG
                        elif prop.lower() == 'h':
                            # Enthalpy: IAPWS95("h")*M + dH
                            prop_values[i] = raw_value * M + dH
                        elif prop.lower() == 'u':
                            # Internal energy: IAPWS95("u")*M + dU
                            prop_values[i] = raw_value * M + dU
                        elif prop.lower() == 'a':
                            # Helmholtz energy: IAPWS95("a")*M + dA
                            dA = dA_base - dS * (T[i] - Tr)
                            prop_values[i] = raw_value * M + dA
                        elif prop.lower() == 's':
                            # Entropy: IAPWS95("s")*M + dS
                            prop_values[i] = raw_value * M + dS
                        elif prop.lower() in ['cv', 'cp']:
                            # Heat capacities: just convert to molar units (no reference correction)
                            prop_values[i] = raw_value * M
                        else:
                            # Other properties (P, w, etc.): use as-is or convert units as needed
                            if prop.lower() == 'w':
                                # Speed of sound: convert m/s to cm/s (factor of 100)
                                prop_values[i] = raw_value * 100
                            elif prop.lower() == 'p':
                                # Pressure: convert from MPa to bar (factor of 10)
                                prop_values[i] = raw_value * 10
                            else:
                                # Other properties: return as-is
                                prop_values[i] = raw_value
                                
                    except Exception:
                        prop_values[i] = np.nan
            
            results[prop] = prop_values if len(prop_values) > 1 else prop_values[0]
    
    # Return results
    if single_prop:
        return results[properties[0]]
    else:
        return results

Calculate water properties using accurate IAPWS-95 implementation.

This function provides an accurate implementation of IAPWS-95 that matches the R CHNOSZ package exactly, with no shortcuts or approximations.

Parameters

properties : str or list of str
Property or properties to calculate ('P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho')
T : float or array
Temperature in Kelvin
P : float or array
Pressure in bar (used to calculate density if rho not provided)
rho : float or array, optional
Density in kg/m³. If provided, used directly; if not, calculated from T,P

Returns

float, array, or dict
Calculated water properties

Examples

>>> # Single property with T,P
>>> p = water_IAPWS95_accurate('P', T=298.15, P=1.0)
>>> 
>>> # Single property with T,rho
>>> p = water_IAPWS95_accurate('P', T=298.15, rho=997.0)
>>> 
>>> # Multiple properties
>>> props = water_IAPWS95_accurate(['rho', 'Cp'], T=298.15, P=1.0)
def water_iapws95(properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | numpy.ndarray = 100.0) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def water_IAPWS95(properties: Union[str, List[str]],
                  T: Union[float, np.ndarray] = 298.15,
                  P: Union[float, np.ndarray] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using IAPWS-95.

    This function provides the main interface to IAPWS-95 water properties,
    using the accurate implementation that matches R CHNOSZ exactly.

    Parameters
    ----------
    properties : str or list of str
        Property or properties to calculate:
        - 'P': Pressure in MPa
        - 'S': Entropy in kJ/(kg·K)
        - 'U': Internal energy in kJ/kg
        - 'H': Enthalpy in kJ/kg
        - 'G': Gibbs free energy in kJ/kg
        - 'Cv': Isochoric heat capacity in kJ/(kg·K)
        - 'Cp': Isobaric heat capacity in kJ/(kg·K)
        - 'w': Speed of sound in m/s
        - 'rho': Density in kg/m³
    T : float or array
        Temperature in Kelvin
    P : float or array
        Pressure in kPa (note: different from other modules that use bar)

    Returns
    -------
    float, array, or dict
        Calculated water properties

    Examples
    --------
    >>> import numpy as np
    >>>
    >>> # Single property at standard conditions
    >>> rho = water_IAPWS95('rho', T=298.15, P=100.0)  # 100 kPa = 1 bar
    >>> print(f"Density: {rho:.3f} kg/m³")
    >>>
    >>> # Multiple properties
    >>> props = water_IAPWS95(['rho', 'Cp'], T=298.15, P=100.0)
    >>> print(f"Density: {props['rho']:.3f} kg/m³")
    >>> print(f"Heat capacity: {props['Cp']:.3f} kJ/(kg·K)")
    >>>
    >>> # Array calculations
    >>> T_array = np.array([273.15, 298.15, 373.15])
    >>> densities = water_IAPWS95('rho', T=T_array, P=100.0)
    """
    # Convert pressure from kPa to bar for the accurate implementation
    if isinstance(P, (int, float)):
        P_bar = P / 100.0
    else:
        P_bar = np.asarray(P) / 100.0

    # Use the accurate implementation
    return water_IAPWS95_accurate(properties, T=T, P=P_bar)

Calculate water properties using IAPWS-95.

This function provides the main interface to IAPWS-95 water properties, using the accurate implementation that matches R CHNOSZ exactly.

Parameters

properties : str or list of str
Property or properties to calculate: - 'P': Pressure in MPa - 'S': Entropy in kJ/(kg·K) - 'U': Internal energy in kJ/kg - 'H': Enthalpy in kJ/kg - 'G': Gibbs free energy in kJ/kg - 'Cv': Isochoric heat capacity in kJ/(kg·K) - 'Cp': Isobaric heat capacity in kJ/(kg·K) - 'w': Speed of sound in m/s - 'rho': Density in kg/m³
T : float or array
Temperature in Kelvin
P : float or array
Pressure in kPa (note: different from other modules that use bar)

Returns

float, array, or dict
Calculated water properties

Examples

>>> import numpy as np
>>>
>>> # Single property at standard conditions
>>> rho = water_IAPWS95('rho', T=298.15, P=100.0)  # 100 kPa = 1 bar
>>> print(f"Density: {rho:.3f} kg/m³")
>>>
>>> # Multiple properties
>>> props = water_IAPWS95(['rho', 'Cp'], T=298.15, P=100.0)
>>> print(f"Density: {props['rho']:.3f} kg/m³")
>>> print(f"Heat capacity: {props['Cp']:.3f} kJ/(kg·K)")
>>>
>>> # Array calculations
>>> T_array = np.array([273.15, 298.15, 373.15])
>>> densities = water_IAPWS95('rho', T=T_array, P=100.0)

Classes

class AccurateIAPWS95Water
Expand source code
class AccurateIAPWS95Water:
    """
    Accurate IAPWS-95 water model implementation matching R CHNOSZ exactly.
    
    This class provides thermodynamic properties of water using the IAPWS-95
    formulation with exact coefficients and derivative calculations from the
    Wagner & Pruss (2002) specification as implemented in R CHNOSZ.
    """
    
    def __init__(self):
        """Initialize IAPWS95 water model with exact constants."""
        # Physical constants (exactly matching R CHNOSZ)
        self.R = 0.46151805  # kJ/(kg·K) - Specific gas constant for water
        self.MW = 18.015268  # g/mol - Molecular weight
        
        # Critical constants (exactly matching R CHNOSZ)
        self.Tc = 647.096    # K - Critical temperature
        self.rhoc = 322.0    # kg/m³ - Critical density
        
        # Triple point constants
        self.Tt = 273.16     # K - Triple point temperature
        
        # Initialize coefficients exactly as in R CHNOSZ
        self._init_coefficients()
    
    def _init_coefficients(self):
        """Initialize coefficients for IAPWS-95 fundamental equation (exact R match)."""
        # Ideal gas coefficients (Table 6.1 Wagner & Pruss 2002, R CHNOSZ lines 114-117)
        self.n_ideal = np.array([
            -8.32044648201, 6.6832105268, 3.00632, 0.012436,
            0.97315, 1.27950, 0.96956, 0.24873
        ])
        
        self.gamma_ideal = np.array([
            np.nan, np.nan, np.nan, 1.28728967,
            3.53734222, 7.74073708, 9.24437796, 27.5075105
        ])
        
        # Residual part coefficients (Table 6.2 Wagner & Pruss 2002, R CHNOSZ lines 134-171)
        # c coefficients
        c_list = [np.nan]*7 + [1]*15 + [2]*20 + [3]*4 + [4] + [6]*4 + [np.nan]*5
        self.c_res = np.array(c_list)
        
        # d coefficients  
        self.d_res = np.array([
            1,1,1,2,2,3,4,1,1,1,2,2,3,4,
            4,5,7,9,10,11,13,15,1,2,2,2,3,4,
            4,4,5,6,6,7,9,9,9,9,9,10,10,12,
            3,4,4,5,14,3,6,6,6,3,3,3,np.nan,np.nan
        ])
        
        # t coefficients
        self.t_res = np.array([
            -0.5,0.875,1,0.5,0.75,0.375,1,4,6,12,1,5,4,2,
            13,9,3,4,11,4,13,1,7,1,9,10,10,3,
            7,10,10,6,10,10,1,2,3,4,8,6,9,8,
            16,22,23,23,10,50,44,46,50,0,1,4,np.nan,np.nan
        ])
        
        # n coefficients (exact values from R CHNOSZ)
        self.n_res = np.array([
            0.12533547935523E-1, 0.78957634722828E1 ,-0.87803203303561E1 ,
            0.31802509345418   ,-0.26145533859358   ,-0.78199751687981E-2,
            0.88089493102134E-2,-0.66856572307965   , 0.20433810950965   ,
           -0.66212605039687E-4,-0.19232721156002   ,-0.25709043003438   ,
            0.16074868486251   ,-0.40092828925807E-1, 0.39343422603254E-6,
           -0.75941377088144E-5, 0.56250979351888E-3,-0.15608652257135E-4,
            0.11537996422951E-8, 0.36582165144204E-6,-0.13251180074668E-11,
           -0.62639586912454E-9,-0.10793600908932   , 0.17611491008752E-1,
            0.22132295167546   ,-0.40247669763528   , 0.58083399985759   ,
            0.49969146990806E-2,-0.31358700712549E-1,-0.74315929710341   ,
            0.47807329915480   , 0.20527940895948E-1,-0.13636435110343   ,
            0.14180634400617E-1, 0.83326504880713E-2,-0.29052336009585E-1,
            0.38615085574206E-1,-0.20393486513704E-1,-0.16554050063734E-2,
            0.19955571979541E-2, 0.15870308324157E-3,-0.16388568342530E-4,
            0.43613615723811E-1, 0.34994005463765E-1,-0.76788197844621E-1,
            0.22446277332006E-1,-0.62689710414685E-4,-0.55711118565645E-9,
           -0.19905718354408   , 0.31777497330738   ,-0.11841182425981   ,
           -0.31306260323435E2 , 0.31546140237781E2 ,-0.25213154341695E4 ,
           -0.14874640856724   , 0.31806110878444
        ])
        
        # Additional coefficients for complex terms (R CHNOSZ lines 162-171)
        alpha_list = [np.nan]*51 + [20,20,20,np.nan,np.nan]
        self.alpha_res = np.array(alpha_list)
        
        beta_list = [np.nan]*51 + [150,150,250,0.3,0.3]
        self.beta_res = np.array(beta_list)
        
        gamma_list = [np.nan]*51 + [1.21,1.21,1.25,np.nan,np.nan]
        self.gamma_res = np.array(gamma_list)
        
        epsilon_list = [np.nan]*51 + [1,1,1,np.nan,np.nan]
        self.epsilon_res = np.array(epsilon_list)
        
        a_list = [np.nan]*54 + [3.5,3.5]
        self.a_res = np.array(a_list)
        
        b_list = [np.nan]*54 + [0.85,0.95]
        self.b_res = np.array(b_list)
        
        B_list = [np.nan]*54 + [0.2,0.2]
        self.B_res = np.array(B_list)
        
        C_list = [np.nan]*54 + [28,32]
        self.C_res = np.array(C_list)
        
        D_list = [np.nan]*54 + [700,800]
        self.D_res = np.array(D_list)
        
        A_list = [np.nan]*54 + [0.32,0.32]
        self.A_res = np.array(A_list)
        
        # Index ranges (from R CHNOSZ Table 6.5)
        self.i1 = np.arange(0, 7)      # 1:7 in R (0-based in Python)
        self.i2 = np.arange(7, 51)     # 8:51 in R
        self.i3 = np.arange(51, 54)    # 52:54 in R  
        self.i4 = np.arange(54, 56)    # 55:56 in R
        
    def _phi_ideal(self, delta: float, tau: float, derivative: str = 'phi') -> float:
        """
        Calculate ideal gas part of dimensionless Helmholtz energy and derivatives.
        
        Exact implementation matching R CHNOSZ IAPWS95.idealgas function.
        """
        if derivative == 'phi':
            # Equation 6.5 from Wagner & Pruss 2002
            result = (np.log(delta) + self.n_ideal[0] + self.n_ideal[1]*tau + 
                     self.n_ideal[2]*np.log(tau))
            
            # Sum term with exponentials
            for i in range(3, 8):  # n[4:8] in R (indices 3:7 in Python)
                gamma_val = self.gamma_ideal[i]
                if not np.isnan(gamma_val):
                    result += self.n_ideal[i] * np.log(1 - np.exp(-gamma_val*tau))
            
            return result
            
        elif derivative == 'phi.delta':
            return 1.0/delta
            
        elif derivative == 'phi.delta.delta':
            return -1.0/(delta**2)
            
        elif derivative == 'phi.tau':
            result = self.n_ideal[1] + self.n_ideal[2]/tau
            
            # Sum term with exponentials and gamma
            for i in range(3, 8):
                gamma_val = self.gamma_ideal[i]
                if not np.isnan(gamma_val):
                    exp_term = np.exp(-gamma_val*tau)
                    result += self.n_ideal[i] * gamma_val * ((1-exp_term)**(-1) - 1)
            
            return result
            
        elif derivative == 'phi.tau.tau':
            result = -self.n_ideal[2]/(tau**2)
            
            # Sum term with exponentials
            for i in range(3, 8):
                gamma_val = self.gamma_ideal[i]
                if not np.isnan(gamma_val):
                    exp_term = np.exp(-gamma_val*tau)
                    result -= (self.n_ideal[i] * gamma_val**2 * exp_term * 
                              (1-exp_term)**(-2))
            
            return result
            
        elif derivative == 'phi.delta.tau':
            return 0.0
            
        elif derivative == 'phi.tau.tau.tau':
            # Third derivative with respect to tau
            result = 2*self.n_ideal[2]/(tau**3)
            
            # Sum term with exponentials
            for i in range(3, 8):
                gamma_val = self.gamma_ideal[i]
                if not np.isnan(gamma_val):
                    exp_term = np.exp(-gamma_val*tau)
                    result += (self.n_ideal[i] * gamma_val**3 * exp_term * 
                              (1-exp_term)**(-3) * (2*exp_term - 1))
            
            return result
            
        elif derivative == 'phi.delta.delta.delta':
            # Third derivative with respect to delta
            return 2.0/(delta**3)
            
        elif derivative == 'phi.delta.tau.tau':
            # Mixed derivative: d³φ⁰/dδdτ²
            return 0.0
            
        elif derivative == 'phi.delta.delta.tau':
            # Mixed derivative: d³φ⁰/dδ²dτ
            return 0.0
            
        else:
            raise ValueError(f"Unknown derivative: {derivative}")
    
    def _delta_function(self, i: int, delta: float, tau: float) -> float:
        """Delta function for complex terms (R CHNOSZ Delta function)."""
        theta = self._theta_function(i, delta, tau)
        B_val = self.B_res[i]
        a_val = self.a_res[i]
        return theta**2 + B_val * ((delta-1)**2)**a_val
    
    def _theta_function(self, i: int, delta: float, tau: float) -> float:
        """Theta function for complex terms (R CHNOSZ Theta function)."""
        A_val = self.A_res[i]
        beta_val = self.beta_res[i]
        return (1-tau) + A_val * ((delta-1)**2)**(1/(2*beta_val))
    
    def _psi_function(self, i: int, delta: float, tau: float) -> float:
        """Psi function for complex terms (R CHNOSZ Psi function)."""
        C_val = self.C_res[i]
        D_val = self.D_res[i]
        return np.exp(-C_val*(delta-1)**2 - D_val*(tau-1)**2)
    
    def _delta_derivatives(self, i: int, delta: float, tau: float) -> Dict[str, float]:
        """Calculate Delta function derivatives (matching R CHNOSZ exactly)."""
        theta = self._theta_function(i, delta, tau)
        A_val = self.A_res[i]
        B_val = self.B_res[i]
        a_val = self.a_res[i]
        b_val = self.b_res[i]
        beta_val = self.beta_res[i]
        
        # dDelta/ddelta
        dDelta_ddelta = ((delta-1) * 
                        (A_val*theta*2/beta_val*((delta-1)**2)**(1/(2*beta_val)-1) +
                         2*B_val*a_val*((delta-1)**2)**(a_val-1)))
        
        # d²Delta/ddelta² (handle division by zero when delta ≈ 1)
        if abs(delta - 1) < 1e-15:
            # Use L'Hôpital's rule or limit behavior
            d2Delta_ddelta2 = (4*B_val*a_val*(a_val-1) + 
                              2*A_val**2*(1/beta_val)**2 + 
                              A_val*theta*4/beta_val*(1/(2*beta_val)-1))
        else:
            d2Delta_ddelta2 = (1/(delta-1)*dDelta_ddelta + (delta-1)**2 * (
                4*B_val*a_val*(a_val-1)*((delta-1)**2)**(a_val-2) + 
                2*A_val**2*(1/beta_val)**2 * (((delta-1)**2)**(1/(2*beta_val)-1))**2 + 
                A_val*theta*4/beta_val*(1/(2*beta_val)-1) * 
                ((delta-1)**2)**(1/(2*beta_val)-2)))
        
        # Delta^b derivatives
        delta_func = self._delta_function(i, delta, tau)
        dDelta_bi_ddelta = b_val * delta_func**(b_val-1) * dDelta_ddelta
        d2Delta_bi_ddelta2 = (b_val * (delta_func**(b_val-1) * d2Delta_ddelta2 + 
                             (b_val-1) * delta_func**(b_val-2) * dDelta_ddelta**2))
        
        # Tau derivatives
        dDelta_bi_dtau = -2*theta*b_val*delta_func**(b_val-1)
        d2Delta_bi_dtau2 = (2*b_val*delta_func**(b_val-1) + 
                           4*theta**2*b_val*(b_val-1)*delta_func**(b_val-2))
        
        # Mixed derivative
        d2Delta_bi_ddelta_dtau = (-A_val*b_val*2/beta_val*delta_func**(b_val-1)*(delta-1) *
                                 ((delta-1)**2)**(1/(2*beta_val)-1) - 
                                 2*theta*b_val*(b_val-1)*delta_func**(b_val-2)*dDelta_ddelta)
        
        return {
            'dDelta_ddelta': dDelta_ddelta,
            'd2Delta_ddelta2': d2Delta_ddelta2,
            'dDelta_bi_ddelta': dDelta_bi_ddelta,
            'd2Delta_bi_ddelta2': d2Delta_bi_ddelta2,
            'dDelta_bi_dtau': dDelta_bi_dtau,
            'd2Delta_bi_dtau2': d2Delta_bi_dtau2,
            'd2Delta_bi_ddelta_dtau': d2Delta_bi_ddelta_dtau
        }
    
    def _psi_derivatives(self, i: int, delta: float, tau: float) -> Dict[str, float]:
        """Calculate Psi function derivatives (matching R CHNOSZ exactly)."""
        C_val = self.C_res[i]
        D_val = self.D_res[i]
        psi = self._psi_function(i, delta, tau)
        
        return {
            'dPsi_ddelta': -2*C_val*(delta-1)*psi,
            'd2Psi_ddelta2': (2*C_val*(delta-1)**2 - 1) * 2*C_val*psi,
            'dPsi_dtau': -2*D_val*(tau-1)*psi,
            'd2Psi_dtau2': (2*D_val*(tau-1)**2 - 1) * 2*D_val*psi,
            'd2Psi_ddelta_dtau': 4*C_val*D_val*(delta-1)*(tau-1)*psi
        }
    
    def _phi_residual(self, delta: float, tau: float, derivative: str = 'phi') -> float:
        """
        Calculate residual part of dimensionless Helmholtz energy and derivatives.
        
        Exact implementation matching R CHNOSZ IAPWS95.residual function.
        """
        if derivative == 'phi':
            # Four terms as in R CHNOSZ phi function (lines 201-206)
            term1 = np.sum(self.n_res[self.i1] * delta**self.d_res[self.i1] * tau**self.t_res[self.i1])
            
            term2 = np.sum(self.n_res[self.i2] * delta**self.d_res[self.i2] * tau**self.t_res[self.i2] *
                          np.exp(-delta**self.c_res[self.i2]))
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] *
                             np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2))
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    delta_func = self._delta_function(i, delta, tau)
                    psi_val = self._psi_function(i, delta, tau)
                    term4 += self.n_res[i] * delta_func**self.b_res[i] * delta * psi_val
            
            return term1 + term2 + term3 + term4
            
        elif derivative == 'phi.delta':
            # phi.delta implementation (R CHNOSZ lines 208-214)
            term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] * 
                          delta**(self.d_res[self.i1]-1) * tau**self.t_res[self.i1])
            
            term2 = np.sum(self.n_res[self.i2] * np.exp(-delta**self.c_res[self.i2]) *
                          (delta**(self.d_res[self.i2]-1) * tau**self.t_res[self.i2] *
                           (self.d_res[self.i2] - self.c_res[self.i2] * delta**self.c_res[self.i2])))
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
                    term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
                             (self.d_res[i]/delta - 2*alpha_val*(delta-epsilon_val)))
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    delta_func = self._delta_function(i, delta, tau)
                    psi_val = self._psi_function(i, delta, tau)
                    psi_derivs = self._psi_derivatives(i, delta, tau)
                    delta_derivs = self._delta_derivatives(i, delta, tau)
                    
                    term4 += (self.n_res[i] * 
                             (delta_func**self.b_res[i] * (psi_val + delta*psi_derivs['dPsi_ddelta']) +
                              delta_derivs['dDelta_bi_ddelta'] * delta * psi_val))
            
            return term1 + term2 + term3 + term4
            
        elif derivative == 'phi.delta.delta':
            # phi.delta.delta implementation (R CHNOSZ lines 216-224)
            term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] * (self.d_res[self.i1]-1) * 
                          delta**(self.d_res[self.i1]-2) * tau**self.t_res[self.i1])
            
            term2 = 0.0
            for i in self.i2:
                d_val = self.d_res[i]
                c_val = self.c_res[i]
                exp_term = np.exp(-delta**c_val)
                factor = ((d_val - c_val*delta**c_val) * (d_val - 1 - c_val*delta**c_val) - 
                         c_val**2 * delta**c_val)
                term2 += (self.n_res[i] * exp_term * delta**(d_val-2) * tau**self.t_res[i] * factor)
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    d_val = self.d_res[i]
                    exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
                    factor = (-2*alpha_val*delta**d_val + 4*alpha_val**2*delta**d_val*(delta-epsilon_val)**2 -
                             4*d_val*alpha_val*delta**(d_val-1)*(delta-epsilon_val) + 
                             d_val*(d_val-1)*delta**(d_val-2))
                    term3 += self.n_res[i] * tau**self.t_res[i] * exp_term * factor
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    delta_func = self._delta_function(i, delta, tau)
                    psi_val = self._psi_function(i, delta, tau)
                    psi_derivs = self._psi_derivatives(i, delta, tau)
                    delta_derivs = self._delta_derivatives(i, delta, tau)
                    
                    term4 += (self.n_res[i] * 
                             (delta_func**self.b_res[i] * 
                              (2*psi_derivs['dPsi_ddelta'] + delta*psi_derivs['d2Psi_ddelta2']) +
                              2*delta_derivs['dDelta_bi_ddelta'] * 
                              (psi_val + delta*psi_derivs['dPsi_ddelta']) +
                              delta_derivs['d2Delta_bi_ddelta2'] * delta * psi_val))
            
            return term1 + term2 + term3 + term4
            
        elif derivative == 'phi.tau':
            # phi.tau implementation (R CHNOSZ lines 226-231)
            term1 = np.sum(self.n_res[self.i1] * self.t_res[self.i1] * 
                          delta**self.d_res[self.i1] * tau**(self.t_res[self.i1]-1))
            
            term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] * 
                          delta**self.d_res[self.i2] * tau**(self.t_res[self.i2]-1) * 
                          np.exp(-delta**self.c_res[self.i2]))
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
                    term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
                             (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val)))
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    psi_val = self._psi_function(i, delta, tau)
                    psi_derivs = self._psi_derivatives(i, delta, tau)
                    delta_derivs = self._delta_derivatives(i, delta, tau)
                    
                    term4 += (self.n_res[i] * delta * 
                             (delta_derivs['dDelta_bi_dtau'] * psi_val +
                              self._delta_function(i, delta, tau)**self.b_res[i] * psi_derivs['dPsi_dtau']))
            
            return term1 + term2 + term3 + term4
            
        elif derivative == 'phi.tau.tau':
            # phi.tau.tau implementation (R CHNOSZ lines 233-239)
            term1 = np.sum(self.n_res[self.i1] * self.t_res[self.i1] * (self.t_res[self.i1]-1) *
                          delta**self.d_res[self.i1] * tau**(self.t_res[self.i1]-2))
            
            term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] * (self.t_res[self.i2]-1) *
                          delta**self.d_res[self.i2] * tau**(self.t_res[self.i2]-2) *
                          np.exp(-delta**self.c_res[self.i2]))
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
                    tau_factor = (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val))
                    term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
                             (tau_factor**2 - self.t_res[i]/tau**2 - 2*beta_val))
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    delta_func = self._delta_function(i, delta, tau)
                    psi_val = self._psi_function(i, delta, tau)
                    psi_derivs = self._psi_derivatives(i, delta, tau)
                    delta_derivs = self._delta_derivatives(i, delta, tau)
                    
                    term4 += (self.n_res[i] * delta * 
                             (delta_derivs['d2Delta_bi_dtau2'] * psi_val +
                              2*delta_derivs['dDelta_bi_dtau'] * psi_derivs['dPsi_dtau'] +
                              delta_func**self.b_res[i] * psi_derivs['d2Psi_dtau2']))
            
            return term1 + term2 + term3 + term4
            
        elif derivative == 'phi.delta.tau':
            # phi.delta.tau implementation (R CHNOSZ lines 241-248)
            term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] * self.t_res[self.i1] *
                          delta**(self.d_res[self.i1]-1) * tau**(self.t_res[self.i1]-1))
            
            term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] *
                          delta**(self.d_res[self.i2]-1) * tau**(self.t_res[self.i2]-1) *
                          (self.d_res[self.i2] - self.c_res[self.i2]*delta**self.c_res[self.i2]) *
                          np.exp(-delta**self.c_res[self.i2]))
            
            term3 = 0.0
            for i in self.i3:
                alpha_val = self.alpha_res[i]
                beta_val = self.beta_res[i]
                epsilon_val = self.epsilon_res[i]
                gamma_val = self.gamma_res[i]
                if not (np.isnan(alpha_val) or np.isnan(beta_val)):
                    exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
                    delta_factor = (self.d_res[i]/delta - 2*alpha_val*(delta-epsilon_val))
                    tau_factor = (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val))
                    term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
                             delta_factor * tau_factor)
            
            term4 = 0.0
            for i in self.i4:
                if not np.isnan(self.b_res[i]):
                    delta_func = self._delta_function(i, delta, tau)
                    psi_val = self._psi_function(i, delta, tau)
                    psi_derivs = self._psi_derivatives(i, delta, tau)
                    delta_derivs = self._delta_derivatives(i, delta, tau)
                    
                    term4 += (self.n_res[i] *
                             (delta_func**self.b_res[i] * 
                              (psi_derivs['dPsi_dtau'] + delta*psi_derivs['d2Psi_ddelta_dtau']) +
                              delta*delta_derivs['dDelta_bi_ddelta']*psi_derivs['dPsi_dtau'] +
                              delta_derivs['dDelta_bi_dtau'] * 
                              (psi_val + delta*psi_derivs['dPsi_ddelta']) +
                              delta_derivs['d2Delta_bi_ddelta_dtau']*delta*psi_val))
            
            return term1 + term2 + term3 + term4
            
            
            
            
        else:
            raise ValueError(f"Unknown derivative: {derivative}")
    
    def calculate_IAPWS95_property(self, property_name: str, T: float, rho: float) -> float:
        """
        Calculate individual IAPWS95 property (matching R CHNOSZ IAPWS95 function).
        
        Parameters
        ----------
        property_name : str
            Property name (matching R CHNOSZ property names)
        T : float
            Temperature in K
        rho : float
            Density in kg/m³
            
        Returns
        -------
        float
            Property value
        """
        # Calculate dimensionless variables (Equation 6.4)
        delta = rho / self.rhoc
        tau = self.Tc / T
        
        # Property calculations matching R CHNOSZ (lines 32-81)
        if property_name.lower() == 'p':
            # Pressure in MPa (R line 34: x*rho*R*T/1000)
            x = 1 + delta * self._phi_residual(delta, tau, 'phi.delta')
            return x * rho * self.R * T / 1000.0
            
        elif property_name.lower() == 's':
            # Entropy in kJ/(kg·K) (R lines 36-38)
            phi_ideal = self._phi_ideal(delta, tau, 'phi')
            phi_residual = self._phi_residual(delta, tau, 'phi')
            phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
            phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
            x = tau * (phi_tau_ideal + phi_tau_residual) - phi_ideal - phi_residual
            return x * self.R
            
        elif property_name.lower() == 'u':
            # Internal energy in kJ/kg (R lines 40-42)
            phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
            phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
            x = tau * (phi_tau_ideal + phi_tau_residual)
            return x * self.R * T
            
        elif property_name.lower() == 'h':
            # Enthalpy in kJ/kg (R lines 44-46)
            phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
            phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
            phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
            x = 1 + tau * (phi_tau_ideal + phi_tau_residual) + delta * phi_delta_residual
            return x * self.R * T
            
        elif property_name.lower() == 'g':
            # Gibbs energy in kJ/kg (R lines 48-50)
            phi_ideal = self._phi_ideal(delta, tau, 'phi')
            phi_residual = self._phi_residual(delta, tau, 'phi')
            phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
            x = 1 + phi_ideal + phi_residual + delta * phi_delta_residual
            return x * self.R * T
            
        elif property_name.lower() == 'cv':
            # Isochoric heat capacity in kJ/(kg·K) (R lines 52-54)
            phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
            phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
            x = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
            return x * self.R
            
        elif property_name.lower() == 'cp':
            # Isobaric heat capacity in kJ/(kg·K) (R lines 56-60)
            phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
            phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
            phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
            phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
            phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
            
            term1 = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
            term2 = ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
                    (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual))
            x = term1 + term2
            return x * self.R
            
        elif property_name.lower() == 'w':
            # Speed of sound in m/s (R lines 71-75)
            phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
            phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
            phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
            phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
            phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
            
            x = (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual - 
                ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
                 (tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual))))
            return np.sqrt(x * self.R * T)
            
            
            
            
            
            
            
        else:
            raise ValueError(f"Unknown property: {property_name}")
    
    def calculate(self, 
                  properties: Union[str, List[str]], 
                  T: Union[float, np.ndarray] = 298.15,
                  rho: Union[float, np.ndarray] = 1000.0) -> Union[float, np.ndarray, Dict[str, Any]]:
        """
        Calculate water properties using accurate IAPWS-95.
        
        Parameters
        ----------
        properties : str or list of str
            Property or list of properties to calculate
        T : float or array
            Temperature in Kelvin
        rho : float or array
            Density in kg/m³
            
        Returns
        -------
        float, array, or dict
            Calculated properties
        """
        # 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))
        rho = np.atleast_1d(np.asarray(rho, dtype=float))
        
        # Ensure same length
        max_len = max(len(T), len(rho))
        if len(T) < max_len:
            T = np.resize(T, max_len)
        if len(rho) < max_len:
            rho = np.resize(rho, max_len)
        
        # Calculate properties
        results = {}
        
        for prop in properties:
            prop_values = np.full(max_len, np.nan)
            
            for i in range(max_len):
                if not (np.isnan(T[i]) or np.isnan(rho[i]) or T[i] <= 0 or rho[i] <= 0):
                    try:
                        prop_values[i] = self.calculate_IAPWS95_property(prop, T[i], rho[i])
                    except Exception:
                        prop_values[i] = np.nan
            
            results[prop] = prop_values if len(prop_values) > 1 else prop_values[0]
        
        # Return results
        if single_prop:
            return results[properties[0]]
        else:
            return results

Accurate IAPWS-95 water model implementation matching R CHNOSZ exactly.

This class provides thermodynamic properties of water using the IAPWS-95 formulation with exact coefficients and derivative calculations from the Wagner & Pruss (2002) specification as implemented in R CHNOSZ.

Initialize IAPWS95 water model with exact constants.

Methods

def calculate(self,
properties: str | List[str],
T: float | numpy.ndarray = 298.15,
rho: float | numpy.ndarray = 1000.0) ‑> float | numpy.ndarray | Dict[str, Any]
Expand source code
def calculate(self, 
              properties: Union[str, List[str]], 
              T: Union[float, np.ndarray] = 298.15,
              rho: Union[float, np.ndarray] = 1000.0) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using accurate IAPWS-95.
    
    Parameters
    ----------
    properties : str or list of str
        Property or list of properties to calculate
    T : float or array
        Temperature in Kelvin
    rho : float or array
        Density in kg/m³
        
    Returns
    -------
    float, array, or dict
        Calculated properties
    """
    # 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))
    rho = np.atleast_1d(np.asarray(rho, dtype=float))
    
    # Ensure same length
    max_len = max(len(T), len(rho))
    if len(T) < max_len:
        T = np.resize(T, max_len)
    if len(rho) < max_len:
        rho = np.resize(rho, max_len)
    
    # Calculate properties
    results = {}
    
    for prop in properties:
        prop_values = np.full(max_len, np.nan)
        
        for i in range(max_len):
            if not (np.isnan(T[i]) or np.isnan(rho[i]) or T[i] <= 0 or rho[i] <= 0):
                try:
                    prop_values[i] = self.calculate_IAPWS95_property(prop, T[i], rho[i])
                except Exception:
                    prop_values[i] = np.nan
        
        results[prop] = prop_values if len(prop_values) > 1 else prop_values[0]
    
    # Return results
    if single_prop:
        return results[properties[0]]
    else:
        return results

Calculate water properties using accurate IAPWS-95.

Parameters

properties : str or list of str
Property or list of properties to calculate
T : float or array
Temperature in Kelvin
rho : float or array
Density in kg/m³

Returns

float, array, or dict
Calculated properties
def calculate_IAPWS95_property(self, property_name: str, T: float, rho: float) ‑> float
Expand source code
def calculate_IAPWS95_property(self, property_name: str, T: float, rho: float) -> float:
    """
    Calculate individual IAPWS95 property (matching R CHNOSZ IAPWS95 function).
    
    Parameters
    ----------
    property_name : str
        Property name (matching R CHNOSZ property names)
    T : float
        Temperature in K
    rho : float
        Density in kg/m³
        
    Returns
    -------
    float
        Property value
    """
    # Calculate dimensionless variables (Equation 6.4)
    delta = rho / self.rhoc
    tau = self.Tc / T
    
    # Property calculations matching R CHNOSZ (lines 32-81)
    if property_name.lower() == 'p':
        # Pressure in MPa (R line 34: x*rho*R*T/1000)
        x = 1 + delta * self._phi_residual(delta, tau, 'phi.delta')
        return x * rho * self.R * T / 1000.0
        
    elif property_name.lower() == 's':
        # Entropy in kJ/(kg·K) (R lines 36-38)
        phi_ideal = self._phi_ideal(delta, tau, 'phi')
        phi_residual = self._phi_residual(delta, tau, 'phi')
        phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
        phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
        x = tau * (phi_tau_ideal + phi_tau_residual) - phi_ideal - phi_residual
        return x * self.R
        
    elif property_name.lower() == 'u':
        # Internal energy in kJ/kg (R lines 40-42)
        phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
        phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
        x = tau * (phi_tau_ideal + phi_tau_residual)
        return x * self.R * T
        
    elif property_name.lower() == 'h':
        # Enthalpy in kJ/kg (R lines 44-46)
        phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
        phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
        phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
        x = 1 + tau * (phi_tau_ideal + phi_tau_residual) + delta * phi_delta_residual
        return x * self.R * T
        
    elif property_name.lower() == 'g':
        # Gibbs energy in kJ/kg (R lines 48-50)
        phi_ideal = self._phi_ideal(delta, tau, 'phi')
        phi_residual = self._phi_residual(delta, tau, 'phi')
        phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
        x = 1 + phi_ideal + phi_residual + delta * phi_delta_residual
        return x * self.R * T
        
    elif property_name.lower() == 'cv':
        # Isochoric heat capacity in kJ/(kg·K) (R lines 52-54)
        phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
        phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
        x = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
        return x * self.R
        
    elif property_name.lower() == 'cp':
        # Isobaric heat capacity in kJ/(kg·K) (R lines 56-60)
        phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
        phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
        phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
        phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
        phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
        
        term1 = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
        term2 = ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
                (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual))
        x = term1 + term2
        return x * self.R
        
    elif property_name.lower() == 'w':
        # Speed of sound in m/s (R lines 71-75)
        phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
        phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
        phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
        phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
        phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
        
        x = (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual - 
            ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
             (tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual))))
        return np.sqrt(x * self.R * T)
        
        
        
        
        
        
        
    else:
        raise ValueError(f"Unknown property: {property_name}")

Calculate individual IAPWS95 property (matching R CHNOSZ IAPWS95 function).

Parameters

property_name : str
Property name (matching R CHNOSZ property names)
T : float
Temperature in K
rho : float
Density in kg/m³

Returns

float
Property value
class IAPWS95Water
Expand source code
class IAPWS95Water:
    """
    IAPWS-95 water model interface class.

    This class provides thermodynamic properties of water using the IAPWS-95
    formulation based on a fundamental equation for the Helmholtz free energy.

    This interface handles unit conversions and provides a standardized API
    that matches other water models in the package.
    """

    def __init__(self):
        """Initialize IAPWS95 water model."""
        pass

    def available_properties(self) -> List[str]:
        """
        Get list of available properties.

        Returns
        -------
        List[str]
            List of available property names
        """
        return ['P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho']

    def calculate(self,
                  properties: Union[str, List[str]],
                  T: Union[float, np.ndarray] = 298.15,
                  P: Union[float, np.ndarray] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
        """
        Calculate water properties using IAPWS-95.

        Parameters
        ----------
        properties : str or list of str
            Property or list of properties to calculate
        T : float or array
            Temperature in Kelvin
        P : float or array
            Pressure in kPa

        Returns
        -------
        float, array, or dict
            Calculated properties
        """
        # Convert pressure from kPa to bar for the accurate implementation
        if isinstance(P, (int, float)):
            P_bar = P / 100.0
        else:
            P_bar = np.asarray(P) / 100.0

        # Use the accurate implementation (defined above in this file)
        return water_IAPWS95_accurate(properties, T=T, P=P_bar)

IAPWS-95 water model interface class.

This class provides thermodynamic properties of water using the IAPWS-95 formulation based on a fundamental equation for the Helmholtz free energy.

This interface handles unit conversions and provides a standardized API that matches other water models in the package.

Initialize IAPWS95 water model.

Methods

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

    Returns
    -------
    List[str]
        List of available property names
    """
    return ['P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho']

Get list of available properties.

Returns

List[str]
List of available property names
def calculate(self,
properties: str | List[str],
T: float | numpy.ndarray = 298.15,
P: float | numpy.ndarray = 100.0) ‑> 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] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
    """
    Calculate water properties using IAPWS-95.

    Parameters
    ----------
    properties : str or list of str
        Property or list of properties to calculate
    T : float or array
        Temperature in Kelvin
    P : float or array
        Pressure in kPa

    Returns
    -------
    float, array, or dict
        Calculated properties
    """
    # Convert pressure from kPa to bar for the accurate implementation
    if isinstance(P, (int, float)):
        P_bar = P / 100.0
    else:
        P_bar = np.asarray(P) / 100.0

    # Use the accurate implementation (defined above in this file)
    return water_IAPWS95_accurate(properties, T=T, P=P_bar)

Calculate water properties using IAPWS-95.

Parameters

properties : str or list of str
Property or list of properties to calculate
T : float or array
Temperature in Kelvin
P : float or array
Pressure in kPa

Returns

float, array, or dict
Calculated properties