File size: 3,118 Bytes
2b07453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"""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])