code wiki / (root) / nx_ac_fdd.nx

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}