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}