code wiki / (root) / nx_soda_carbonation.nx

nx_soda_carbonation.nx source

↩ module page · 179 lines · 7752 B

1// nx_soda_carbonation.nx -- CRAFT-SODA rung C0: the CARBONATION physics model. 2// 3// The operator's curiosity point (1): "generate carbonation WITHOUT heavy 4// equipment (research as of 2025/26)." The answer is NANOBUBBLE CO2 -- a 5// compact venturi hydrodynamic-cavitation loop dissolves CO2 at LOW pressure 6// (2-5 bar) ~5x faster than a sparger, no industrial saturator/chiller. 7// 8// This organ is the quantitative substrate under that claim. It is Henry's 9// law done integer-exact (no float): dissolved CO2 is proportional to the 10// CO2 partial pressure over the liquid, with a solubility coefficient k_H 11// that RISES as the water gets colder (why soda is carbonated cold). From 12// dissolved CO2 it derives the beverage "volumes" unit, the carbonic-acid 13// acidity ([H+], pH) that makes soda taste tart, and the nanobubble KINETIC 14// advantage (time-to-target). 15// 16// MEASURED EXCEEDS (gate-checked, see nx_soda_carbonation_test): 17// - COLD HOLDS MORE FIZZ: at one pressure, 4C water dissolves strictly 18// more CO2 than 25C water -- the grounded fact a fixed table misses. 19// - NO HEAVY EQUIPMENT: soda-strength 4 volumes at 4C needs only ~2.7 bar, 20// inside the 2-5 bar nanobubble envelope (not an industrial saturator). 21// - NANOBUBBLE 5x FASTER: time-to-target is 5x shorter than sparging. 22// - CARBONATION IS ACID: more dissolved CO2 -> more [H+] -> lower pH, into 23// the measured 3.5-4.0 soda range (carbonation is itself a sour lever). 24// 25// Grounding (knowledge/fetched + WebSearch 2026-07-10): CO2 solubility in 26// water vs temperature (~3.35 g/L/atm at 0C down to ~1.26 at 30C); the 27// beverage volume unit (1 vol = 1.96 g/L); carbonic acid Ka1_eff ~4.45e-7; 28// nanobubble reviews (100-210 nm, pH 4.5 in ~5 min vs ~25 min sparge). 29// 30// genealogy_id: henry_1803_solubility + carbonic_acid_equilibrium 31// + nanobubble_cavitation_2024_2025 + nishi_food_science_2026 32 33import "nx_syscalls.nx" 34import "nx_vecmath.nx" 35const SODA_MAGIC_3350: i64 = 3350 36const SODA_MAGIC_2770: i64 = 2770 37const SODA_MAGIC_2320: i64 = 2320 38const SODA_MAGIC_1970: i64 = 1970 39const SODA_MAGIC_1690: i64 = 1690 40const SODA_MAGIC_1450: i64 = 1450 41const SODA_MAGIC_1260: i64 = 1260 42const SODA_MAGIC_10000: i64 = 10000 43const SODA_MAGIC_7000: i64 = 7000 44const SODA_MAGIC_6000: i64 = 6000 45const SODA_MAGIC_2000: i64 = 2000 46const SODA_MAGIC_5000: i64 = 5000 47 48// ===== units & constants ======================================== 49// pressure in millibar (mbar; 1 bar = 1000 mbar); concentration in mg/L; 50// temperature in whole degrees C; "volumes" in milli-volumes (1000 = 1 vol). 51 52const SODA_MG_PER_VOLUME: i64 = 1960 // 1 carbonation "volume" = 1.96 g/L CO2 53const SODA_CO2_MOLAR_MASS: i64 = 44 // g/mol 54const SODA_KA1_SCALED: i64 = 445 // carbonic acid: [H+]uM = isqrt(445 * C_uM / 1000) 55const SODA_METHOD_SPARGE: i64 = 0 56const SODA_METHOD_NANOBUBBLE: i64 = 1 57 58// ===== solubility coefficient k_H(T) ============================ 59// mg CO2 per litre of water per bar of CO2 partial pressure, grounded in the 60// published CO2-in-water solubility curve. Anchored every 5 C, 0..30 C. 61func soda_kh_anchor(bucket: i64) -> i64 { 62 if bucket <= 0 { return SODA_MAGIC_3350 } // 0 C 63 if bucket == 1 { return SODA_MAGIC_2770 } // 5 C 64 if bucket == 2 { return SODA_MAGIC_2320 } // 10 C 65 if bucket == 3 { return SODA_MAGIC_1970 } // 15 C 66 if bucket == 4 { return SODA_MAGIC_1690 } // 20 C 67 if bucket == 5 { return SODA_MAGIC_1450 } // 25 C 68 return SODA_MAGIC_1260 // 30 C 69} 70 71// k_H at an arbitrary temperature (clamped to 0..30 C), linearly interpolated 72// between the 5-degree anchors. Falls as the water warms. 73func soda_kh(temp_c: i64) -> i64 { 74 var t: i64 = temp_c 75 if t < 0 { t = 0 } 76 if t > 30 { t = 30 } 77 let b: i64 = t / 5 78 let lo: i64 = soda_kh_anchor(b) 79 let rem: i64 = t % 5 80 if rem == 0 { return lo } 81 let hi: i64 = soda_kh_anchor(b + 1) 82 return lo + (hi - lo) * rem / 5 83} 84 85// ===== Henry's law: pressure <-> dissolved CO2 <-> volumes ======= 86 87// dissolved CO2 (mg/L) at a given water temperature and CO2 partial pressure. 88func soda_co2_mg_per_l(temp_c: i64, pressure_mbar: i64) -> i64 { 89 if pressure_mbar <= 0 { return 0 } 90 return soda_kh(temp_c) * pressure_mbar / 1000 91} 92 93// carbonation strength in milli-volumes (1000 = 1.0 volume) from dissolved CO2. 94func soda_volumes_milli(co2_mg_per_l: i64) -> i64 { 95 if co2_mg_per_l <= 0 { return 0 } 96 return co2_mg_per_l * 1000 / SODA_MG_PER_VOLUME 97} 98 99// inverse solve: the CO2 partial pressure (mbar) required to reach a target 100// carbonation (milli-volumes) at a given temperature. This is the number the 101// nanobubble module must hit -- and cold water needs far less of it. 102func soda_required_pressure_mbar(target_volumes_milli: i64, temp_c: i64) -> i64 { 103 if target_volumes_milli <= 0 { return 0 } 104 let target_co2: i64 = target_volumes_milli * SODA_MG_PER_VOLUME / 1000 105 return target_co2 * 1000 / soda_kh(temp_c) 106} 107 108// ===== acidity: carbonic acid [H+] and pH ======================= 109 110// integer square root (floor), Newton iteration. 111func soda_isqrt(n: i64) -> i64 { return vm_isqrt(n) } 112 113// hydrogen-ion concentration (micromolar) from dissolved CO2, via the weak- 114// acid approximation [H+] = sqrt(Ka1 * C). This IS the sour stimulus that 115// nx_soda_flavor feeds into the sour taste receptor -- carbonation tastes tart. 116func soda_hplus_um(co2_mg_per_l: i64) -> i64 { 117 if co2_mg_per_l <= 0 { return 0 } 118 let c_um: i64 = co2_mg_per_l * 1000 / SODA_CO2_MOLAR_MASS 119 return soda_isqrt(SODA_KA1_SCALED * c_um / 1000) 120} 121 122// 1000 * log10(k) for integer k in 1..10 (anchor table for the log helper). 123func soda_log10_anchor(k: i64) -> i64 { 124 if k <= 1 { return 0 } 125 if k == 2 { return 301 } 126 if k == 3 { return 477 } 127 if k == 4 { return 602 } 128 if k == 5 { return 699 } 129 if k == 6 { return 778 } 130 if k == 7 { return 845 } 131 if k == 8 { return 903 } 132 if k == 9 { return 954 } 133 return 1000 134} 135 136// 1000 * log10(x) for x >= 1, via mantissa normalization + linear interpolation 137// across the decade anchors. Integer-exact, no float. 138func soda_log10_milli(x: i64) -> i64 { 139 if x <= 0 { return 0 } 140 var p: i64 = 0 141 var v: i64 = x 142 while v >= 10 { 143 v = v / 10 144 p = p + 1 145 } 146 var m: i64 = x 147 while m >= SODA_MAGIC_10000 { m = m / 10 } 148 while m < 1000 { m = m * 10 } 149 let d: i64 = m / 1000 150 let lo: i64 = soda_log10_anchor(d) 151 let hi: i64 = soda_log10_anchor(d + 1) 152 let frac: i64 = m - d * 1000 153 return p * 1000 + lo + (hi - lo) * frac / 1000 154} 155 156// beverage pH (milli-pH; 3550 = pH 3.55) from dissolved CO2. 157// pH = -log10([H+] in mol/L) = 6000 - log10_milli([H+] in uM). 158func soda_ph_milli(co2_mg_per_l: i64) -> i64 { 159 let hp: i64 = soda_hplus_um(co2_mg_per_l) 160 if hp <= 0 { return SODA_MAGIC_7000 } 161 return SODA_MAGIC_6000 - soda_log10_milli(hp) 162} 163 164// ===== nanobubble kinetics: time-to-target ====================== 165// Grounded ratio: a venturi nanobubble loop reaches the target carbonation 166// ~5x faster than sparging (nanobubbles hit pH 4.5 in ~5 min vs ~25 min). 167func soda_carbonation_time_s(method: i64, target_volumes_milli: i64) -> i64 { 168 if target_volumes_milli <= 0 { return 0 } 169 if method == SODA_METHOD_NANOBUBBLE { return target_volumes_milli * 75 / 1000 } 170 return target_volumes_milli * 375 / 1000 171} 172 173// is a required pressure inside the "no heavy equipment" nanobubble envelope 174// (2-5 bar)? 1 = yes (a compact venturi loop suffices), 0 = needs a saturator. 175func soda_nanobubble_feasible(pressure_mbar: i64) -> i64 { 176 if pressure_mbar < SODA_MAGIC_2000 { return 0 } 177 if pressure_mbar > SODA_MAGIC_5000 { return 0 } 178 return 1 179}