geoforce / solver /properties.py
Ubuntu
GeoForce-Solver pressure solver passes Theis benchmark (0.38% err)
2b07453
"""IAPWS-IF97 liquid water properties for GeoForce-Solver.
For the Day-1 Theis benchmark we only need *reference-state* properties
(constant density and viscosity), which matches the linearized Boussinesq
assumption in the pressure equation.
The full IAPWS-IF97 wrapper is available for the coupled T-P solve where
density and viscosity vary with temperature.
Single-phase liquid only. If the user asks for vapor or two-phase, return
NaN — the scope rules forbid that physics in this hackathon build.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from iapws import IAPWS97
def _celsius_to_kelvin(t_c: float) -> float:
return t_c + 273.15
def _pa_to_mpa(p_pa: float) -> float:
return p_pa * 1e-6
@dataclass(frozen=True)
class WaterProperties:
"""Constant reference-state water properties (Boussinesq).
Attributes:
rho: density (kg/m^3)
mu: dynamic viscosity (Pa.s)
cp: isobaric specific heat (J/kg/K)
h: specific enthalpy (J/kg)
c_f: isothermal fluid compressibility (1/Pa)
"""
rho: float
mu: float
cp: float
h: float
c_f: float
@classmethod
def at_reference(cls, t_c: float = 100.0, p_pa: float = 1.0e7) -> "WaterProperties":
"""Compute IAPWS-IF97 properties at the given (T, P) reference state.
Args:
t_c: reference temperature in Celsius (default 100 C).
p_pa: reference pressure in Pa (default 10 MPa).
"""
state = IAPWS97(T=_celsius_to_kelvin(t_c), P=_pa_to_mpa(p_pa))
# IAPWS exposes cp in kJ/kg/K and h in kJ/kg, so convert.
cp_j = state.cp * 1000.0
h_j = state.h * 1000.0
c_f = 1.0 / (state.rho * state.w**2) # isothermal compressibility = 1 / (rho c^2)
return cls(
rho=float(state.rho),
mu=float(state.mu),
cp=float(cp_j),
h=float(h_j),
c_f=float(c_f),
)
def water_rho(t_c: float | np.ndarray, p_pa: float | np.ndarray) -> np.ndarray:
"""Vectorized liquid density (kg/m^3) via IAPWS-IF97. Arrays are elementwise."""
t_arr = np.atleast_1d(t_c)
p_arr = np.atleast_1d(p_pa)
t_arr, p_arr = np.broadcast_arrays(t_arr, p_arr)
out = np.empty_like(t_arr, dtype=np.float64)
for idx in np.ndindex(t_arr.shape):
s = IAPWS97(T=_celsius_to_kelvin(float(t_arr[idx])), P=_pa_to_mpa(float(p_arr[idx])))
out[idx] = s.rho
return out.reshape(np.asarray(t_c).shape) if np.ndim(t_c) else float(out[0])
def water_mu(t_c: float | np.ndarray, p_pa: float | np.ndarray = 1.0e7) -> np.ndarray:
"""Vectorized liquid dynamic viscosity (Pa.s) via IAPWS-IF97."""
t_arr = np.atleast_1d(t_c)
p_arr = np.atleast_1d(p_pa)
t_arr, p_arr = np.broadcast_arrays(t_arr, p_arr)
out = np.empty_like(t_arr, dtype=np.float64)
for idx in np.ndindex(t_arr.shape):
s = IAPWS97(T=_celsius_to_kelvin(float(t_arr[idx])), P=_pa_to_mpa(float(p_arr[idx])))
out[idx] = s.mu
return out.reshape(np.asarray(t_c).shape) if np.ndim(t_c) else float(out[0])