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}