code wiki / (root) / nx_arrhenius_crosscheck_test.nx

nx_arrhenius_crosscheck_test.nx source

↩ module page · 236 lines · 12794 B

1// nx_arrhenius_crosscheck_test.nx -- do the ecosystem's TWO independent 2// Arrhenius implementations agree? 3// 4// ===== WHY THIS EXISTS INSTEAD OF A REFACTOR ===================== 5// 6// nx_arrhenius.nx (seed-storage / soil-respiration lane) and nx_stability.nx 7// (food + pharma stability lane) both compute how much faster a reaction runs 8// when it gets warmer. Two implementations of one physical law is normally 9// drift waiting to happen -- it is exactly what nx_pow10 was extracted to 10// prevent -- so the obvious move is to collapse them into one. 11// 12// THAT WOULD DESTROY EVIDENCE. These two do not merely differ in style: 13// - nx_arrhenius works in the NATURAL base, via nx_exp_q10 14// - nx_stability works in BASE TEN, via ipow10_q3 15// - nx_arrhenius uses Q10 fixed point (1024 = 1.0) 16// - nx_stability uses Q3 fixed point (1000 = 1.0) 17// - the reciprocal-temperature step is scaled completely differently 18// Nothing is shared between them but the physics. So if they AGREE, that 19// agreement is an independent cross-validation of both -- the same reason 20// nx_icecream_balance is gated against nx_icecream rather than merged into 21// it, and the same reason nx_peptide_formula's atom-count path is kept 22// separate from the residue-sum path. Merging turns evidence into a 23// tautology: one implementation can only ever agree with itself. 24// 25// So this gate MEASURES the agreement and keeps both. The DRY debt is 26// answered by proving the copies converge, not by deleting one of them. 27// 28// The comparison: for the same Ea and the same temperature pair, 29// nx_arrhenius: ratio_q10 = exp((-Ea/R)(1/T2 - 1/T1)) [1024 = 1.0] 30// nx_stability: ratio_q3 = 10^(E10/T1 - E10/T2), E10 = Ea/(R ln10) 31// which are the same number in two bases. Rescaled to a common Q3 they must 32// land on top of each other. 33// expect_exit: 0 license_tier: ORIGINAL 34 35import "nx_syscalls.nx" 36import "nx_pow10.nx" 37import "nx_stability.nx" 38import "nx_arrhenius.nx" 39 40func t_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 41func t_putn(v: i64) -> i64 { let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) } let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } sys_write(1, bb, k); return 0 } 42func iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 43 44// nx_stability's rate ratio k(T2)/k(T1), in Q3. Built from the same pieces 45// stab_mkt_centi_kelvin uses, so this exercises the real code path rather 46// than a private re-derivation written to match. 47func stab_rate_ratio_q3(ea_j: i64, t1_c: i64, t2_c: i64) -> i64 { 48 var e10: i64 = 0 49 var a: i64 = 0 50 var b: i64 = 0 51 e10 = stab_e10_milli_kelvin(ea_j) 52 if e10 == STAB_INVALID { return STAB_INVALID } 53 a = stab_e10_over_t_milli(e10, stab_c_to_centi_kelvin(t1_c)) 54 b = stab_e10_over_t_milli(e10, stab_c_to_centi_kelvin(t2_c)) 55 return ipow10_q3(a - b) 56} 57 58// nx_arrhenius's rate ratio for the same inputs, rescaled from Q10 to Q3. 59func arrh_rate_ratio_as_q3(ea_j: i64, t1_c: i64, t2_c: i64) -> i64 { 60 var ea_q10: i64 = 0 61 var t1: i64 = 0 62 var t2: i64 = 0 63 var r: i64 = 0 64 ea_q10 = ea_j * NX_ARRH_Q10_ONE 65 t1 = nx_arrh_celsius_to_kelvin_q10(t1_c * NX_ARRH_Q10_ONE) 66 t2 = nx_arrh_celsius_to_kelvin_q10(t2_c * NX_ARRH_Q10_ONE) 67 r = nx_arrh_rate_ratio_q10(ea_q10, t1, t2) 68 return r * 1000 / NX_ARRH_Q10_ONE 69} 70 71func main() -> i64 { 72 var pass: i64 = 0 73 var total: i64 = 0 74 var ea: i64 = 0 75 ea = stab_ich_ea_j() 76 77 // --- T1 BOTH must reproduce the identity ratio at equal temperatures. 78 // An implementation that drifts at zero has a scale bug. --- 79 total = total + 1 80 let s_id: i64 = stab_rate_ratio_q3(ea, 25, 25) 81 let a_id: i64 = arrh_rate_ratio_as_q3(ea, 25, 25) 82 t_puts("T1 identity T2==T1: stability=" as *u8); t_putn(s_id) 83 t_puts(" arrhenius=" as *u8); t_putn(a_id); t_puts(" both ~1000: " as *u8) 84 var ok1: i64 = 1 85 if iabs(s_id - 1000) > 20 { ok1 = 0 } 86 if iabs(a_id - 1000) > 20 { ok1 = 0 } 87 if ok1 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 88 89 // --- T2 ***THE CROSS-CHECK*** The ICH 25 C -> 40 C step, computed in 90 // two bases by two lanes that share no code. Both should land on 91 // the acceleration factor of about 4 that the regulation implies. --- 92 total = total + 1 93 let s_ich: i64 = stab_rate_ratio_q3(ea, 25, 40) 94 let a_ich: i64 = arrh_rate_ratio_as_q3(ea, 25, 40) 95 let gap: i64 = iabs(s_ich - a_ich) * 1000 / s_ich 96 t_puts("T2 25C->40C: stability=" as *u8); t_putn(s_ich) 97 t_puts(" arrhenius=" as *u8); t_putn(a_ich) 98 t_puts(" disagreement=" as *u8); t_putn(gap); t_puts(" permil (<=50): " as *u8) 99 if gap <= 50 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 100 101 // --- T3 Agreement must hold ACROSS A RANGE, not at one flattering point. 102 // Scans 5 C to 45 C against a 25 C reference and reports the worst. --- 103 total = total + 1 104 var worst: i64 = 0 105 var worst_at: i64 = 0 106 var tc: i64 = 5 107 while tc <= 45 { 108 let sv: i64 = stab_rate_ratio_q3(ea, 25, tc) 109 let av: i64 = arrh_rate_ratio_as_q3(ea, 25, tc) 110 if sv > 0 { 111 let g: i64 = iabs(sv - av) * 1000 / sv 112 if g > worst { worst = g; worst_at = tc } 113 } 114 tc = tc + 5 115 } 116 t_puts("T3 worst disagreement over 5..45C = " as *u8); t_putn(worst) 117 t_puts(" permil at " as *u8); t_putn(worst_at); t_puts("C (<=60): " as *u8) 118 if worst <= 60 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 119 120 // --- T4 NON-VACUITY. If the two paths agreed on everything regardless 121 // of input, the comparison would be measuring nothing. A WRONG Ea 122 // must move both of them together and away from the right answer, 123 // proving the ratio actually depends on the parameter under test. --- 124 total = total + 1 125 let s_wrong: i64 = stab_rate_ratio_q3(ea * 2, 25, 40) 126 let a_wrong: i64 = arrh_rate_ratio_as_q3(ea * 2, 25, 40) 127 t_puts("T4 double-Ea moves BOTH: stability " as *u8); t_putn(s_ich); t_puts("->" as *u8); t_putn(s_wrong) 128 t_puts(", arrhenius " as *u8); t_putn(a_ich); t_puts("->" as *u8); t_putn(a_wrong); t_puts(": " as *u8) 129 var ok4: i64 = 1 130 if s_wrong <= s_ich { ok4 = 0 } 131 if a_wrong <= a_ich { ok4 = 0 } 132 if iabs(s_wrong - a_wrong) * 1000 / s_wrong > 80 { ok4 = 0 } 133 if ok4 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 134 135 // --- T5 DIRECTION. Colder must be slower in both, which is the one 136 // thing a sign error would invert while leaving magnitudes plausible. --- 137 total = total + 1 138 let s_cold: i64 = stab_rate_ratio_q3(ea, 25, 5) 139 let a_cold: i64 = arrh_rate_ratio_as_q3(ea, 25, 5) 140 t_puts("T5 25C->5C both below 1000: stability=" as *u8); t_putn(s_cold) 141 t_puts(" arrhenius=" as *u8); t_putn(a_cold); t_puts(": " as *u8) 142 var ok5: i64 = 1 143 if s_cold >= 1000 { ok5 = 0 } 144 if a_cold >= 1000 { ok5 = 0 } 145 if ok5 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 146 147 // ===== FOUND BY nx_gatequality, NOT BY ME ========================= 148 // 149 // This gate scored 333 permil: it exercised the rate-ratio core and the 150 // Celsius conversion, and left FOUR functions in the same file untouched. 151 // Two of them are published rules of thumb built directly on the function 152 // that was returning 1.0 for every input -- so they have never been 153 // checked, before the fix or after it. Fixing a defect and gating only 154 // the defect is how the neighbours stay broken. 155 156 // --- T6 ***THE HARRINGTON RULE, AGAINST ITS PUBLISHED VALUE.*** 157 // Harrington 1972: every 5 C drop in storage temperature roughly 158 // DOUBLES seed longevity. That is an independent literature 159 // claim, never used as an input here, and the file's own seed-decay 160 // activation energy has to reproduce it. --- 161 total = total + 1 162 let h5: i64 = nx_arrh_harrington_longevity_multiplier_q10(25 * NX_ARRH_Q10_ONE, 20 * NX_ARRH_Q10_ONE) 163 let h5_q3: i64 = h5 * 1000 / NX_ARRH_Q10_ONE 164 t_puts("T6 Harrington: 25C->20C longevity multiplier = " as *u8); t_putn(h5_q3) 165 t_puts(" (published rule says ~2000 = doubles): " as *u8) 166 var ok6: i64 = 1 167 if h5_q3 < 1500 { ok6 = 0 } 168 if h5_q3 > 2600 { ok6 = 0 } 169 if ok6 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 170 171 // --- T7 DIRECTION AND COMPOUNDING of the same rule. Colder must mean 172 // LONGER life, and 10 C must beat 5 C -- the ordering a sign 173 // error would invert while leaving the magnitude plausible. --- 174 total = total + 1 175 let h10: i64 = nx_arrh_harrington_longevity_multiplier_q10(25 * NX_ARRH_Q10_ONE, 15 * NX_ARRH_Q10_ONE) 176 let hwarm: i64 = nx_arrh_harrington_longevity_multiplier_q10(25 * NX_ARRH_Q10_ONE, 30 * NX_ARRH_Q10_ONE) 177 t_puts("T7 10C colder multiplier=" as *u8); t_putn(h10 * 1000 / NX_ARRH_Q10_ONE) 178 t_puts(" (> 5C colder), 5C WARMER multiplier=" as *u8); t_putn(hwarm * 1000 / NX_ARRH_Q10_ONE) 179 t_puts(" (< 1000): " as *u8) 180 var ok7: i64 = 1 181 if h10 <= h5 { ok7 = 0 } 182 if hwarm >= NX_ARRH_Q10_ONE { ok7 = 0 } 183 if ok7 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 184 185 // --- T8 ***THE BIOLOGICAL Q10 COEFFICIENT, AGAINST THE FILE'S OWN 186 // DOCUMENTED VALUE.*** Its header states Q10 is about 3.3 near 187 // 25 C for Ea = 90 kJ/mol and about 2.3 for 60 kJ/mol. Those are 188 // checkable claims, and until now nothing checked them. --- 189 total = total + 1 190 let q90: i64 = nx_arrh_biological_q10_coefficient(NX_ARRH_EA_SEED_DECAY_TYPICAL_Q10, NX_ARRH_T_ROOM_Q10) 191 let q90_q3: i64 = q90 * 1000 / NX_ARRH_Q10_ONE 192 let q60: i64 = nx_arrh_biological_q10_coefficient(61440000, NX_ARRH_T_ROOM_Q10) 193 let q60_q3: i64 = q60 * 1000 / NX_ARRH_Q10_ONE 194 t_puts("T8 biological Q10 at 25C: Ea=90kJ -> " as *u8); t_putn(q90_q3) 195 t_puts(" (header says ~3300), Ea=60kJ -> " as *u8); t_putn(q60_q3) 196 t_puts(" (header says ~2300): " as *u8) 197 var ok8: i64 = 1 198 if q90_q3 < 2900 { ok8 = 0 } 199 if q90_q3 > 3700 { ok8 = 0 } 200 if q60_q3 < 2000 { ok8 = 0 } 201 if q60_q3 > 2600 { ok8 = 0 } 202 if q60 >= q90 { ok8 = 0 } 203 if ok8 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 204 205 // --- T9 The Celsius wrapper agrees with the Kelvin core, and the two 206 // conversions round-trip. Cheap, but these are the functions 207 // every caller actually reaches for. --- 208 total = total + 1 209 let via_c: i64 = nx_arrh_rate_ratio_celsius_q10(ea * NX_ARRH_Q10_ONE, 25 * NX_ARRH_Q10_ONE, 40 * NX_ARRH_Q10_ONE) 210 let via_k: i64 = nx_arrh_rate_ratio_q10(ea * NX_ARRH_Q10_ONE, 211 nx_arrh_celsius_to_kelvin_q10(25 * NX_ARRH_Q10_ONE), 212 nx_arrh_celsius_to_kelvin_q10(40 * NX_ARRH_Q10_ONE)) 213 let back_c: i64 = nx_arrh_kelvin_to_celsius_q10(nx_arrh_celsius_to_kelvin_q10(25 * NX_ARRH_Q10_ONE)) 214 t_puts("T9 celsius wrapper=" as *u8); t_putn(via_c); t_puts(" kelvin core=" as *u8); t_putn(via_k) 215 t_puts(", C->K->C round trip=" as *u8); t_putn(back_c * 1000 / NX_ARRH_Q10_ONE); t_puts(" milli-C: " as *u8) 216 var ok9: i64 = 1 217 if via_c != via_k { ok9 = 0 } 218 if iabs(back_c - 25 * NX_ARRH_Q10_ONE) > 2 { ok9 = 0 } 219 if ok9 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 220 221 // --- T10 FAIL-CLOSED on the whole surface. The rate-ratio core refuses 222 // a non-positive temperature and a negative activation energy; 223 // the wrappers must inherit that rather than each inventing a 224 // fallback. --- 225 total = total + 1 226 var ok10: i64 = 1 227 if nx_arrh_rate_ratio_q10(ea * NX_ARRH_Q10_ONE, 0, NX_ARRH_T_ROOM_Q10) != 0 { ok10 = 0 } 228 if nx_arrh_rate_ratio_q10(ea * NX_ARRH_Q10_ONE, NX_ARRH_T_ROOM_Q10, 0) != 0 { ok10 = 0 } 229 if nx_arrh_rate_ratio_q10(0 - 1, NX_ARRH_T_ROOM_Q10, NX_ARRH_T_WARM_Q10) != 0 { ok10 = 0 } 230 t_puts("T10 zero Kelvin and negative Ea REFUSED across the surface: " as *u8) 231 if ok10 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 232 233 t_puts("ARRHENIUS-CROSSCHECK-GATE passed " as *u8); t_putn(pass); t_puts("/" as *u8); t_putn(total) 234 if pass == total { t_puts(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 } 235 t_puts(" verdict=RED\n" as *u8); sys_exit(1); return 1 236}