nx_ac_fdd.nx source
↩ module page · 200 lines · 9015 B
1// nx_ac_fdd.nx -- Converts refrigerant/air-side diagnostic metrics into actionable fault verdicts for HVAC systems.
2const NX_MAGIC_8333: i64 = 8333
3const NX_MAGIC_2778: i64 = 2778
4const NX_MAGIC_5556: i64 = 5556
5const NX_MAGIC_13889: i64 = 13889
6const NX_MAGIC_25000: i64 = 25000
7// nx_ac_fdd.nx -- refrigerant/air-side FAULT DETECTION & DIAGNOSIS verdict.
8//
9// Turns the derived quantities (superheat, subcooling, evaporator air split,
10// condenser-over-ambient, suction saturation, CFM/ton, cycles/hr) into a
11// named, actionable fault verdict -- the "what is wrong and what do I do"
12// layer of the AC monitoring product.
13//
14// GROUNDED IN PUBLISHED FDD SCIENCE (verified via the research crew against
15// primary sources): the fault signatures are the standard rule-of-thumb set
16// from NIST residential AFDD (NISTIR 7350 / TN 1774) + ASHRAE APAR (NISTIR
17// 6964), and the thresholds default to California Title 24 field-verification
18// limits -- all DATA-DRIVEN (CLAUDE.md #11), overridable per unit:
19// UNDERCHARGE / leak : high superheat + NOT high subcooling
20// LIQUID_RESTRICTION : high superheat AND high subcooling (the tell
21// that distinguishes it from undercharge)
22// OVERCHARGE : high subcooling, superheat not high
23// FOULED_CONDENSER : condensing temp far above ambient
24// LOW_AIRFLOW / dirty filter: evaporator air split too high, or CFM/ton < 350
25// FROZEN_COIL : suction saturation temp below 0 C (freeze)
26// SHORT_CYCLING : cycles/hr over the ceiling (APAR Rule 28 analog)
27// Precedence = safety/specificity first (freeze -> restriction -> charge ->
28// coil -> airflow -> cycling). Absent channels are skipped -> an air-side-only
29// kit still gets airflow/cycling faults; a full kit gets charge/coil faults.
30//
31// Title 24 default thresholds (verified): superheat in-band 4-25 F, TXV normal
32// ~15 F; subcooling within +/-5 F of manufacturer target; airflow >= 350
33// CFM/ton. NIST charge-fault sensitivity: features depart from no-fault at a
34// 10% charge deficit. Datasets to benchmark against (external-comp axis-4):
35// NIST residential HP/AC FDD data, ASHRAE RP-1043 chiller, LBNL FDD (DOI
36// 10.25984/1881324).
37//
38// NO-FLOAT integer (milli-degC; 1 F = 556 mC). NO syscalls (deterministic).
39// NEVER-BRICK #26: read-only advisory verdict; no device/firmware write.
40//
41// genealogy_id: project-hvac-efficiency-sclass-2026-06-23 (R4 FDD verdict)
42// + NISTIR 7350 / NISTIR 6964 (APAR) fault signatures
43// + project-nishi-sensor-gap-census-2026-07-14 (external-comp datasets)
44// license_tier: ORIGINAL
45//
46// nx_capability_claims:
47// needs: [pointer_arithmetic]
48// provides: [refrigerant_charge_fdd, undercharge_detect, overcharge_detect,
49// liquid_restriction_detect, fouled_condenser_detect,
50// low_airflow_detect, frozen_coil_detect, short_cycle_detect,
51// title24_grounded_thresholds]
52// safety: [no_floating_point, no_syscall, bounded_iteration,
53// read_only_no_device_write, sealed_enum_verdict,
54// data_driven_thresholds, absent_channel_skipped]
55// verdict: [sealed_enum_fault, no_silent_failure, verdict_tracks_inputs]
56// license: ORIGINAL
57// kind: iot_runtime_primitive
58// sss: [S0 (bit-equal), S6 (no cloud), S7 (sealed verdict)]
59
60// ---- channel "not present" sentinel --------------------------------
61const NX_FDD_ABSENT: i64 = -1000000
62
63// ---- sealed-enum fault verdict -------------------------------------
64const NX_FDD_INSUFFICIENT_DATA: i64 = 0
65const NX_FDD_NOMINAL: i64 = 1
66const NX_FDD_UNDERCHARGE: i64 = 2 // high SH + not-high SC (leak/undercharge)
67const NX_FDD_LIQUID_RESTRICTION: i64 = 3 // high SH + high SC
68const NX_FDD_OVERCHARGE: i64 = 4 // high SC + not-high SH
69const NX_FDD_FOULED_CONDENSER: i64 = 5 // condensing far above ambient
70const NX_FDD_LOW_AIRFLOW: i64 = 6 // high evap split OR CFM/ton < floor
71const NX_FDD_FROZEN_COIL: i64 = 7 // suction saturation below freezing
72const NX_FDD_SHORT_CYCLING: i64 = 8 // cycles/hr over ceiling
73const NX_FDD_BAD_ARG: i64 = 9
74const NX_FDD_N: i64 = 10
75
76func nx_fdd_verdict_is_valid(v: i64) -> i64 {
77 if v < 0 { return 0 }
78 if v >= NX_FDD_N { return 0 }
79 return 1
80}
81
82// ---- derived-quantity inputs (ABSENT where not measured) -----------
83struct FddInput {
84 superheat_mC: i64, // suction - evaporator saturation
85 subcool_mC: i64, // condenser saturation - liquid line
86 suction_sat_mC: i64, // evaporator saturation temp (freeze check)
87 evap_split_mC: i64, // return air - supply air
88 cond_over_ambient_mC: i64, // condenser saturation - outdoor ambient
89 cfm_per_ton: i64, // measured airflow per ton of capacity
90 cph_x10: i64, // cycles per hour x10
91}
92
93// ---- data-driven thresholds (Title 24 / NIST defaults) -------------
94struct FddThresh {
95 target_superheat_mC: i64, // NX_MAGIC_8333 (15 F) TXV normal
96 superheat_band_mC: i64, // NX_MAGIC_2778 (5 F)
97 target_subcool_mC: i64, // NX_MAGIC_5556 (10 F) typical target
98 subcool_band_mC: i64, // NX_MAGIC_2778 (5 F) Title 24
99 split_ceiling_mC: i64, // NX_MAGIC_13889 (25 F) low-airflow trip
100 cond_over_ambient_ceiling_mC: i64, // NX_MAGIC_25000 (~45 F) fouled trip
101 cfm_per_ton_floor: i64, // 350 (Title 24)
102 max_cph_x10: i64, // 35 (3.5/hr)
103 freeze_suction_sat_mC: i64, // 0 (0 C)
104}
105
106// ---- output: verdict + charge flags (telemetry) --------------------
107struct FddOut {
108 verdict: i64,
109 sh_high: i64,
110 sh_low: i64,
111 sc_high: i64,
112 sc_low: i64,
113}
114
115func nx_ac_fdd_analyze(rd: *FddInput, th: *FddThresh, out: *FddOut) -> i64 {
116 out.verdict = NX_FDD_INSUFFICIENT_DATA
117 out.sh_high = 0
118 out.sh_low = 0
119 out.sc_high = 0
120 out.sc_low = 0
121
122 // require at least one evidence channel
123 var have: i64 = 0
124 if rd.superheat_mC != NX_FDD_ABSENT { have = have + 1 }
125 if rd.subcool_mC != NX_FDD_ABSENT { have = have + 1 }
126 if rd.suction_sat_mC != NX_FDD_ABSENT { have = have + 1 }
127 if rd.evap_split_mC != NX_FDD_ABSENT { have = have + 1 }
128 if rd.cond_over_ambient_mC != NX_FDD_ABSENT { have = have + 1 }
129 if rd.cfm_per_ton != NX_FDD_ABSENT { have = have + 1 }
130 if rd.cph_x10 != NX_FDD_ABSENT { have = have + 1 }
131 if have == 0 {
132 out.verdict = NX_FDD_INSUFFICIENT_DATA
133 return out.verdict
134 }
135
136 // ---- charge flags --------------------------------------------
137 if rd.superheat_mC != NX_FDD_ABSENT {
138 if rd.superheat_mC > (th.target_superheat_mC + th.superheat_band_mC) { out.sh_high = 1 }
139 if rd.superheat_mC < (th.target_superheat_mC - th.superheat_band_mC) { out.sh_low = 1 }
140 }
141 if rd.subcool_mC != NX_FDD_ABSENT {
142 if rd.subcool_mC > (th.target_subcool_mC + th.subcool_band_mC) { out.sc_high = 1 }
143 if rd.subcool_mC < (th.target_subcool_mC - th.subcool_band_mC) { out.sc_low = 1 }
144 }
145
146 // ---- precedence: safety / specificity first ------------------
147 // 1. frozen evaporator (suction saturation below freezing)
148 if rd.suction_sat_mC != NX_FDD_ABSENT {
149 if rd.suction_sat_mC < th.freeze_suction_sat_mC {
150 out.verdict = NX_FDD_FROZEN_COIL
151 return out.verdict
152 }
153 }
154 // 2. liquid-line restriction: high superheat AND high subcooling
155 if out.sh_high == 1 {
156 if out.sc_high == 1 {
157 out.verdict = NX_FDD_LIQUID_RESTRICTION
158 return out.verdict
159 }
160 }
161 // 3. undercharge / leak: high superheat, subcooling not high
162 if out.sh_high == 1 {
163 out.verdict = NX_FDD_UNDERCHARGE
164 return out.verdict
165 }
166 // 4. overcharge: high subcooling, superheat not high
167 if out.sc_high == 1 {
168 out.verdict = NX_FDD_OVERCHARGE
169 return out.verdict
170 }
171 // 5. fouled condenser: condensing far above ambient
172 if rd.cond_over_ambient_mC != NX_FDD_ABSENT {
173 if rd.cond_over_ambient_mC > th.cond_over_ambient_ceiling_mC {
174 out.verdict = NX_FDD_FOULED_CONDENSER
175 return out.verdict
176 }
177 }
178 // 6. low airflow: high evaporator split OR CFM/ton below floor
179 var low_air: i64 = 0
180 if rd.evap_split_mC != NX_FDD_ABSENT {
181 if rd.evap_split_mC > th.split_ceiling_mC { low_air = 1 }
182 }
183 if rd.cfm_per_ton != NX_FDD_ABSENT {
184 if rd.cfm_per_ton < th.cfm_per_ton_floor { low_air = 1 }
185 }
186 if low_air == 1 {
187 out.verdict = NX_FDD_LOW_AIRFLOW
188 return out.verdict
189 }
190 // 7. short cycling
191 if rd.cph_x10 != NX_FDD_ABSENT {
192 if rd.cph_x10 > th.max_cph_x10 {
193 out.verdict = NX_FDD_SHORT_CYCLING
194 return out.verdict
195 }
196 }
197
198 out.verdict = NX_FDD_NOMINAL
199 return out.verdict
200}