code wiki / (root) / nx_ac_sensor_verify.nx

nx_ac_sensor_verify.nx source

↩ module page · 291 lines · 12798 B

1// nx_ac_sensor_verify.nx -- the reading-level LIAR-KILLER: the trust backbone 2// of the sovereign AC monitoring product. 3// 4// Operator: monitoring "as a liar killer ... for a real product." A monitor is 5// only worth trusting if a reading that is physically impossible, or that a 6// second independent sensor contradicts, is REJECTED before it drives an alert 7// (or silently HIDES a fault). This organ stamps every reading bundle with a 8// sealed-enum trust verdict + a machine-readable reason, and surfaces the 9// derived telemetry (superheat / subcool / split / COP / Carnot / capacity). 10// 11// TWO INDEPENDENT DISCIPLINES (both required for SOTA-grade trust): 12// A. PHYSICS INVARIANTS -- a reading that violates thermodynamics cannot be 13// trusted no matter how confident the sensor is: 14// 1 humidity reading outside 0..100% RH (impossible sensor value) 15// 2 supply-air enthalpy > return-air enthalpy while drawing power in 16// cooling mode (the evaporator cannot ADD enthalpy -> swapped sensors) 17// 3 refrigerant superheat far below 0 (flooded coil / bad probe) 18// 4 refrigerant subcooling far below 0 19// 5 measured COP above the Carnot ceiling Tc/(Th-Tc) (thermodynamically 20// impossible -> a spoofed/miswired power or flow sensor) 21// 7 condenser liquid-line colder than outdoor ambient (heat cannot flow 22// from cold to hot -> impossible) 23// B. CROSS-SENSOR INDEPENDENCE (the nx_research_crossval discipline applied 24// to physical sensors): a second reading corroborates ONLY if it comes 25// from a DISTINCT source (same-source echo != corroboration); independent 26// disagreement is surfaced as CONFLICTED, never silently averaged. 27// 28// The FDD verdict (short-cycling / degraded / etc.) is a SEPARATE organ 29// (nx_hvac_efficiency): this one asks only "can this reading be trusted?", not 30// "is the equipment healthy?". A plausible-but-faulty reading (e.g. compressor 31// drawing power yet delivering ~0 cooling) PASSES the liar-killer (it is 32// physically self-consistent) and is classified downstream by the FDD. 33// 34// NO-FLOAT integer; NO syscalls (deterministic, canned-input gateable). 35// NEVER-BRICK #26: read-only, no device/firmware write. 36// 37// genealogy_id: project-hvac-efficiency-sclass-2026-06-23 38// + project-nishi-verification-stack-sota-2026-07-14 (crossval independence) 39// + project-nishi-sensor-gap-census-2026-07-14 (liar-killer axis-1) 40// license_tier: ORIGINAL 41// 42// nx_capability_claims: 43// needs: [nx_psychrometrics, nx_ac_enthalpy_metrics] 44// provides: [reading_trust_verdict, physics_invariant_liar_kill, 45// cross_sensor_independence, carnot_ceiling_check, 46// swapped_sensor_detect, evidence_grounded_monitoring] 47// safety: [no_floating_point, no_syscall, bounded_iteration, 48// read_only_no_device_write, sealed_enum_verdict, 49// impossible_reading_rejected, echo_is_not_corroboration] 50// verdict: [sealed_enum_trust, no_silent_failure, conflict_surfaced] 51// license: ORIGINAL 52// kind: iot_runtime_primitive 53// sss: [S0 (bit-equal), S6 (no cloud), S7 (sealed verdict)] 54 55import "nx_hvac_efficiency.nx" 56import "nx_psychrometrics.nx" 57import "nx_ac_enthalpy_metrics.nx" 58const NX_MAGIC_1200: i64 = 1200 59 60// ---- sensor-channel "not present" sentinel ------------------------- 61const NX_ACV_ABSENT: i64 = -1000000 62 63// ---- sealed-enum trust verdict ------------------------------------- 64const NX_ACV_INSUFFICIENT_DATA: i64 = 0 65const NX_ACV_PLAUSIBLE: i64 = 1 // physics OK, single source 66const NX_ACV_CORROBORATED: i64 = 2 // physics OK + independent agreement 67const NX_ACV_IMPLAUSIBLE_PHYSICS: i64 = 3 // a hard invariant violated 68const NX_ACV_CONFLICTED: i64 = 4 // independent sensors disagree 69const NX_ACV_BAD_ARG: i64 = 5 // non-cooling mode / bad args 70const NX_ACV_N: i64 = 6 71 72func nx_acv_verdict_is_valid(v: i64) -> i64 { 73 if v < 0 { return 0 } 74 if v >= NX_ACV_N { return 0 } 75 return 1 76} 77 78// ---- input: one time-aligned sensor reading bundle ----------------- 79// Optional channels absent -> set to NX_ACV_ABSENT (air-side-only kits work; 80// the refrigerant/outdoor invariants simply do not run). 81struct AcReading { 82 mode: i64, // NX_HVAC_MODE_COOL (this organ verifies cooling) 83 t_supply_mC: i64, // supply (conditioned) air temp 84 rh_supply_pm: i64, // supply air RH permille 85 t_return_mC: i64, // return air temp 86 rh_return_pm: i64, // return air RH permille 87 airflow_cfm: i64, // measured/estimated airflow 88 rho_gm3: i64, // air density g/m^3 (NX_MAGIC_1200 default; altitude-corrected) 89 t_suction_mC: i64, // suction line temp (ABSENT ok) 90 t_sat_evap_mC: i64, // evaporator saturation temp (ABSENT ok) 91 t_liquid_mC: i64, // liquid line temp (ABSENT ok) 92 t_sat_cond_mC: i64, // condenser saturation temp (ABSENT ok) 93 t_outdoor_mC: i64, // outdoor ambient (ABSENT ok) 94 p_elec_w: i64, // electrical power drawn 95 t_return_b_mC: i64, // independent 2nd return-air reading (ABSENT ok) 96 return_a_src: i64, // source id of the primary return sensor 97 return_b_src: i64, // source id of the 2nd sensor (0 = none) 98 return_tol_mC: i64, // agreement tolerance for corroboration 99} 100 101// ---- output: trust verdict + reason + derived telemetry ------------ 102struct AcVerify { 103 verdict: i64, // NX_ACV_* 104 reason: i64, // invariant/conflict code (0 = none) 105 superheat_mC: i64, // ABSENT if not computable 106 subcool_mC: i64, 107 split_mC: i64, // return - supply air 108 q_cool_w: i64, // delivered cooling (loud negative if not cooling) 109 cop_x100: i64, // measured COP (INFEASIBLE if not computable) 110 carnot_x100: i64, // Carnot ceiling (INFEASIBLE if not computable) 111 corroborated: i64, // 1 agree / 0 none / -1 conflict / -2 echo(non-indep) 112} 113 114// ---- reason codes -------------------------------------------------- 115const NX_ACV_R_NONE: i64 = 0 116const NX_ACV_R_RH_RANGE: i64 = 1 // humidity outside 0..100% 117const NX_ACV_R_REVERSED: i64 = 2 // supply enthalpy > return under power 118const NX_ACV_R_SUPERHEAT: i64 = 3 // superheat far below 0 119const NX_ACV_R_SUBCOOL: i64 = 4 // subcooling far below 0 120const NX_ACV_R_CARNOT: i64 = 5 // COP above Carnot ceiling 121const NX_ACV_R_LIQUID_AMB: i64 = 7 // liquid line colder than ambient 122const NX_ACV_R_CONFLICT: i64 = 8 // independent sensors disagree 123 124// Cross-sensor independence check (the crossval discipline for sensors): 125// 0 no independent second sensor present 126// 1 independent + agree (within tol) -> corroborated 127// -1 independent + disagree (beyond tol) -> conflicted (surfaced) 128// -2 same source id -> ECHO, not corroboration (independence violated) 129func nx_acv_corroborate(a_val: i64, a_src: i64, b_val: i64, b_src: i64, tol: i64) -> i64 { 130 if b_src == 0 { return 0 } 131 if a_src == 0 { return 0 } 132 if a_src == b_src { return -2 } 133 var d: i64 = a_val - b_val 134 if d < 0 { d = 0 - d } 135 if d <= tol { return 1 } 136 return -1 137} 138 139// ---- the liar-killer ----------------------------------------------- 140func nx_acv_verify(r: *AcReading, out: *AcVerify) -> i64 { 141 out.verdict = NX_ACV_INSUFFICIENT_DATA 142 out.reason = NX_ACV_R_NONE 143 out.superheat_mC = NX_ACV_ABSENT 144 out.subcool_mC = NX_ACV_ABSENT 145 out.split_mC = NX_ACV_ABSENT 146 out.q_cool_w = NX_ACM_INFEASIBLE 147 out.cop_x100 = NX_ACM_INFEASIBLE 148 out.carnot_x100 = NX_ACM_INFEASIBLE 149 out.corroborated = 0 150 151 // this organ verifies COOLING (air conditioning) 152 if r.mode != NX_HVAC_MODE_COOL { 153 out.verdict = NX_ACV_BAD_ARG 154 return out.verdict 155 } 156 157 // need the air-side core channels 158 var have_air: i64 = 1 159 if r.t_supply_mC == NX_ACV_ABSENT { have_air = 0 } 160 if r.t_return_mC == NX_ACV_ABSENT { have_air = 0 } 161 if r.airflow_cfm == NX_ACV_ABSENT { have_air = 0 } 162 if r.p_elec_w == NX_ACV_ABSENT { have_air = 0 } 163 if have_air == 0 { 164 out.verdict = NX_ACV_INSUFFICIENT_DATA 165 return out.verdict 166 } 167 168 out.split_mC = nx_ac_evap_split_mC(r.t_return_mC, r.t_supply_mC) 169 170 // ---- INVARIANT 1: humidity readings must be physically possible ---- 171 if r.rh_supply_pm < 0 { 172 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 173 out.reason = NX_ACV_R_RH_RANGE 174 return out.verdict 175 } 176 if r.rh_supply_pm > 1000 { 177 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 178 out.reason = NX_ACV_R_RH_RANGE 179 return out.verdict 180 } 181 if r.rh_return_pm < 0 { 182 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 183 out.reason = NX_ACV_R_RH_RANGE 184 return out.verdict 185 } 186 if r.rh_return_pm > 1000 { 187 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 188 out.reason = NX_ACV_R_RH_RANGE 189 return out.verdict 190 } 191 192 let h_sup: i64 = nx_psy_enthalpy_from_rh(r.t_supply_mC, r.rh_supply_pm, NX_PSY_P_ATM_PA) 193 let h_ret: i64 = nx_psy_enthalpy_from_rh(r.t_return_mC, r.rh_return_pm, NX_PSY_P_ATM_PA) 194 if h_sup == NX_PSY_INFEASIBLE { 195 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 196 out.reason = NX_ACV_R_RH_RANGE 197 return out.verdict 198 } 199 if h_ret == NX_PSY_INFEASIBLE { 200 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 201 out.reason = NX_ACV_R_RH_RANGE 202 return out.verdict 203 } 204 205 let dh: i64 = h_ret - h_sup 206 out.q_cool_w = nx_ac_delivered_cooling_w(r.airflow_cfm, dh, r.rho_gm3) 207 208 // ---- INVARIANT 2: no reversed cooling while drawing real power ---- 209 // dh < 0 means supply air carries MORE enthalpy than return -> the 210 // evaporator is adding heat to the air it should cool -> impossible. 211 // (dh ~ 0 with power = a broken-but-honest unit; that is PLAUSIBLE and is 212 // classified by the FDD, not rejected here.) 213 if r.p_elec_w > 50 { 214 if dh < -200 { 215 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 216 out.reason = NX_ACV_R_REVERSED 217 return out.verdict 218 } 219 } 220 221 // ---- INVARIANT 3: superheat cannot be far below zero ------------- 222 if r.t_suction_mC != NX_ACV_ABSENT { 223 if r.t_sat_evap_mC != NX_ACV_ABSENT { 224 let sh: i64 = nx_ac_superheat_mC(r.t_suction_mC, r.t_sat_evap_mC) 225 out.superheat_mC = sh 226 if sh < -1000 { 227 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 228 out.reason = NX_ACV_R_SUPERHEAT 229 return out.verdict 230 } 231 } 232 } 233 234 // ---- INVARIANT 4: subcooling cannot be far below zero ------------ 235 if r.t_liquid_mC != NX_ACV_ABSENT { 236 if r.t_sat_cond_mC != NX_ACV_ABSENT { 237 let sc: i64 = nx_ac_subcool_mC(r.t_sat_cond_mC, r.t_liquid_mC) 238 out.subcool_mC = sc 239 if sc < -1000 { 240 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 241 out.reason = NX_ACV_R_SUBCOOL 242 return out.verdict 243 } 244 } 245 } 246 247 // ---- INVARIANT 5: measured COP cannot exceed the Carnot ceiling -- 248 if r.t_outdoor_mC != NX_ACV_ABSENT { 249 let carnot: i64 = nx_ac_carnot_cop_x100(r.t_return_mC, r.t_outdoor_mC) 250 out.carnot_x100 = carnot 251 let cop: i64 = nx_ac_cop_x100(out.q_cool_w, r.p_elec_w) 252 out.cop_x100 = cop 253 if carnot != NX_ACM_INFEASIBLE { 254 if cop != NX_ACM_INFEASIBLE { 255 if cop > carnot { 256 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 257 out.reason = NX_ACV_R_CARNOT 258 return out.verdict 259 } 260 } 261 } 262 } 263 264 // ---- INVARIANT 7: liquid line >= outdoor ambient ----------------- 265 if r.t_liquid_mC != NX_ACV_ABSENT { 266 if r.t_outdoor_mC != NX_ACV_ABSENT { 267 if r.t_liquid_mC < (r.t_outdoor_mC - 1000) { 268 out.verdict = NX_ACV_IMPLAUSIBLE_PHYSICS 269 out.reason = NX_ACV_R_LIQUID_AMB 270 return out.verdict 271 } 272 } 273 } 274 275 // ---- physics OK: now cross-sensor independence ------------------- 276 let corr: i64 = nx_acv_corroborate(r.t_return_mC, r.return_a_src, r.t_return_b_mC, r.return_b_src, r.return_tol_mC) 277 out.corroborated = corr 278 if corr == -1 { 279 out.verdict = NX_ACV_CONFLICTED 280 out.reason = NX_ACV_R_CONFLICT 281 return out.verdict 282 } 283 if corr == 1 { 284 out.verdict = NX_ACV_CORROBORATED 285 return out.verdict 286 } 287 // corr 0 (single source) or -2 (echo, non-independent) -> plausible but 288 // not corroborated (the echo is recorded in out.corroborated = -2). 289 out.verdict = NX_ACV_PLAUSIBLE 290 return out.verdict 291}