| """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)) |
| |
| cp_j = state.cp * 1000.0 |
| h_j = state.h * 1000.0 |
| c_f = 1.0 / (state.rho * state.w**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]) |
|
|