STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_props_reference.py
Go to the documentation of this file.
1"""Does the property layer produce physically right numbers?
2
3Phase 01's exit criterion, and the only part of it that catches the failure
4that matters: a plumbing bug that returns *a* number rather than *the* number.
5Everything here is checked against values from outside this codebase.
6
7Three kinds of check, because each catches something the others cannot:
8
9* **Saturation and single-phase points** against standard reference values.
10 Loose tolerances -- these are literature figures, not this package's output --
11 but they would catch a wrong species mapping, a unit error, or a molar/mass
12 basis mix-up instantly.
13* **Critical points**, tightly. They are constants of the equation of state, so
14 the only way they move is if a species now resolves to a different fluid.
15* **Cross-backend agreement.** Interpolated tables must reproduce the equation
16 of state they were built from. This is the one that catches a corrupt or
17 stale table cache, and it is the check with no external reference at all --
18 it asks only that the two paths agree.
19
20Note that these are *reference* points, not a validation campaign. Agreeing
21with CoolProp is not evidence that CoolProp is right; it is evidence that this
22package is asking it correctly. Physical validation against measured hardware
23data is Phase 12, and cannot start until there is measured hardware data.
24"""
25
26from __future__ import annotations
27
28import pytest
29
30from feedtwin.props import Fluid, Phase
31
32#: Saturated liquid at one standard atmosphere, from standard references.
33#: (fluid, T_sat [K], rho_liquid [kg/m3]) -- 0.5% tolerance, which is wider
34#: than the equation of state and narrower than any plausible bug.
35NORMAL_BOILING = [
36 ("nitrogen", 77.355, 806.1),
37 ("oxygen", 90.188, 1141.2),
38 ("ethanol", 351.57, 736.4),
39 ("helium", 4.224, 124.7),
40]
41
42#: Critical points -- constants of the equation of state.
43#: (fluid, Tc [K], Pc [MPa]). Temperature is pinned tighter than pressure
44#: because reference sources agree on it more closely.
45CRITICAL_POINTS = [
46 ("nitrogen", 126.192, 3.3958),
47 ("oxygen", 154.599, 5.0464),
48 ("ethanol", 514.71, 6.268),
49 ("helium", 5.195, 0.2283),
50 ("methane", 190.564, 4.5992),
51]
52
53ATM = 101325.0
54
55
56@pytest.mark.parametrize("name,T_sat,rho_liquid", NORMAL_BOILING)
57def test_normal_boiling_point(name: str, T_sat: float, rho_liquid: float) -> None:
58 """Saturation temperature and liquid density at 1 atm."""
59 fluid = Fluid(name)
60 state = fluid.state(p=ATM, q=0.0)
61
62 assert state.T == pytest.approx(T_sat, rel=5e-3)
63 assert state.rho == pytest.approx(rho_liquid, rel=5e-3)
64 assert state.phase is Phase.TWO_PHASE
65 assert state.quality == pytest.approx(0.0, abs=1e-9)
66
67
68@pytest.mark.parametrize("name,Tc,Pc_MPa", CRITICAL_POINTS)
69def test_critical_point(name: str, Tc: float, Pc_MPa: float) -> None:
70 """The species resolves to the fluid we think it does.
71
72 Read through CoolProp's trivial-property interface rather than a state
73 query, because the critical point is a property of the substance and not of
74 any state point.
75 """
76 import CoolProp.CoolProp as CP
77
78 backend_fluid = Fluid(name).species.backend_fluid
79 assert CP.PropsSI("Tcrit", backend_fluid) == pytest.approx(Tc, rel=1e-4)
80 assert CP.PropsSI("Pcrit", backend_fluid) == pytest.approx(Pc_MPa * 1e6, rel=5e-3)
81
82
84 """Liquid ethanol at 20 C, 1 atm: about 789 kg/m3.
85
86 The one fluid here whose density most people can check by eye, which is
87 exactly why it is worth pinning -- a mass/molar basis error would show up
88 as roughly 46x, not as a subtle drift.
89 """
90 assert Fluid("ethanol").get("rho", p=ATM, T=293.15) == pytest.approx(
91 789.4, rel=5e-3
92 )
93
94
95@pytest.mark.parametrize("name,expected_Z", [("nitrogen", 1.1506), ("helium", 1.1460)])
96def test_copv_is_not_an_ideal_gas(name: str, expected_Z: float) -> None:
97 """Z at a 4500 psi COPV, 293.15 K -- roughly 15% off ideal for both gases.
98
99 This is the number that justifies a real-gas property layer existing at
100 all. An ideal-gas COPV model over-predicts stored pressurant mass by about
101 15%, which propagates straight into how long the tanks stay pressurised.
102 """
103 Z = Fluid(name).get("Z", p=3.103e7, T=293.15)
104 assert Z == pytest.approx(expected_Z, rel=1e-3)
105 assert Z > 1.10, "both pressurants are strongly non-ideal at COPV pressure"
106
107
109 """Derived Z agrees with CoolProp's own, on the backend that implements it.
110
111 Z is computed here as p/(rho.R_specific.T) because the tabulated backend
112 does not implement ``compressibility_factor``. Deriving it is only
113 legitimate if it reproduces the real thing.
114 """
115 import CoolProp.CoolProp as CP
116
117 state = CP.AbstractState("HEOS", "Nitrogen")
118 state.update(CP.PT_INPUTS, 3.103e7, 293.15)
119
120 derived = Fluid("nitrogen", chain=["heos"]).get("Z", p=3.103e7, T=293.15)
121 assert derived == pytest.approx(state.compressibility_factor(), rel=1e-5)
122
123
124@pytest.mark.parametrize("name", ["nitrogen", "oxygen", "ethanol", "helium"])
125@pytest.mark.parametrize("prop", ["rho", "mu", "k", "cp", "a", "h"])
126def test_tables_reproduce_the_equation_of_state(name: str, prop: str) -> None:
127 """Interpolated tables agree with the equation of state behind them.
128
129 The check with no external reference: it does not ask whether either path
130 is right, only that they are the same. That is what catches a stale table
131 cache after a CoolProp upgrade -- a failure mode with no other symptom,
132 because both paths keep returning confident numbers.
133
134 1e-3 relative: bicubic interpolation error, not agreement to machine
135 precision, which tables cannot give.
136 """
137 fast = Fluid(name, chain=["bicubic"])
138 exact = Fluid(name, chain=["heos"])
139
140 # A state point comfortably inside the tables for every fluid here:
141 # supercritical for the cryogens, compressed liquid for ethanol.
142 p, T = 2.0e6, 320.0
143 assert fast.get(prop, p=p, T=T) == pytest.approx(
144 exact.get(prop, p=p, T=T), rel=1e-3
145 )
146
147
149 """Derived quantities agree with the fields they are derived from."""
150 state = Fluid("nitrogen").state(p=1.0e6, T=300.0)
151 assert state.gamma == pytest.approx(state.cp / state.cv, rel=1e-12)
152 assert state.nu == pytest.approx(state.mu / state.rho, rel=1e-12)
153 assert 1.3 < state.gamma < 1.5, "diatomic gas near room temperature"
static ActuatorControllerState state
Definition main.cpp:57
None test_compressibility_matches_the_equation_of_state()
None test_critical_point(str name, float Tc, float Pc_MPa)
None test_copv_is_not_an_ideal_gas(str name, float expected_Z)
None test_ethanol_density_at_room_temperature()
None test_gamma_and_kinematic_viscosity_are_consistent()
None test_tables_reproduce_the_equation_of_state(str name, str prop)
None test_normal_boiling_point(str name, float T_sat, float rho_liquid)