code wiki / (root) / nx_stability_rh_test.nx

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}