1"""What a property query returns, and the registry of what can be asked for.
3Two things live here, and they are deliberately the same concept seen twice:
5* :data:`PROPERTIES` -- the registry of every quantity the layer can produce.
6 Adding one is a single entry here plus a line in whichever backends can
7 compute it. Nothing else in the codebase enumerates properties.
8* :class:`ThermoState` -- an eager, immutable snapshot at one state point.
10**Everything is SI.** Pa, K, kg/m3, Pa.s, J/kg, J/(kg.K), W/(m.K), m/s. There is
11no unit handling inside the solver; conversion happens once at the schema
12boundary (Phase 02). A number that reaches this module is already SI.
14Why the snapshot is eager
15-------------------------
16The obvious cheaper design is lazy: hold the backend and fetch properties on
17demand. It is a trap. Backend handles are mutable and reused -- a second query
18re-points the same handle -- so a lazily-evaluated state would silently answer
19with a *later* state point's numbers. Every field is therefore read before the
22That costs about eight backend reads where the solver's hot path needs one, so
23the hot path does not use snapshots at all: :meth:`Fluid.get` fetches a single
24property. :meth:`Fluid.state` is for reporting, diagnostics and frames.
27from __future__
import annotations
29from dataclasses
import dataclass
34 """Fluid phase, independent of any backend's integer encoding.
36 Backends map their own constants onto this so that nothing above the
37 property layer imports a CoolProp enum -- the point of ADR-0001's boundary
38 is that swapping an equation-of-state library is a change inside
39 ``props/backends/``, not a change everywhere ``phase == 6`` was written.
44 TWO_PHASE =
"two_phase"
45 SUPERCRITICAL =
"supercritical"
46 SUPERCRITICAL_GAS =
"supercritical_gas"
47 SUPERCRITICAL_LIQUID =
"supercritical_liquid"
48 CRITICAL_POINT =
"critical_point"
53 return self
is Phase.TWO_PHASE
57 """Liquid or supercritical-liquid: dense, effectively incompressible.
59 Used to pick correlations -- single-phase liquid pressure drop applies
60 to a supercritical liquid too, and asking ``phase is Phase.LIQUID``
61 alone would silently route a supercritical LOX line down the gas path.
63 return self
in (Phase.LIQUID, Phase.SUPERCRITICAL_LIQUID)
66@dataclass(frozen=True, slots=True)
68 """One queryable quantity: its SI unit and what it means."""
75def _spec(name: str, unit: str, description: str) -> tuple[str, PropertySpec]:
85PROPERTIES: dict[str, PropertySpec] = dict(
87 _spec(
"p",
"Pa",
"Pressure"),
88 _spec(
"T",
"K",
"Temperature"),
89 _spec(
"rho",
"kg/m^3",
"Mass density"),
90 _spec(
"h",
"J/kg",
"Specific enthalpy"),
91 _spec(
"s",
"J/(kg.K)",
"Specific entropy"),
92 _spec(
"u",
"J/kg",
"Specific internal energy"),
93 _spec(
"cp",
"J/(kg.K)",
"Specific heat at constant pressure"),
94 _spec(
"cv",
"J/(kg.K)",
"Specific heat at constant volume"),
95 _spec(
"mu",
"Pa.s",
"Dynamic viscosity"),
96 _spec(
"k",
"W/(m.K)",
"Thermal conductivity"),
97 _spec(
"a",
"m/s",
"Speed of sound"),
98 _spec(
"Z",
"-",
"Compressibility factor, p/(rho.R_specific.T)"),
99 _spec(
"gamma",
"-",
"Heat capacity ratio cp/cv"),
100 _spec(
"molar_mass",
"kg/mol",
"Molar mass"),
106 """Add a property to the registry at runtime.
108 For quantities a project needs that this package does not ship -- a
109 surface tension correlation, a user-supplied fouling factor. Backends that
110 cannot compute it simply do not advertise it, and the fluid's chain falls
111 through to one that can.
113 if name
in PROPERTIES:
114 raise ValueError(f
"property {name!r} is already registered")
116 PROPERTIES[name] = spec
120@dataclass(frozen=True, slots=True)
122 """An immutable snapshot of one fluid at one state point. All SI.
124 ``quality`` is ``None`` outside the two-phase dome rather than a sentinel.
125 Backends signal "not applicable" with out-of-band numbers -- CoolProp uses
126 -1 in one situation and -1000 in another -- and letting either escape into
127 a mass balance as a vapour fraction is the kind of bug that produces a
128 plausible-looking wrong answer rather than a crash.
147 quality: float |
None
151 """Heat capacity ratio cp/cv."""
152 return self.cp / self.cv
155 def nu(self) -> float:
156 """Kinematic viscosity [m^2/s]."""
157 return self.mu / self.rho
160 q =
"-" if self.
quality is None else f
"{self.quality:.3f}"
162 f
"{self.fluid} @ {self.p / 1e5:.2f} bar, {self.T:.2f} K "
163 f
"[{self.phase.value}, x={q}] rho={self.rho:.4g} mu={self.mu:.4g}"
bool is_liquid_like(self)
PropertySpec register_property(str name, str unit, str description)
tuple[str, PropertySpec] _spec(str name, str unit, str description)