code wiki / (root) / nx_stability_test.nx

nx_stability_test.nx source

↩ module page · 246 lines · 13763 B

1// nx_stability_test.nx -- gate for the ICH stability rung. 2// 3// THE LIAR-KILL IS T1/T2: the activation energy and Q10 are RECOVERED from 4// ICH's own statement that 6 months at 40 C supports 24 months at 25 C, and 5// then checked against published bands (Q10 2-3, Ea 60-100 kJ/mol) that were 6// never inputs to the derivation. A wrong kinetic model would land outside 7// them. T3/T4 are the MKT pair: a constant history must return its own 8// temperature (no bias), and a cycling history must land ABOVE its 9// arithmetic mean -- with T5 measuring the convexity precondition that 10// makes that true rather than assuming it. 11// expect_exit: 0 license_tier: ORIGINAL 12import "nx_syscalls.nx" 13import "nx_pow10.nx" 14import "nx_stability.nx" 15import "nx_shelf_life.nx" 16 17func 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 } 18func 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 } 19 20func main() -> i64 { 21 var pass: i64 = 0 22 var total: i64 = 0 23 var ea: i64 = 0 24 var q10: i64 = 0 25 var i: i64 = 0 26 27 ea = stab_ich_ea_j() 28 q10 = stab_ich_q10_q3() 29 30 // --- T1 Ea RECOVERED from the regulation lands in the published band --- 31 total = total + 1 32 t_puts("T1 Ea recovered from ICH equivalence = " as *u8); t_putn(ea) 33 t_puts(" J/mol, published band 60000..100000: " as *u8) 34 if stab_recovered_ea_in_published_band() == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 35 36 // --- T2 Q10 recovered from the SAME equivalence, published band 2.0-3.0 --- 37 total = total + 1 38 t_puts("T2 Q10 recovered = " as *u8); t_putn(q10) 39 t_puts(" (Q3), published band 2000..3000: " as *u8) 40 if stab_recovered_q10_in_published_band() == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 41 42 // --- T3 ROUND TRIP: the recovered Q10 must reproduce the regulation's 43 // own acceleration factor of 4x over the 15 C it was derived from. 44 // If the recovery were wrong this would not come back to 4000. --- 45 total = total + 1 46 let af: i64 = stab_accel_factor_q3(q10, 15) 47 t_puts("T3 accel factor at dT=15 from recovered Q10 = " as *u8); t_putn(af) 48 t_puts(" want 4000 +/-120 (ICH's own 24mo/6mo): " as *u8) 49 var ok3: i64 = 1 50 if af < 3880 { ok3 = 0 } 51 if af > 4120 { ok3 = 0 } 52 if ok3 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 53 54 // --- T4 MKT of a CONSTANT history returns that same temperature. 55 // An identity test: any bias in the log-sum-exp shows up here. --- 56 total = total + 1 57 let tc: *i64 = sys_mmap(8 * 8) 58 let hc: *i64 = sys_mmap(8 * 8) 59 tc[0] = 25; tc[1] = 25; tc[2] = 25 60 hc[0] = 100; hc[1] = 200; hc[2] = 300 61 let mkt_const: i64 = stab_mkt_centi_kelvin(tc, hc, 3, ea) 62 t_puts("T4 MKT of constant 25C history = " as *u8); t_putn(mkt_const) 63 t_puts(" centi-K, want 29815 +/-10: " as *u8) 64 var ok4: i64 = 1 65 if mkt_const < 29805 { ok4 = 0 } 66 if mkt_const > 29825 { ok4 = 0 } 67 if ok4 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 68 69 // --- T5 ***THE FINDING*** A history cycling 5C/45C has an arithmetic 70 // mean of exactly 25 C, but degrades like something much hotter. 71 // The naive mean UNDER-states degradation: the dangerous direction. --- 72 total = total + 1 73 tc[0] = 5; tc[1] = 45 74 hc[0] = 500; hc[1] = 500 75 let mkt_cyc: i64 = stab_mkt_centi_kelvin(tc, hc, 2, ea) 76 let mean_cyc: i64 = stab_mean_centi_kelvin(tc, hc, 2) 77 t_puts("T5 5C/45C equal-time: arithmetic mean = " as *u8); t_putn(mean_cyc) 78 t_puts(" centi-K, MKT = " as *u8); t_putn(mkt_cyc) 79 t_puts(" centi-K, gap = " as *u8); t_putn(mkt_cyc - mean_cyc) 80 t_puts(" centi-K; MKT must EXCEED the mean: " as *u8) 81 var ok5: i64 = 1 82 if mean_cyc != 29815 { ok5 = 0 } 83 if mkt_cyc <= mean_cyc { ok5 = 0 } 84 if mkt_cyc - mean_cyc < 500 { ok5 = 0 } 85 if ok5 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 86 87 // --- T6 The PRECONDITION of T5, measured not assumed. MKT > mean is a 88 // Jensen consequence of the Arrhenius term being convex, which 89 // holds iff Ea/(R*T) > 2. Measure it instead of trusting it. --- 90 total = total + 1 91 let cvx: i64 = stab_convexity_ratio_milli(ea, stab_c_to_centi_kelvin(25)) 92 t_puts("T6 convexity ratio Ea/(R*T) at 25C = " as *u8); t_putn(cvx) 93 t_puts(" milli, convex iff > 2000: " as *u8) 94 if cvx > 2000 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 95 96 // --- T7 MKT can never exceed the hottest segment (physical bound). --- 97 total = total + 1 98 t_puts("T7 MKT " as *u8); t_putn(mkt_cyc); t_puts(" <= hottest segment 45C=31815: " as *u8) 99 if mkt_cyc <= 31815 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 100 101 // --- T8 NEGATIVE CONTROL for T5: a history that is genuinely constant 102 // has NO gap. Proves the gap in T5 comes from the variation and 103 // is not an artefact the function adds to every input. --- 104 total = total + 1 105 tc[0] = 25; tc[1] = 25 106 hc[0] = 500; hc[1] = 500 107 let mkt_flat: i64 = stab_mkt_centi_kelvin(tc, hc, 2, ea) 108 let mean_flat: i64 = stab_mean_centi_kelvin(tc, hc, 2) 109 t_puts("T8 flat history gap = " as *u8); t_putn(mkt_flat - mean_flat) 110 t_puts(" centi-K, want |gap| <= 5: " as *u8) 111 var ok8: i64 = 1 112 if mkt_flat - mean_flat > 5 { ok8 = 0 } 113 if mean_flat - mkt_flat > 5 { ok8 = 0 } 114 if ok8 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 115 116 // --- T9 Potency decay: full at t=0, crosses the ICH significant-change 117 // line exactly at the shelf life, and is monotone in between. --- 118 total = total + 1 119 let r0: i64 = stab_potency_retained_permil(0, 24, 5) 120 let r12: i64 = stab_potency_retained_permil(12, 24, 5) 121 let r24: i64 = stab_potency_retained_permil(24, 24, 5) 122 t_puts("T9 retention permil t=0/12/24mo: " as *u8); t_putn(r0); t_puts("/" as *u8); t_putn(r12); t_puts("/" as *u8); t_putn(r24) 123 t_puts(" monotone, r0=1000, r24<=950: " as *u8) 124 var ok9: i64 = 1 125 if r0 != 1000 { ok9 = 0 } 126 if r12 >= r0 { ok9 = 0 } 127 if r24 >= r12 { ok9 = 0 } 128 if r24 > 950 { ok9 = 0 } 129 if ok9 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 130 131 // --- T10 The significant-change flag agrees with the retention curve, 132 // and DISCRIMINATES: not tripped at half life, tripped at full. --- 133 total = total + 1 134 t_puts("T10 significant change at 12mo=" as *u8); t_putn(stab_is_significant_change(r12)) 135 t_puts(" at 24mo=" as *u8); t_putn(stab_is_significant_change(r24)); t_puts(" want 0 then 1: " as *u8) 136 var ok10: i64 = 1 137 if stab_is_significant_change(r12) != 0 { ok10 = 0 } 138 if stab_is_significant_change(r24) != 1 { ok10 = 0 } 139 if ok10 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 140 141 // --- T11 Overage: the top-up needed so label claim still holds at expiry. 142 // Must be strictly positive and round-trip back over the line. --- 143 total = total + 1 144 let ov: i64 = stab_overage_permil(r24) 145 t_puts("T11 overage needed = " as *u8); t_putn(ov) 146 t_puts(" permil; (1000+ov)*r24/1000 must reach 1000: " as *u8) 147 var ok11: i64 = 1 148 if ov <= 0 { ok11 = 0 } 149 if (1000 + ov) * r24 / 1000 < 1000 { ok11 = 0 } 150 if ok11 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 151 152 // --- T12 FAIL-CLOSED. An equivalence pair that is not an acceleration 153 // (hot condition lasting LONGER) says nothing about degradation; 154 // a returned number there would be pure noise wearing a lab coat. --- 155 total = total + 1 156 var ok12: i64 = 1 157 if stab_q10_from_equivalence_q3(25, 6, 40, 24) != STAB_INVALID { ok12 = 0 } 158 if stab_q10_from_equivalence_q3(40, 24, 25, 6) != STAB_INVALID { ok12 = 0 } 159 if stab_ea_from_equivalence(25, 24, 40, 0) != STAB_INVALID { ok12 = 0 } 160 if stab_mkt_centi_kelvin(tc, hc, 0, ea) != STAB_INVALID { ok12 = 0 } 161 if stab_potency_retained_permil(12, 0, 5) != STAB_INVALID { ok12 = 0 } 162 t_puts("T12 fail-closed on non-acceleration / zero divisor / empty history: " as *u8) 163 if ok12 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 164 165 // --- T13 HARD LIMIT as a function: accelerated data alone never sets a 166 // shelf life, and ICH Q1E's extrapolation ceiling is computed. --- 167 total = total + 1 168 t_puts("T13 accelerated-alone sets shelf life = " as *u8); t_putn(stab_accelerated_alone_sets_shelf_life()) 169 t_puts(" (must be 0); Q1E ceiling from 6mo = " as *u8); t_putn(stab_max_extrapolated_months(6)) 170 t_puts(" (2x=12), from 18mo = " as *u8); t_putn(stab_max_extrapolated_months(18)); t_puts(" (+12=30): " as *u8) 171 var ok13: i64 = 1 172 if stab_accelerated_alone_sets_shelf_life() != 0 { ok13 = 0 } 173 if stab_max_extrapolated_months(6) != 12 { ok13 = 0 } 174 if stab_max_extrapolated_months(18) != 30 { ok13 = 0 } 175 if ok13 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 176 177 // --- T14 CROSS-ORGAN, AND NON-VACUOUS. The interpolated shelf-life path 178 // is called DIRECTLY (not through the dispatcher, which would 179 // short-circuit to the exact path and test it twice) at decade 180 // steps where the exact integer answer is independently known. 181 // Measures the agreement instead of asserting it. --- 182 total = total + 1 183 let exact_warm: i64 = sl_at_temp(100, 4, 2, 14) // exact: 50 184 let exact_cold: i64 = sl_at_temp(100, 4, 2, 0 - 6) // exact: 200 185 let frac_warm: i64 = 100 * sl_scale_frac_q3(2000, 10) / 1000 186 let frac_cold: i64 = 100 * sl_scale_frac_q3(2000, 0 - 10) / 1000 187 t_puts("T14 warmer exact=" as *u8); t_putn(exact_warm); t_puts(" interp=" as *u8); t_putn(frac_warm) 188 t_puts(" colder exact=" as *u8); t_putn(exact_cold); t_puts(" interp=" as *u8); t_putn(frac_cold) 189 t_puts(" agree within 1%: " as *u8) 190 var ok14: i64 = 1 191 if exact_warm - frac_warm > 1 { ok14 = 0 } 192 if frac_warm - exact_warm > 1 { ok14 = 0 } 193 if exact_cold - frac_cold > 2 { ok14 = 0 } 194 if frac_cold - exact_cold > 2 { ok14 = 0 } 195 if ok14 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 196 197 // --- T15 THE GAP THAT MOTIVATED THE RUNG: the exact path REFUSES the 198 // 25C->40C comparison the whole category rests on; the new path 199 // answers it, and the answer matches the ICH acceleration factor. --- 200 total = total + 1 201 let refused: i64 = sl_at_temp(24, 25, 2, 40) 202 let answered: i64 = sl_at_temp_frac(24000, 25, q10, 40) 203 t_puts("T15 decade-only path at 25C->40C = " as *u8); t_putn(refused) 204 t_puts(" (refuses); fractional path: 24000 -> " as *u8); t_putn(answered) 205 t_puts(" want ~6000 (ICH's 24mo->6mo), +/-250: " as *u8) 206 var ok15: i64 = 1 207 if refused != 0 - 1 { ok15 = 0 } 208 if answered < 5750 { ok15 = 0 } 209 if answered > 6250 { ok15 = 0 } 210 if ok15 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 211 212 // --- T16 ***THE HONESTY RUNG*** ICH's two conditions differ in humidity 213 // (60% vs 75% RH) as well as temperature, so the recovered figure 214 // is a LUMPED effective Ea, not a pure thermal one. The file must 215 // SAY so as a function, and the reason must be derived from the 216 // RH constants rather than hard-coded -- if the two humidities 217 // were ever equal, the lumping claim would have to go away by 218 // itself instead of persisting as a stale assertion. --- 219 total = total + 1 220 var ok16: i64 = 1 221 if stab_ea_is_lumped_with_humidity() != 1 { ok16 = 0 } 222 if stab_conditions_needed_to_separate() != 3 { ok16 = 0 } 223 if STAB_ICH_ACCEL_RH_PCT == STAB_ICH_LONG_RH_PCT { ok16 = 0 } 224 t_puts("T16 recovered Ea declared LUMPED (ICH " as *u8); t_putn(STAB_ICH_LONG_RH_PCT) 225 t_puts("% vs " as *u8); t_putn(STAB_ICH_ACCEL_RH_PCT) 226 t_puts("% RH), separation needs " as *u8); t_putn(stab_conditions_needed_to_separate()) 227 t_puts(" conditions: " as *u8) 228 if ok16 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 229 230 // --- T17 The on-path guard DISCRIMINATES rather than blanket-approving: 231 // inside the recovered humidity band it blesses the projection, 232 // outside it refuses in BOTH directions (drier and wetter). --- 233 total = total + 1 234 var ok17: i64 = 1 235 if stab_projection_on_recovered_path(60) != 1 { ok17 = 0 } 236 if stab_projection_on_recovered_path(75) != 1 { ok17 = 0 } 237 if stab_projection_on_recovered_path(65) != 1 { ok17 = 0 } 238 if stab_projection_on_recovered_path(20) != 0 { ok17 = 0 } 239 if stab_projection_on_recovered_path(90) != 0 { ok17 = 0 } 240 t_puts("T17 on-path guard: 60/75/65%RH blessed, 20% and 90% refused: " as *u8) 241 if ok17 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } 242 243 t_puts("STABILITY-GATE passed " as *u8); t_putn(pass); t_puts("/" as *u8); t_putn(total) 244 if pass == total { t_puts(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 } 245 t_puts(" verdict=RED\n" as *u8); sys_exit(1); return 1 246}