code wiki / (root) / nx_psychrometrics.nx

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}