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}