code wiki / (root) / nx_soda_carbonation.nx

nx_soda_carbonation.nx source

↩ module page · 187 lines · 7864 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" 34const SODA_MAGIC_3350: i64 = 3350 35const SODA_MAGIC_2770: i64 = 2770 36const SODA_MAGIC_2320: i64 = 2320 37const SODA_MAGIC_1970: i64 = 1970 38const SODA_MAGIC_1690: i64 = 1690 39const SODA_MAGIC_1450: i64 = 1450 40const SODA_MAGIC_1260: i64 = 1260 41const SODA_MAGIC_10000: i64 = 10000 42const SODA_MAGIC_7000: i64 = 7000 43const SODA_MAGIC_6000: i64 = 6000 44const SODA_MAGIC_2000: i64 = 2000 45const SODA_MAGIC_5000: i64 = 5000 46 47// ===== units & constants ======================================== 48// pressure in millibar (mbar; 1 bar = 1000 mbar); concentration in mg/L; 49// temperature in whole degrees C; "volumes" in milli-volumes (1000 = 1 vol). 50 51const SODA_MG_PER_VOLUME: i64 = 1960 // 1 carbonation "volume" = 1.96 g/L CO2 52const SODA_CO2_MOLAR_MASS: i64 = 44 // g/mol 53const SODA_KA1_SCALED: i64 = 445 // carbonic acid: [H+]uM = isqrt(445 * C_uM / 1000) 54const SODA_METHOD_SPARGE: i64 = 0 55const SODA_METHOD_NANOBUBBLE: i64 = 1 56 57// ===== solubility coefficient k_H(T) ============================ 58// mg CO2 per litre of water per bar of CO2 partial pressure, grounded in the 59// published CO2-in-water solubility curve. Anchored every 5 C, 0..30 C. 60func soda_kh_anchor(bucket: i64) -> i64 { 61 if bucket <= 0 { return SODA_MAGIC_3350 } // 0 C 62 if bucket == 1 { return SODA_MAGIC_2770 } // 5 C 63 if bucket == 2 { return SODA_MAGIC_2320 } // 10 C 64 if bucket == 3 { return SODA_MAGIC_1970 } // 15 C 65 if bucket == 4 { return SODA_MAGIC_1690 } // 20 C 66 if bucket == 5 { return SODA_MAGIC_1450 } // 25 C 67 return SODA_MAGIC_1260 // 30 C 68} 69 70// k_H at an arbitrary temperature (clamped to 0..30 C), linearly interpolated 71// between the 5-degree anchors. Falls as the water warms. 72func soda_kh(temp_c: i64) -> i64 { 73 var t: i64 = temp_c 74 if t < 0 { t = 0 } 75 if t > 30 { t = 30 } 76 let b: i64 = t / 5 77 let lo: i64 = soda_kh_anchor(b) 78 let rem: i64 = t % 5 79 if rem == 0 { return lo } 80 let hi: i64 = soda_kh_anchor(b + 1) 81 return lo + (hi - lo) * rem / 5 82} 83 84// ===== Henry's law: pressure <-> dissolved CO2 <-> volumes ======= 85 86// dissolved CO2 (mg/L) at a given water temperature and CO2 partial pressure. 87func soda_co2_mg_per_l(temp_c: i64, pressure_mbar: i64) -> i64 { 88 if pressure_mbar <= 0 { return 0 } 89 return soda_kh(temp_c) * pressure_mbar / 1000 90} 91 92// carbonation strength in milli-volumes (1000 = 1.0 volume) from dissolved CO2. 93func soda_volumes_milli(co2_mg_per_l: i64) -> i64 { 94 if co2_mg_per_l <= 0 { return 0 } 95 return co2_mg_per_l * 1000 / SODA_MG_PER_VOLUME 96} 97 98// inverse solve: the CO2 partial pressure (mbar) required to reach a target 99// carbonation (milli-volumes) at a given temperature. This is the number the 100// nanobubble module must hit -- and cold water needs far less of it. 101func soda_required_pressure_mbar(target_volumes_milli: i64, temp_c: i64) -> i64 { 102 if target_volumes_milli <= 0 { return 0 } 103 let target_co2: i64 = target_volumes_milli * SODA_MG_PER_VOLUME / 1000 104 return target_co2 * 1000 / soda_kh(temp_c) 105} 106 107// ===== acidity: carbonic acid [H+] and pH ======================= 108 109// integer square root (floor), Newton iteration. 110func soda_isqrt(n: i64) -> i64 { 111 if n <= 0 { return 0 } 112 var x: i64 = n 113 var y: i64 = (x + 1) / 2 114 while y < x { 115 x = y 116 y = (x + n / x) / 2 117 } 118 return x 119} 120 121// hydrogen-ion concentration (micromolar) from dissolved CO2, via the weak- 122// acid approximation [H+] = sqrt(Ka1 * C). This IS the sour stimulus that 123// nx_soda_flavor feeds into the sour taste receptor -- carbonation tastes tart. 124func soda_hplus_um(co2_mg_per_l: i64) -> i64 { 125 if co2_mg_per_l <= 0 { return 0 } 126 let c_um: i64 = co2_mg_per_l * 1000 / SODA_CO2_MOLAR_MASS 127 return soda_isqrt(SODA_KA1_SCALED * c_um / 1000) 128} 129 130// 1000 * log10(k) for integer k in 1..10 (anchor table for the log helper). 131func soda_log10_anchor(k: i64) -> i64 { 132 if k <= 1 { return 0 } 133 if k == 2 { return 301 } 134 if k == 3 { return 477 } 135 if k == 4 { return 602 } 136 if k == 5 { return 699 } 137 if k == 6 { return 778 } 138 if k == 7 { return 845 } 139 if k == 8 { return 903 } 140 if k == 9 { return 954 } 141 return 1000 142} 143 144// 1000 * log10(x) for x >= 1, via mantissa normalization + linear interpolation 145// across the decade anchors. Integer-exact, no float. 146func soda_log10_milli(x: i64) -> i64 { 147 if x <= 0 { return 0 } 148 var p: i64 = 0 149 var v: i64 = x 150 while v >= 10 { 151 v = v / 10 152 p = p + 1 153 } 154 var m: i64 = x 155 while m >= SODA_MAGIC_10000 { m = m / 10 } 156 while m < 1000 { m = m * 10 } 157 let d: i64 = m / 1000 158 let lo: i64 = soda_log10_anchor(d) 159 let hi: i64 = soda_log10_anchor(d + 1) 160 let frac: i64 = m - d * 1000 161 return p * 1000 + lo + (hi - lo) * frac / 1000 162} 163 164// beverage pH (milli-pH; 3550 = pH 3.55) from dissolved CO2. 165// pH = -log10([H+] in mol/L) = 6000 - log10_milli([H+] in uM). 166func soda_ph_milli(co2_mg_per_l: i64) -> i64 { 167 let hp: i64 = soda_hplus_um(co2_mg_per_l) 168 if hp <= 0 { return SODA_MAGIC_7000 } 169 return SODA_MAGIC_6000 - soda_log10_milli(hp) 170} 171 172// ===== nanobubble kinetics: time-to-target ====================== 173// Grounded ratio: a venturi nanobubble loop reaches the target carbonation 174// ~5x faster than sparging (nanobubbles hit pH 4.5 in ~5 min vs ~25 min). 175func soda_carbonation_time_s(method: i64, target_volumes_milli: i64) -> i64 { 176 if target_volumes_milli <= 0 { return 0 } 177 if method == SODA_METHOD_NANOBUBBLE { return target_volumes_milli * 75 / 1000 } 178 return target_volumes_milli * 375 / 1000 179} 180 181// is a required pressure inside the "no heavy equipment" nanobubble envelope 182// (2-5 bar)? 1 = yes (a compact venturi loop suffices), 0 = needs a saturator. 183func soda_nanobubble_feasible(pressure_mbar: i64) -> i64 { 184 if pressure_mbar < SODA_MAGIC_2000 { return 0 } 185 if pressure_mbar > SODA_MAGIC_5000 { return 0 } 186 return 1 187}