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}