nx_arrhenius.nx source
↩ module page · 229 lines · 10639 B
1// nx_arrhenius.nx -- Arrhenius rate-vs-temperature primitive.
2//
3// license_tier: PUBLIC_DOMAIN_PHYSICS
4// genealogy_id: arrhenius_1889_aktivieringsenergi + van_hoff_1884_kinetics
5// + ellis_roberts_1980_seed_viability_eq_uses_arrhenius_form
6// + nishi_q10_fixed_point_substrate_2026
7//
8// The canonical chemical-kinetics + biological-rate primitive.
9//
10// ⚠INTENDED consumers, NOT current ones. This header used to read "Used by:"
11// and list the four lanes below. As of 2026-07-25 NOTHING in the tree imports
12// this file except its own cross-check gate -- the list was a design intent
13// that read like an inventory of live dependents, which is how a dead
14// primitive keeps looking load-bearing. Corrected to say what is true:
15//
16// - Seed viability decay (Ellis-Roberts 1980 σ-equation) [planned]
17// - Soil microbial respiration rate (Lloyd & Taylor 1994) [planned]
18// - Indoor-greenhouse climate-effect modeling (greenhouse plant
19// growth rates per Q10 biological rule of thumb) [planned]
20// - Reservoir nutrient solution chemistry kinetics [planned]
21// - Any temperature-dependent rate the substrate needs to model
22//
23// The zero-consumer state is also why the reciprocal-precision defect fixed
24// below survived: nothing called it, so nothing noticed it always returned
25// "temperature has no effect". A primitive with no gate AND no callers is
26// not a capability, it is an unverified intention.
27//
28// ===== The equation ===============================================
29//
30// k(T) = A * exp(-Ea / (R * T))
31//
32// where:
33// k(T) = rate constant at absolute temperature T
34// A = pre-exponential factor (frequency factor)
35// Ea = activation energy [J / mol]
36// R = universal gas constant = 8.314 J/(mol*K)
37// T = absolute temperature [Kelvin]
38//
39// In practice the substrate cares about RATE RATIOS more than absolute
40// rate constants:
41//
42// k(T2) / k(T1) = exp(-Ea/R * (1/T2 - 1/T1))
43//
44// This form cancels A (we rarely know it) and is what every applied
45// kinetics paper actually uses. Returns a Q10-fixed-point ratio.
46//
47// ===== Q10 fixed-point convention =================================
48//
49// All scalars in Q10 (1024 = 1.0) per nx_q10.nx substrate convention.
50// Temperatures are Kelvin * Q10_ONE. Activation energy is J/mol * Q10_ONE.
51// Rate ratios are dimensionless, Q10-encoded.
52//
53// Temperature note: the nishi "Q10" fixed-point convention and the
54// biological "Q10 temperature coefficient" (rate doubling per 10°C) are
55// UNRELATED collisions of nomenclature. This file uses both -- the
56// fixed-point Q10 is the representation, the biological Q10 is the
57// rule-of-thumb function below. Read each callsite carefully.
58//
59// ===== Compose-against ===========================================
60// nx_exp.nx (nx_exp_q10) -- the exponential primitive
61// nx_q10.nx (NX_Q10_ONE, etc.) -- fixed-point conventions
62//
63// Per cardinal [[feedback-bits-up-canonical-layer-no-reinventing]]:
64// we do not re-implement exp() inline; we compose against the canonical
65// substrate primitive.
66
67// nx_safety_envelope:
68// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
69// sil_target: SIL1
70// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
71// verdict: NOT_YET_EVALUATED
72
73import "nx_syscalls.nx"
74import "nx_exp.nx"
75
76// === Physical constants in Q10 fixed-point =========================
77
78// Universal gas constant R = 8.314 J/(mol*K).
79// In Q10: 8.314 * 1024 = 8513.5 -> 8514 (rounded).
80const NX_ARRH_R_J_PER_MOL_K_Q10: i64 = 8514
81
82// Q10 fixed-point one (mirrored from nx_q10.nx for clarity at the
83// callsite; the convention is 1024 = 1.0).
84const NX_ARRH_Q10_ONE: i64 = 1024
85
86// Ambient absolute zero offset for Celsius->Kelvin (Q10): 273.15 K.
87// In Q10: 273.15 * 1024 = 279,705.6 -> 279,706.
88const NX_ARRH_KELVIN_OFFSET_Q10: i64 = 279706
89
90// Reference temperatures commonly used in seed/biology literature,
91// in Kelvin * Q10:
92const NX_ARRH_T_FREEZER_Q10: i64 = 261177 // -18°C = 255.15 K (long-term seed storage)
93const NX_ARRH_T_FRIDGE_Q10: i64 = 283802 // 4°C = 277.15 K
94const NX_ARRH_T_ROOM_Q10: i64 = 304586 // 25°C = 298.15 K (lab reference)
95const NX_ARRH_T_WARM_Q10: i64 = 314978 // 35°C = 308.15 K (accelerated-aging tests)
96const NX_ARRH_T_HOT_Q10: i64 = 325370 // 45°C = 318.15 K
97
98// Common activation energies (J/mol * Q10) for biology/chemistry:
99const NX_ARRH_EA_SEED_DECAY_TYPICAL_Q10: i64 = 92160000 // ~90 kJ/mol (orthodox seed)
100const NX_ARRH_EA_SEED_DECAY_OILSEED_Q10: i64 = 107520000 // ~105 kJ/mol (oilseed, lipid)
101const NX_ARRH_EA_SOIL_RESPIRATION_Q10: i64 = 66560000 // ~65 kJ/mol (Lloyd-Taylor)
102
103// === Errors =======================================================
104
105const NX_ARRH_OK: i64 = 0
106const NX_ARRH_ERR_BAD_TEMP: i64 = -1 // T <= 0 K (invalid)
107const NX_ARRH_ERR_BAD_EA: i64 = -2 // Ea < 0 (invalid)
108
109// === Celsius / Fahrenheit conversion helpers ======================
110//
111// Q10 in / Q10 out. Defensive at boundaries per Cardinal 12 -- caller
112// passes Q10-encoded scalar, we don't validate sub-Kelvin values
113// here (caller's job to construct sensible inputs).
114
115func nx_arrh_celsius_to_kelvin_q10(c_q10: i64) -> i64 {
116 return c_q10 + NX_ARRH_KELVIN_OFFSET_Q10
117}
118
119func nx_arrh_kelvin_to_celsius_q10(k_q10: i64) -> i64 {
120 return k_q10 - NX_ARRH_KELVIN_OFFSET_Q10
121}
122
123// === Core primitive: rate ratio between two temperatures ==========
124//
125// Returns k(T2)/k(T1) in Q10 fixed-point.
126//
127// ratio_q10 = exp( (-Ea / R) * (1/T2 - 1/T1) )
128//
129// Practical interpretation:
130// - ratio > NX_Q10_ONE: T2 has higher rate (warmer = faster decay
131// for biological samples; warmer = faster reaction for chemistry)
132// - ratio < NX_Q10_ONE: T2 has lower rate (cold storage works)
133//
134// All arithmetic in Q10. We carefully manage scale across the
135// reciprocal step to avoid precision loss.
136//
137// Errors:
138// - returns NX_Q10_ZERO if either temperature is non-positive
139// - returns NX_Q10_ZERO if Ea is negative
140func nx_arrh_rate_ratio_q10(ea_j_per_mol_q10: i64, t1_kelvin_q10: i64, t2_kelvin_q10: i64) -> i64 {
141 if t1_kelvin_q10 <= 0 { return 0 }
142 if t2_kelvin_q10 <= 0 { return 0 }
143 if ea_j_per_mol_q10 < 0 { return 0 }
144
145 // ⚠⚠FIXED 2026-07-25 -- THIS FUNCTION WAS DEAD AND RETURNED 1.0 FOR EVERY
146 // INPUT. The old code materialised 1/T in Q10 fixed point:
147 // inv_t = (Q10_ONE * Q10_ONE) / t_kelvin_q10
148 // For any ambient temperature 1/T is about 0.0033, which in Q10 is
149 // 0.0033 * 1024 = 3.4, truncating to the INTEGER 3. Both 25 C and 40 C
150 // truncate to the same 3, so their difference was exactly ZERO, x was
151 // zero, and exp(0) = 1.0. Every caller was told temperature has no
152 // effect on rate -- from a file whose comment claimed it "carefully
153 // manages scale across the reciprocal step to avoid precision loss".
154 //
155 // THE FIX IS THE LAW nx_thermal_process ALREADY BANKED: in fixed point,
156 // divide by the large number, never materialise a small one. 1/T is
157 // never formed at all; the algebraically identical difference-over-product
158 // is used instead, where every intermediate is large:
159 // 1/T2 - 1/T1 == (T1 - T2) / (T1 * T2)
160 // so x = (-Ea/R)(1/T2 - 1/T1) = (Ea/R)(T2 - T1)/(T1 * T2).
161 //
162 // Widest intermediate is coeff * dT, about 1.4e11 for a 100 kJ/mol Ea
163 // over a 15 K step -- seven orders inside the i64 ceiling.
164 // Cross-validated against the independent base-10 implementation in
165 // nx_stability by nx_arrhenius_crosscheck_test, which is what caught this.
166
167 // Ea/R in Q10. Units are Kelvin.
168 let coeff_q10: i64 = (ea_j_per_mol_q10 * NX_ARRH_Q10_ONE) / NX_ARRH_R_J_PER_MOL_K_Q10
169
170 // (T2 - T1) in Q10, and T1*T2 brought back to Q10 from Q20.
171 let dt_q10: i64 = t2_kelvin_q10 - t1_kelvin_q10
172 let prod_q10: i64 = (t1_kelvin_q10 * t2_kelvin_q10) / NX_ARRH_Q10_ONE
173 if prod_q10 == 0 { return 0 }
174
175 // x = (Ea/R) * (T2-T1) / (T1*T2), in Q10.
176 let x_q10: i64 = (coeff_q10 * dt_q10) / prod_q10
177
178 // exp(x_q10) via the canonical substrate primitive.
179 return nx_exp_q10(x_q10)
180}
181
182// === Convenience: rate ratio between two CELSIUS temperatures =====
183//
184// Wraps the Kelvin core. Callers in biology / seed-storage / soil-temp
185// usually have Celsius; this saves a conversion step at every callsite.
186func nx_arrh_rate_ratio_celsius_q10(ea_j_per_mol_q10: i64, t1_c_q10: i64, t2_c_q10: i64) -> i64 {
187 let t1_k_q10: i64 = nx_arrh_celsius_to_kelvin_q10(t1_c_q10)
188 let t2_k_q10: i64 = nx_arrh_celsius_to_kelvin_q10(t2_c_q10)
189 return nx_arrh_rate_ratio_q10(ea_j_per_mol_q10, t1_k_q10, t2_k_q10)
190}
191
192// === Biological Q10 temperature coefficient ========================
193//
194// The rule of thumb: for every 10°C temperature change, biological
195// rates multiply by Q10. Typical values:
196// Q10 = 2.0 -- classic enzyme kinetics
197// Q10 = 2.5 -- many plant respiration processes (Tjoelker 2001)
198// Q10 = 1.4 -- seed-storage decay (Harrington 1972 rule)
199//
200// Derived from Arrhenius via:
201// Q10 = exp(Ea / R * 10 / (T * (T+10)))
202//
203// For storage temperatures near 25°C (298 K), and Ea = 90 kJ/mol:
204// Q10 ≈ 3.3. For Ea = 60 kJ/mol: Q10 ≈ 2.3.
205//
206// This function returns the Q10 biological coefficient implied by an
207// Ea + reference temperature. Useful for sanity-checking literature.
208func nx_arrh_biological_q10_coefficient(ea_j_per_mol_q10: i64, t_ref_kelvin_q10: i64) -> i64 {
209 // Compute rate_ratio between T_ref and T_ref + 10K.
210 let t_plus10_q10: i64 = t_ref_kelvin_q10 + (10 * NX_ARRH_Q10_ONE)
211 return nx_arrh_rate_ratio_q10(ea_j_per_mol_q10, t_ref_kelvin_q10, t_plus10_q10)
212}
213
214// === Harrington rule-of-thumb shortcut =============================
215//
216// Harrington 1972: "Every 5°C decrease in storage temperature roughly
217// doubles seed longevity (between 0-50°C)." This is the seed-saver
218// rule everyone references. It's an Arrhenius approximation with
219// implied Ea ~80-90 kJ/mol.
220//
221// Returns the longevity-multiplier when moving from t1_c to t2_c
222// (cold storage = LONGER longevity = LARGER multiplier).
223// Longevity multiplier is the INVERSE of the decay-rate ratio:
224// longevity_ratio = 1 / rate_ratio
225// = rate_ratio(T2 -> T1) // swap arguments
226func nx_arrh_harrington_longevity_multiplier_q10(t1_c_q10: i64, t2_c_q10: i64) -> i64 {
227 // Note swapped arguments: longevity at T2 vs T1 = rate at T1 vs T2.
228 return nx_arrh_rate_ratio_celsius_q10(NX_ARRH_EA_SEED_DECAY_TYPICAL_Q10, t2_c_q10, t1_c_q10)
229}