code wiki / (root) / nx_arrhenius.nx

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}