code wiki / (root) / nx_ferment_kinetics.nx

nx_ferment_kinetics.nx source

↩ module page · 120 lines · 5534 B

1// nx_ferment_kinetics.nx -- R3 science rung: predictive fermentation model. 2// 3// "All the science": predict a ferment's acidification curve pH(t) from 4// first principles -- culture population growth + temperature-scaled 5// lactic-acid production -- so a recipe can be SIMULATED before a drop 6// of milk is warmed, and so the generative rung (R5) can predict whether 7// a NOVEL ferment will reach a safe pH. 8// 9// THE MODEL (fixed-point, integer-exact, deterministic): 10// 1. Population N(t): logistic growth dN = r*N*(K-N)/K 11// r = r_base * temp_factor(T) -- warmer culture grows faster 12// 2. Acid A(t): produced in proportion to population AND metabolic 13// rate dA = acid_yield * temp_factor(T) * N/K 14// (the lactose -> lactic-acid flux; warmer bacteria metabolise 15// faster, so the temperature effect COMPOUNDS through both N and A) 16// 3. pH(t) = pH0 - A (acid accumulation drops pH from milk's 6.5) 17// 18// TEMPERATURE -> RATE via the biological Q10 coefficient (van't Hoff): 19// rate(T) / rate(T_ref) = Q10 ^ ((T - T_ref)/10) 20// computed as exp( ln(Q10)/10 * (T - T_ref) ) using the canonical 21// nx_exp substrate. Q10 ~ 2.5 for lactic-acid bacteria. 22// 23// NOTE: nx_arrhenius's reciprocal-of-Kelvin form was evaluated and is 24// UNUSABLE here -- 1/T in its Q10(1024) fixed-point rounds a 13 C span 25// to delta 0 (1048576/310426 = 3). The biological-Q10 exponential is 26// both the correct microbiology model for this range AND precision- 27// safe, so we compose nx_exp directly. 28// 29// The measured exceed: a $40 yogurt maker holds 43 C but PREDICTS 30// nothing. This rung predicts the set-time curve and proves, by 31// construction, that warmer ferments (within range) acidify faster -- 32// the quantitative Q10 claim, gate-checked, not asserted. 33// 34// genealogy_id: vant_hoff_1884_q10 + verhulst_1838_logistic 35// + monod_1949_growth + nishi_exp_q10_substrate_2026 36 37import "nx_exp.nx" 38 39const NX_FKIN_K: i64 = 1000000 // carrying capacity (relative pop x1e6) 40const NX_FKIN_N0: i64 = 1000 // inoculum 41const NX_FKIN_R_BASE_MILLI: i64 = 300 // growth rate 0.300/hr at T_ref 42const NX_FKIN_ACID_YIELD_MILLI:i64 = 150 // acid flux at full pop, T_ref (milli-pH/hr) 43const NX_FKIN_T_REF_C: i64 = 30 // reference temperature (C) 44const NX_FKIN_Q10_K_Q10: i64 = 94 // ln(2.5)/10 * 1024 (LAB Q10 ~ 2.5) 45const NX_FKIN_PH0_MILLI: i64 = 6500 // milk pH 6.5 46const NX_FKIN_PH_FLOOR_MILLI: i64 = 3900 // practical ferment floor ~3.9 47const NX_FKIN_SET_PH_MILLI: i64 = 4600 // "set"/safe acidification target 48const NX_FKIN_MAX_HOURS: i64 = 600 // simulation horizon 49const NX_FKIN_Q10_ONE: i64 = 1024 50 51// Temperature -> rate multiplier (Q10 fixed-point, 1024 = 1.0x). 52// At T_ref returns 1024 exactly; warmer > 1024, cooler < 1024. 53func nx_ferment_kinetics_temp_factor_q10(t_c: i64) -> i64 { 54 let dt: i64 = t_c - NX_FKIN_T_REF_C 55 let x_q10: i64 = NX_FKIN_Q10_K_Q10 * dt 56 return nx_exp_q10(x_q10) 57} 58 59// Hours until the ferment reaches the safe set pH (<= 4.6) at the given 60// hold temperature; -1 if it never sets within the horizon (too cold / 61// stalled). This IS the predicted set-time. 62func nx_ferment_kinetics_set_time(t_c: i64) -> i64 { 63 let tf: i64 = nx_ferment_kinetics_temp_factor_q10(t_c) 64 let r_eff: i64 = (NX_FKIN_R_BASE_MILLI * tf) / NX_FKIN_Q10_ONE 65 let acid_rate: i64 = (NX_FKIN_ACID_YIELD_MILLI * tf) / NX_FKIN_Q10_ONE 66 var n: i64 = NX_FKIN_N0 67 var acid: i64 = 0 68 var ph: i64 = NX_FKIN_PH0_MILLI 69 var hour: i64 = 0 70 while hour < NX_FKIN_MAX_HOURS { 71 let grow: i64 = ((r_eff * n / 1000) * (NX_FKIN_K - n)) / NX_FKIN_K 72 n = n + grow 73 if n > NX_FKIN_K { n = NX_FKIN_K } 74 let dacid: i64 = (acid_rate * n) / NX_FKIN_K 75 acid = acid + dacid 76 ph = NX_FKIN_PH0_MILLI - acid 77 if ph < NX_FKIN_PH_FLOOR_MILLI { ph = NX_FKIN_PH_FLOOR_MILLI } 78 hour = hour + 1 79 if ph <= NX_FKIN_SET_PH_MILLI { return hour } 80 } 81 return 0 - 1 82} 83 84// Predicted pH at a given hour at the given hold temperature (milli-pH). 85func nx_ferment_kinetics_ph_at(t_c: i64, target_hour: i64) -> i64 { 86 let tf: i64 = nx_ferment_kinetics_temp_factor_q10(t_c) 87 let r_eff: i64 = (NX_FKIN_R_BASE_MILLI * tf) / NX_FKIN_Q10_ONE 88 let acid_rate: i64 = (NX_FKIN_ACID_YIELD_MILLI * tf) / NX_FKIN_Q10_ONE 89 var n: i64 = NX_FKIN_N0 90 var acid: i64 = 0 91 var ph: i64 = NX_FKIN_PH0_MILLI 92 var hour: i64 = 0 93 while hour < target_hour { 94 let grow: i64 = ((r_eff * n / 1000) * (NX_FKIN_K - n)) / NX_FKIN_K 95 n = n + grow 96 if n > NX_FKIN_K { n = NX_FKIN_K } 97 let dacid: i64 = (acid_rate * n) / NX_FKIN_K 98 acid = acid + dacid 99 ph = NX_FKIN_PH0_MILLI - acid 100 if ph < NX_FKIN_PH_FLOOR_MILLI { ph = NX_FKIN_PH_FLOOR_MILLI } 101 hour = hour + 1 102 } 103 return ph 104} 105 106// Predicted relative population (x1e6) at a given hour -- exposes the 107// growth curve for the planner / generative rung. 108func nx_ferment_kinetics_pop_at(t_c: i64, target_hour: i64) -> i64 { 109 let tf: i64 = nx_ferment_kinetics_temp_factor_q10(t_c) 110 let r_eff: i64 = (NX_FKIN_R_BASE_MILLI * tf) / NX_FKIN_Q10_ONE 111 var n: i64 = NX_FKIN_N0 112 var hour: i64 = 0 113 while hour < target_hour { 114 let grow: i64 = ((r_eff * n / 1000) * (NX_FKIN_K - n)) / NX_FKIN_K 115 n = n + grow 116 if n > NX_FKIN_K { n = NX_FKIN_K } 117 hour = hour + 1 118 } 119 return n 120}