nx_stability_rh_test.nx source
↩ module page · 188 lines · 11111 B
1// nx_stability_rh_test.nx -- gate for the moisture axis and the separation.
2//
3// THE LIAR-KILL IS THE ROUND TRIP (T3/T4): pick a true activation energy and
4// a true moisture sensitivity, GENERATE three lifetimes from them, then hand
5// only those lifetimes to the solver and see whether it recovers the two
6// numbers it was never told. A solver that could not actually separate the
7// axes would come back with something else, or with a pair that merely
8// reproduces the data it was fitted to.
9//
10// THE SECOND CLAIM IS THE REFUSAL (T5/T6): ICH's own design has two
11// conditions, which is one contrast, which cannot resolve two axes. The
12// solver must REFUSE that rather than emit a confident number -- and it must
13// refuse for the structural reason (a singular system), not because someone
14// typed a special case for it.
15// expect_exit: 0 license_tier: ORIGINAL
16
17import "nx_syscalls.nx"
18import "nx_pow10.nx"
19import "nx_stability.nx"
20import "nx_stability_rh.nx"
21
22func 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 }
23func 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 }
24func iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
25
26// The truth the solver is not told. Chosen so the forward model reproduces
27// ICH's own 24-months-to-6-months claim, which is what makes the exercise a
28// realistic decomposition of the regulation rather than a toy.
29const TRUE_E10: i64 = 2340000 // milli-Kelvin -> about 44.8 kJ/mol
30const TRUE_B10: i64 = 15 // milli-decades per %RH
31
32func main() -> i64 {
33 var pass: i64 = 0
34 var total: i64 = 0
35
36 // --- T1 Reciprocal temperature keeps enough resolution to tell two
37 // ambient temperatures apart. This is the exact step that was
38 // dead in nx_arrhenius, so it is checked directly here. ---
39 total = total + 1
40 let u25: i64 = srh_inv_t_e7(25)
41 let u40: i64 = srh_inv_t_e7(40)
42 t_puts("T1 1/T x1e7: 25C=" as *u8); t_putn(u25); t_puts(" 40C=" as *u8); t_putn(u40)
43 t_puts(" difference=" as *u8); t_putn(u25 - u40); t_puts(" (must be >>0): " as *u8)
44 var ok1: i64 = 1
45 if u25 - u40 < 1000 { ok1 = 0 }
46 if u25 <= u40 { ok1 = 0 }
47 if ok1 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
48
49 // --- T2 FORWARD MODEL vs the regulation. With the true pair, 24 months
50 // at 25C/60%RH must come out as ICH's 6 months at 40C/75%RH. ---
51 total = total + 1
52 let m2: i64 = srh_lifetime_months(24, TRUE_E10, TRUE_B10, 25, 60, 40, 75)
53 t_puts("T2 24mo @25C/60RH -> " as *u8); t_putn(m2)
54 t_puts("mo @40C/75RH, ICH says 6: " as *u8)
55 if iabs(m2 - 6) <= 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
56
57 // --- T3 ***THE ROUND TRIP*** Generate the third point, hand all three
58 // lifetimes to the solver, recover both axes blind. ---
59 total = total + 1
60 let m3: i64 = srh_lifetime_months(24, TRUE_E10, TRUE_B10, 25, 60, 40, 25)
61 let out: *i64 = sys_mmap(8 * 4)
62 let rc: i64 = srh_separate(25, 60, 24, 40, 75, m2, 40, 25, m3, out)
63 t_puts("T3 generated 3rd point 40C/25RH = " as *u8); t_putn(m3)
64 t_puts("mo; solver rc=" as *u8); t_putn(rc)
65 t_puts(" recovered E10=" as *u8); t_putn(out[0]); t_puts(" (true " as *u8); t_putn(TRUE_E10)
66 t_puts(") B10=" as *u8); t_putn(out[1]); t_puts(" (true " as *u8); t_putn(TRUE_B10); t_puts("): " as *u8)
67 var ok3: i64 = 1
68 if rc != 0 { ok3 = 0 }
69 if iabs(out[0] - TRUE_E10) * 1000 / TRUE_E10 > 30 { ok3 = 0 }
70 if iabs(out[1] - TRUE_B10) > 2 { ok3 = 0 }
71 if ok3 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
72
73 // --- T4 The recovered activation energy round-trips back through
74 // nx_stability's own E10 conversion, so the two files agree on
75 // what the number means and not merely on its digits. ---
76 total = total + 1
77 let ea_rec: i64 = srh_ea_from_e10(out[0])
78 let back: i64 = stab_e10_milli_kelvin(ea_rec)
79 t_puts("T4 recovered Ea=" as *u8); t_putn(ea_rec)
80 t_puts(" J/mol, back through stab_e10 = " as *u8); t_putn(back)
81 t_puts(" vs " as *u8); t_putn(out[0]); t_puts(": " as *u8)
82 if iabs(back - out[0]) * 1000 / out[0] <= 5 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
83
84 // --- T5 ***THE REFUSAL*** ICH gives two conditions. Presenting them as
85 // three (the second repeated) is exactly the mistake a caller
86 // would make, and the system is singular, so it must REFUSE. ---
87 total = total + 1
88 let rc2: i64 = srh_separate(25, 60, 24, 40, 75, 6, 40, 75, 6, out)
89 t_puts("T5 ICH's two conditions only: rc=" as *u8); t_putn(rc2)
90 t_puts(" want UNDERDETERMINED(-2), and the declared answer is " as *u8)
91 t_putn(srh_ich_pair_alone_is_separable()); t_puts(": " as *u8)
92 var ok5: i64 = 1
93 if rc2 != SRH_UNDERDETERMINED { ok5 = 0 }
94 if srh_ich_pair_alone_is_separable() != 0 { ok5 = 0 }
95 if srh_conditions_required() != 3 { ok5 = 0 }
96 if ok5 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
97
98 // --- T6 The refusal is STRUCTURAL, not a check for repeated arguments.
99 // The two physically meaningful singular designs are the ones a
100 // real lab actually runs by mistake: vary temperature but hold
101 // humidity fixed (no moisture contrast to resolve), or vary
102 // humidity but hold temperature fixed (no thermal contrast).
103 // Both must refuse; a design with both contrasts must not. ---
104 total = total + 1
105 let sep_fixed_rh: i64 = srh_is_separable(25, 60, 40, 60, 55, 60) // temp-only
106 let sep_fixed_t: i64 = srh_is_separable(40, 25, 40, 60, 40, 75) // humidity-only
107 let sep_good: i64 = srh_is_separable(25, 60, 40, 75, 40, 25) // both axes move
108 t_puts("T6 fixed-RH design separable=" as *u8); t_putn(sep_fixed_rh)
109 t_puts(" fixed-T design separable=" as *u8); t_putn(sep_fixed_t)
110 t_puts(" (both want 0), real design=" as *u8); t_putn(sep_good); t_puts(" (want 1): " as *u8)
111 var ok6: i64 = 1
112 if sep_fixed_rh != 0 { ok6 = 0 }
113 if sep_fixed_t != 0 { ok6 = 0 }
114 if sep_good != 1 { ok6 = 0 }
115 if ok6 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
116
117 // --- T11 ***NON-SINGULAR IS NOT THE SAME AS USABLE.*** My own first cut
118 // of T6 assumed that stepping temperature and humidity together in
119 // equal increments was collinear. It is NOT -- 1/T is nonlinear
120 // in T, so that design is technically solvable. The CODE WAS RIGHT
121 // AND MY EXPECTATION WAS WRONG. But it is barely solvable, and a
122 // yes/no separability answer would have handed a caller a
123 // meaningless activation energy with no warning. Conditioning is
124 // the honest measure, and it must rank the two designs apart. ---
125 total = total + 1
126 let cond_weak: i64 = srh_design_conditioning_permil(25, 60, 40, 75, 55, 90)
127 let cond_good: i64 = srh_design_conditioning_permil(25, 60, 40, 75, 40, 25)
128 t_puts("T11 conditioning: ladder design=" as *u8); t_putn(cond_weak)
129 t_puts(" permil (solvable but unusable), real design=" as *u8); t_putn(cond_good)
130 t_puts(" permil: " as *u8)
131 var ok11: i64 = 1
132 if srh_is_separable(25, 60, 40, 75, 55, 90) != 1 { ok11 = 0 } // non-singular
133 if cond_weak >= 100 { ok11 = 0 } // yet unusable
134 if cond_good <= 500 { ok11 = 0 } // real design is strong
135 if srh_design_is_well_conditioned(25, 60, 40, 75, 55, 90) != 0 { ok11 = 0 }
136 if srh_design_is_well_conditioned(25, 60, 40, 75, 40, 25) != 1 { ok11 = 0 }
137 if ok11 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
138
139 // --- T7 ***THE COMMERCIAL POINT*** How much of ICH's acceleration is
140 // actually moisture? If this is a large share then a lumped
141 // parameter misattributes badly, which is why wave 23 flagged it. ---
142 total = total + 1
143 let share: i64 = srh_moisture_share_permil(TRUE_E10, TRUE_B10, 25, 60, 40, 75)
144 t_puts("T7 moisture share of the ICH acceleration = " as *u8); t_putn(share)
145 t_puts(" permil (a large share is why lumping matters): " as *u8)
146 var ok7: i64 = 1
147 if share <= 100 { ok7 = 0 }
148 if share >= 900 { ok7 = 0 }
149 if ok7 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
150
151 // --- T8 THE CONSEQUENCE, STATED AS A NUMBER. nx_stability's lumped
152 // value and the separated thermal value must DIFFER materially.
153 // If they agreed, the wave-23 finding would be pedantic; they do
154 // not, so a lumped Ea used off-path is a real error. ---
155 total = total + 1
156 let lumped: i64 = stab_ich_ea_j()
157 t_puts("T8 lumped Ea=" as *u8); t_putn(lumped); t_puts(" J/mol vs separated thermal Ea=" as *u8)
158 t_putn(ea_rec); t_puts(" J/mol, overstated by " as *u8)
159 t_putn((lumped - ea_rec) * 1000 / ea_rec); t_puts(" permil: " as *u8)
160 var ok8: i64 = 1
161 if lumped <= ea_rec { ok8 = 0 }
162 if (lumped - ea_rec) * 1000 / ea_rec < 200 { ok8 = 0 }
163 if ok8 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
164
165 // --- T9 A dry-but-hot condition can outlive a cool-but-humid one. This
166 // is the counterintuitive result the moisture axis exists to
167 // express, and a temperature-only model cannot produce it. ---
168 total = total + 1
169 t_puts("T9 40C/25RH lifetime=" as *u8); t_putn(m3)
170 t_puts("mo EXCEEDS 25C/60RH lifetime=24mo: " as *u8)
171 if m3 > 24 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
172
173 // --- T10 FAIL-CLOSED on inputs, and the recovered moisture sensitivity
174 // is checked against a published band it was not given. ---
175 total = total + 1
176 var ok10: i64 = 1
177 if srh_separate(25, 60, 0, 40, 75, 6, 40, 25, 34, out) != SRH_INVALID { ok10 = 0 }
178 if srh_lifetime_months(0, TRUE_E10, TRUE_B10, 25, 60, 40, 75) != SRH_INVALID { ok10 = 0 }
179 if srh_b10_in_published_band(TRUE_B10) != 1 { ok10 = 0 }
180 if srh_b10_in_published_band(500) != 0 { ok10 = 0 }
181 t_puts("T10 zero lifetime refused, B10 " as *u8); t_putn(TRUE_B10)
182 t_puts(" inside published 5..25 band, absurd 500 rejected: " as *u8)
183 if ok10 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
184
185 t_puts("STABILITY-RH-GATE passed " as *u8); t_putn(pass); t_puts("/" as *u8); t_putn(total)
186 if pass == total { t_puts(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
187 t_puts(" verdict=RED\n" as *u8); sys_exit(1); return 1
188}