nx_psychrometrics.nx source
↩ module page · 179 lines · 8029 B
1// nx_psychrometrics.nx -- Converts temperature and relative humidity into psychrometric properties for HVAC monitoring and control.
2const NX_MAGIC_1228: i64 = 1228
3const NX_MAGIC_1706: i64 = 1706
4const NX_MAGIC_2339: i64 = 2339
5const NX_MAGIC_3170: i64 = 3170
6const NX_MAGIC_4246: i64 = 4246
7const NX_MAGIC_5627: i64 = 5627
8const NX_MAGIC_7385: i64 = 7385
9const NX_MAGIC_9595: i64 = 9595
10const NX_MAGIC_12352: i64 = 12352
11const NX_MAGIC_15762: i64 = 15762
12const NX_MAGIC_19946: i64 = 19946
13const NX_MAGIC_60000: i64 = 60000
14const NX_MAGIC_5000: i64 = 5000
15const NX_MAGIC_621945: i64 = 621945
16const NX_MAGIC_2501000: i64 = 2501000
17const NX_MAGIC_1860: i64 = 1860
18const NX_MAGIC_1000000: i64 = 1000000
19// nx_psychrometrics.nx -- no-float moist-air (psychrometric) property floor.
20//
21// THE PHYSICS RUNG for the sovereign AC/HVAC monitoring product ("Flume for
22// AC", data-center-grade). Raw sensors give temperature + relative humidity;
23// this organ turns those into the thermodynamic quantities that actually say
24// whether an air conditioner is doing work:
25// - saturation vapor pressure of water Psat(T) [Pa]
26// - water-vapor partial pressure Pw(T,RH) [Pa]
27// - humidity ratio (absolute humidity) W(Pw,P) [mg water / kg dry air]
28// - moist-air specific enthalpy h(T,W) [J / kg dry air]
29// - dewpoint temperature Td(Pw) [milli-degC]
30// From these the COP/EER crown number is delivered cooling = airflow * (h_return
31// - h_supply) / electrical-power (built on top in nx_ac_enthalpy_metrics).
32//
33// WHY IT IS THE TRUST FLOOR: every downstream "liar-killer" invariant (superheat
34// >= 0, supply enthalpy < return enthalpy, COP <= Carnot, a claimed
35// temperature+humidity pair must be physically possible) is expressed in THESE
36// quantities. A monitoring reading you cannot ground in physics is a reading you
37// cannot trust -- so the physics has to be right and externally checkable.
38//
39// EXTERNAL-COMP (the sensor-gap census axis-4 discipline): the saturation-
40// pressure LUT nodes are NIST/ASME steam-table / ASHRAE Fundamentals reference
41// values (NIST SRD 10, ingested in our corpus). The matched gate KATs the
42// interpolated + derived quantities against independent reference points and
43// against physical invariants -- it is judged by an outside oracle, not itself.
44//
45// NO-FLOAT: everything is integer fixed-point per the ecosystem doctrine.
46// temperature = milli-degC (mC): 25.000 degC = 25000
47// pressure = Pa (integer): 1 atm = 101325
48// RH = permille: 50% RH = 500
49// humidity W = mg / kg dry air: 0.0119 kg/kg = 11900
50// enthalpy h = J / kg dry air: 55.5 kJ/kg = 55500
51// No syscalls -> deterministic, hard-gateable with canned inputs.
52//
53// NEVER-BRICK (CLAUDE.md #26): pure computation, read-only, writes nothing to
54// any device or firmware. No firmware path exists here by construction.
55//
56// genealogy_id: project-hvac-efficiency-sclass-2026-06-23 (R-HVAC.4 physics floor)
57// + project-iot-discovery-pairing-revival-2026-06-22
58// + project-nishi-sensor-gap-census-2026-07-14 (axis-4 external-comp)
59// license_tier: ORIGINAL
60//
61// nx_capability_claims:
62// needs: [pointer_arithmetic]
63// provides: [saturation_vapor_pressure, humidity_ratio, moist_air_enthalpy,
64// dewpoint, psychrometric_floor, nist_grounded_property_lut]
65// safety: [no_floating_point, no_syscall, no_unchecked_deref,
66// bounded_iteration, read_only_no_device_write,
67// infeasible_input_is_loud]
68// verdict: [monotone_invariant, external_comp_nist_kat, no_silent_failure]
69// license: ORIGINAL
70// kind: iot_runtime_primitive
71// sss: [S0 (bit-equal), S6 (no cloud), S7 (sealed verdict)]
72
73// ---- standard sea-level atmosphere ---------------------------------
74const NX_PSY_P_ATM_PA: i64 = 101325
75
76// ---- infeasible sentinel (loud, never a fabricated positive) -------
77const NX_PSY_INFEASIBLE: i64 = -2000000000
78
79// ---- Saturation vapor pressure over liquid water, Pa ---------------
80// LUT node k == temperature (5*k) degC, 0..60 degC. Values are NIST/ASME
81// steam-table / ASHRAE Fundamentals saturation pressures (rounded to Pa).
82// An if-ladder (not a static array) by NishiLang discipline.
83// 0C=611 5C=872 10C=1228 15C=1706 20C=2339 25C=3170 30C=4246
84// 35C=5627 40C=7385 45C=9595 50C=12352 55C=15762 60C=19946
85func nx_psy_psat_node(k: i64) -> i64 {
86 if k == 0 { return 611 }
87 if k == 1 { return 872 }
88 if k == 2 { return NX_MAGIC_1228 }
89 if k == 3 { return NX_MAGIC_1706 }
90 if k == 4 { return NX_MAGIC_2339 }
91 if k == 5 { return NX_MAGIC_3170 }
92 if k == 6 { return NX_MAGIC_4246 }
93 if k == 7 { return NX_MAGIC_5627 }
94 if k == 8 { return NX_MAGIC_7385 }
95 if k == 9 { return NX_MAGIC_9595 }
96 if k == 10 { return NX_MAGIC_12352 }
97 if k == 11 { return NX_MAGIC_15762 }
98 if k == 12 { return NX_MAGIC_19946 }
99 return -1
100}
101
102// Saturation vapor pressure Psat (Pa) at temperature t_mC (milli-degC).
103// Linear interpolation between 5-degC LUT nodes; clamps to the 0..60 degC range.
104func nx_psy_psat_pa(t_mC: i64) -> i64 {
105 var t: i64 = t_mC
106 if t < 0 { t = 0 }
107 if t > NX_MAGIC_60000 { t = NX_MAGIC_60000 }
108 let k: i64 = t / NX_MAGIC_5000
109 if k >= 12 { return nx_psy_psat_node(12) }
110 let p0: i64 = nx_psy_psat_node(k)
111 let p1: i64 = nx_psy_psat_node(k + 1)
112 let frac_mC: i64 = t - (k * NX_MAGIC_5000)
113 let span: i64 = p1 - p0
114 return p0 + (span * frac_mC) / NX_MAGIC_5000
115}
116
117// Water-vapor partial pressure Pw (Pa) from temperature + RH (permille).
118// Pw = RH * Psat(T)
119func nx_psy_pw_pa(t_mC: i64, rh_permille: i64) -> i64 {
120 let ps: i64 = nx_psy_psat_pa(t_mC)
121 return (ps * rh_permille) / 1000
122}
123
124// Humidity ratio W in mg water / kg dry air.
125// W = 0.621945 * Pw / (P - Pw) (ASHRAE Fundamentals)
126// Returns NX_PSY_INFEASIBLE (loud) when Pw >= P (physically impossible).
127func nx_psy_humratio_mg(pw_pa: i64, p_atm_pa: i64) -> i64 {
128 let denom: i64 = p_atm_pa - pw_pa
129 if denom <= 0 { return NX_PSY_INFEASIBLE }
130 return (NX_MAGIC_621945 * pw_pa) / denom
131}
132
133// Moist-air specific enthalpy h in J / kg dry air (ASHRAE Fundamentals):
134// h = 1.006*t + W*(2501 + 1.86*t) [kJ/kg dry air, t in degC, W in kg/kg]
135// integer form: t_mC (milli-degC), w_mg (mg/kg) -> J/kg.
136func nx_psy_enthalpy_j(t_mC: i64, w_mg: i64) -> i64 {
137 let sensible: i64 = (1006 * t_mC) / 1000
138 let t_c: i64 = t_mC / 1000
139 let hg: i64 = NX_MAGIC_2501000 + (NX_MAGIC_1860 * t_c)
140 let latent: i64 = (w_mg * hg) / NX_MAGIC_1000000
141 return sensible + latent
142}
143
144// Convenience: moist-air enthalpy directly from (T, RH, P).
145// Loud NX_PSY_INFEASIBLE if the (T,RH) pair is not physically possible.
146func nx_psy_enthalpy_from_rh(t_mC: i64, rh_permille: i64, p_atm_pa: i64) -> i64 {
147 let pw: i64 = nx_psy_pw_pa(t_mC, rh_permille)
148 let w: i64 = nx_psy_humratio_mg(pw, p_atm_pa)
149 if w == NX_PSY_INFEASIBLE { return NX_PSY_INFEASIBLE }
150 return nx_psy_enthalpy_j(t_mC, w)
151}
152
153// Dewpoint temperature (milli-degC): the temperature at which the current
154// water-vapor partial pressure Pw would be saturated. Inverts the Psat LUT
155// (Psat is strictly monotone -> a unique inverse). Clamps to 0..60 degC.
156func nx_psy_dewpoint_mC(pw_pa: i64) -> i64 {
157 if pw_pa <= 611 { return 0 }
158 if pw_pa >= NX_MAGIC_19946 { return NX_MAGIC_60000 }
159 var k: i64 = 0
160 var done: i64 = 0
161 var out_mC: i64 = 0
162 while done == 0 {
163 if k >= 12 { done = 1 }
164 if done == 0 {
165 let p0: i64 = nx_psy_psat_node(k)
166 let p1: i64 = nx_psy_psat_node(k + 1)
167 if pw_pa >= p0 {
168 if pw_pa <= p1 {
169 let span: i64 = p1 - p0
170 let frac: i64 = pw_pa - p0
171 out_mC = (k * NX_MAGIC_5000) + (frac * NX_MAGIC_5000) / span
172 done = 1
173 }
174 }
175 if done == 0 { k = k + 1 }
176 }
177 }
178 return out_mC
179}