nx_ice_stabiliser.nx source
↩ module page · 347 lines · 14723 B
1// nx_ice_stabiliser.nx -- THE HYDROCOLLOID TERM, and the reason it is much
2// smaller than this lane assumed.
3//
4// ===== WHY THIS EXISTS ============================================
5//
6// nx_ice_recrystal models Ostwald ripening and nx_ice_distribution derives
7// the geometry, but neither knows what a stabiliser does. The gap ledger
8// named this four separate times, each time in the same words: "no
9// stabiliser/hydrocolloid term, and locust bean gum is exactly how industry
10// slows this mechanism".
11//
12// **THAT SENTENCE CONTAINS TWO ERRORS AND THIS ORGAN CORRECTS BOTH.**
13//
14// ERROR 1 -- THE REFERENCE PRODUCT DOES NOT USE LBG. Ben & Jerry's Vanilla
15// declares GUAR (primary) and CARRAGEENAN (secondary). Haagen-Dazs Vanilla
16// declares no stabiliser at all. The lane had been planning to model the
17// competitor's lever using a gum the competitor does not buy.
18//
19// ERROR 2 -- AND THE LARGER ONE: **IN FINISHED ICE CREAM THE EFFECT IS NOT
20// STATISTICALLY SIGNIFICANT.** Flores & Goff 1999 (JDS 82:1408-1415) cycled
21// full ice cream at 0.2% stabiliser and every stabilised sample shared
22// significance letter "a" with the unstabilised control. The mean X50
23// values LOOK like a 15-25% improvement. The study did not find one.
24//
25// ★★★A DIFFERENCE VISIBLE IN A TABLE IS NOT A DIFFERENCE THE STUDY FOUND.
26// This organ therefore stores the Flores means AND the significance verdict
27// together, and stab_flores_difference_is_significant() returns 0 as a
28// FUNCTION, so no downstream planner can read the means alone and price a
29// lever that the evidence does not support.
30//
31// ===== WHERE THE EFFECT IS REAL ===================================
32//
33// In MODEL SOLUTIONS the effect is real, and it is CONDITIONAL ON MILK
34// PROTEIN rather than monotonic in dose. Regand & Goff (Guelph thesis Table
35// 5.2), 5 cycles -3.5/-6 C, recrystallisation rate in um per cycle:
36//
37// no milk solids with milk solids
38// none 4.02 4.59
39// LBG 4.05 3.17
40// carrageenan 3.80 3.02
41// CMC 3.32 2.83
42// xanthan 2.82 3.58
43// alginate 2.77 2.78
44//
45// ★LBG IS INERT WITHOUT MILK PROTEIN (4.05 vs a 4.02 control) AND WORKS WITH
46// IT (3.17 vs 4.59). A dose-response model would have missed that entirely,
47// and would have predicted a benefit in a protein-free sorbet where there is
48// none. Xanthan runs the other way. So the axis is the SYSTEM, not the gum.
49//
50// Two further constraints, both measured, both encoded as refusals:
51// - SATURATION at 0.3% w/w. Rates fall up to that level and no further
52// (Sutton et al. 1996b/1997a; Sutton & Wilcox 1998). Dose past it is
53// cost with no return.
54// - The mechanism is ADSORPTION ONTO THE ICE SURFACE, not gelation and not
55// viscosity -- gelling and non-gelling stabilisers both work. So
56// "thicker mix therefore slower ripening" is not a valid inference.
57//
58// And the timing constraint that decides where to spend effort:
59// - Stabilisers act during STORAGE, not during freezing. After hardening,
60// stabilised and control samples were not significantly different
61// (Regand Table 5.4). A stabiliser cannot buy a finer initial crystal;
62// only the freezer can.
63//
64// ===== CARRAGEENAN IS NOT A THICKENER HERE ========================
65//
66// It is a secondary colloid whose whole job is to stop the PRIMARY gum's own
67// side effect. Casein micelles and polysaccharides are thermodynamically
68// incompatible, so the primary gum drives depletion flocculation and the
69// serum wheys off; carrageenan adsorbs to the micelles and arrests it.
70// Incompatibility ranks xanthan > guar > LBG (Thaiudom & Goff 2003) -- and
71// the reference product uses GUAR, the second-worst, which is exactly why
72// carrageenan is on that label.
73//
74// Its dose is therefore a function of PROTEIN LOAD, not a fixed percentage:
75// minimum casein-micelle : kappa-carrageenan weight ratio 243 (Vega, Andrew
76// & Goff 2004a). Above ~0.03% it gels and above ~0.05% it fails outright.
77//
78// All INTEGER. _q4 = parts per 10,000 (so 0.35% = 35).
79// _q5 = parts per 100,000. _q2 = x100.
80//
81// Grounding (cited):
82// flores_goff_1999_jds_82_1408_stabiliser_null_result
83// regand_goff_guelph_thesis_table_5_2_protein_conditional
84// sutton_wilcox_1998_jfs_63_9_ice_adsorption_saturation
85// thaiudom_goff_2003_incompatibility_ranking
86// vega_andrew_goff_2004a_casein_carrageenan_ratio_243
87// goff_guelph_suggested_mixes_stabiliser_by_fat
88//
89// genealogy_id: frozen_dessert_science + nishi_food_science_suite
90
91import "nx_syscalls.nx"
92const STAB_MAGIC_100000: i64 = 100000
93const STAB_MAGIC_2068: i64 = 2068
94const STAB_MAGIC_1792: i64 = 1792
95const STAB_MAGIC_1724: i64 = 1724
96const STAB_MAGIC_1636: i64 = 1636
97const STAB_MAGIC_2076: i64 = 2076
98const STAB_MAGIC_1745: i64 = 1745
99const STAB_MAGIC_1541: i64 = 1541
100const STAB_MAGIC_1542: i64 = 1542
101const STAB_MAGIC_2979: i64 = 2979
102const STAB_MAGIC_2706: i64 = 2706
103const STAB_MAGIC_2335: i64 = 2335
104const STAB_MAGIC_2520: i64 = 2520
105
106const STAB_INVALID: i64 = 0 - 1
107const STAB_UNKNOWN: i64 = 0 - 2
108
109// ===== Hydrocolloid identities ======================================
110
111const STAB_NONE: i64 = 0
112const STAB_LBG: i64 = 1
113const STAB_GUAR: i64 = 2
114const STAB_CARRAGEENAN: i64 = 3
115const STAB_CMC: i64 = 4
116const STAB_XANTHAN: i64 = 5
117const STAB_ALGINATE: i64 = 6
118const STAB_GELATIN: i64 = 7
119
120// ===== Measured constants ===========================================
121
122// Saturation: 0.3% w/w, past which further stabiliser changes nothing.
123const STAB_SATURATION_Q4: i64 = 30
124
125// Minimum casein-micelle : kappa-carrageenan weight ratio for macroscopic
126// stability. Below this the serum wheys off.
127const STAB_CASEIN_CARRAGEENAN_RATIO: i64 = 243
128
129// Carrageenan gels above ~0.03% and fails outright above ~0.05%.
130const STAB_CARRAGEENAN_GEL_Q4: i64 = 3
131const STAB_CARRAGEENAN_FAIL_Q4: i64 = 5
132
133// Milk protein is 36 permil of MSNF; casein is ~800 permil of milk protein.
134const STAB_CASEIN_OF_PROTEIN_PERMIL: i64 = 800
135
136// ===== Regand Table 5.2: rate in um/cycle x100 ======================
137//
138// ⚠GUAR IS ABSENT FROM THIS TABLE. The protein-conditional dataset does not
139// cover the gum the reference product actually uses, and stab_rate_is_measured
140// says so rather than substituting a neighbour. Gelatin's with-protein cell
141// was illegible in the source scan and is STAB_UNKNOWN, not a guess.
142
143func stab_rate_no_protein_q2(gum: i64) -> i64 {
144 if gum == STAB_NONE { return 402 }
145 if gum == STAB_LBG { return 405 }
146 if gum == STAB_CARRAGEENAN { return 380 }
147 if gum == STAB_CMC { return 332 }
148 if gum == STAB_XANTHAN { return 282 }
149 if gum == STAB_ALGINATE { return 277 }
150 if gum == STAB_GELATIN { return 365 }
151 if gum == STAB_GUAR { return STAB_UNKNOWN }
152 return STAB_INVALID
153}
154
155func stab_rate_with_protein_q2(gum: i64) -> i64 {
156 if gum == STAB_NONE { return 459 }
157 if gum == STAB_LBG { return 317 }
158 if gum == STAB_CARRAGEENAN { return 302 }
159 if gum == STAB_CMC { return 283 }
160 if gum == STAB_XANTHAN { return 358 }
161 if gum == STAB_ALGINATE { return 278 }
162 if gum == STAB_GELATIN { return STAB_UNKNOWN }
163 if gum == STAB_GUAR { return STAB_UNKNOWN }
164 return STAB_INVALID
165}
166
167func stab_rate_q2(gum: i64, has_milk_protein: i64) -> i64 {
168 if has_milk_protein == 1 { return stab_rate_with_protein_q2(gum) }
169 return stab_rate_no_protein_q2(gum)
170}
171
172func stab_rate_is_measured(gum: i64, has_milk_protein: i64) -> i64 {
173 let r: i64 = stab_rate_q2(gum, has_milk_protein)
174 if r == STAB_UNKNOWN { return 0 }
175 if r == STAB_INVALID { return 0 }
176 return 1
177}
178
179// Retardation vs the matching control, in permil. Positive = slower
180// ripening. Uses the control from the SAME protein condition, because the
181// control itself moves (4.02 without protein, 4.59 with).
182func stab_retardation_permil(gum: i64, has_milk_protein: i64) -> i64 {
183 let ctrl: i64 = stab_rate_q2(STAB_NONE, has_milk_protein)
184 let r: i64 = stab_rate_q2(gum, has_milk_protein)
185 if stab_rate_is_measured(gum, has_milk_protein) != 1 { return STAB_UNKNOWN }
186 if ctrl <= 0 { return STAB_INVALID }
187 return 1000 - r * 1000 / ctrl
188}
189
190// ★The headline: a gum whose benefit exists only in the presence of milk
191// protein. Returns 1 when the sign of the effect flips with the system.
192func stab_is_protein_conditional(gum: i64) -> i64 {
193 if stab_rate_is_measured(gum, 0) != 1 { return 0 }
194 if stab_rate_is_measured(gum, 1) != 1 { return 0 }
195 let without: i64 = stab_retardation_permil(gum, 0)
196 let with: i64 = stab_retardation_permil(gum, 1)
197 if without > 20 { return 0 }
198 if with > 100 { return 1 }
199 return 0
200}
201
202// LBG measured inert in a protein-free system: 4.05 against a 4.02 control.
203func stab_lbg_inert_without_protein() -> i64 {
204 let r: i64 = stab_retardation_permil(STAB_LBG, 0)
205 if r <= 0 { return 1 }
206 return 0
207}
208
209// ===== Dose: saturation and waste ===================================
210
211func stab_effective_dose_q4(dose_q4: i64) -> i64 {
212 if dose_q4 < 0 { return STAB_INVALID }
213 if dose_q4 > STAB_SATURATION_Q4 { return STAB_SATURATION_Q4 }
214 return dose_q4
215}
216
217func stab_wasted_dose_q4(dose_q4: i64) -> i64 {
218 if dose_q4 < 0 { return STAB_INVALID }
219 if dose_q4 <= STAB_SATURATION_Q4 { return 0 }
220 return dose_q4 - STAB_SATURATION_Q4
221}
222
223// Goff's suggested mixes scale stabiliser DOWN as fat rises, because total
224// solids already immobilise the water. Parts per 10,000 of mix.
225func stab_recommended_total_q4(fat_permil: i64) -> i64 {
226 if fat_permil < 0 { return STAB_INVALID }
227 if fat_permil < 110 { return 35 }
228 if fat_permil < 120 { return 35 }
229 if fat_permil < 130 { return 30 }
230 if fat_permil < 140 { return 30 }
231 if fat_permil < 150 { return 25 }
232 if fat_permil < 160 { return 20 }
233 return 15
234}
235
236// ===== Carrageenan: dosed off protein, not off a percentage =========
237
238func stab_casein_permil(protein_permil: i64) -> i64 {
239 if protein_permil < 0 { return STAB_INVALID }
240 return protein_permil * STAB_CASEIN_OF_PROTEIN_PERMIL / 1000
241}
242
243// Minimum carrageenan in parts per 100,000, from the ratio-243 rule.
244func stab_min_carrageenan_q5(protein_permil: i64) -> i64 {
245 let casein: i64 = stab_casein_permil(protein_permil)
246 if casein == STAB_INVALID { return STAB_INVALID }
247 if casein <= 0 { return STAB_INVALID }
248 return casein * STAB_MAGIC_100000 / (STAB_CASEIN_CARRAGEENAN_RATIO * 1000)
249}
250
251func stab_carrageenan_dose_ok_q4(dose_q4: i64, protein_permil: i64) -> i64 {
252 let min_q5: i64 = stab_min_carrageenan_q5(protein_permil)
253 if min_q5 == STAB_INVALID { return 0 }
254 if dose_q4 * 10 < min_q5 { return 0 }
255 if dose_q4 >= STAB_CARRAGEENAN_FAIL_Q4 { return 0 }
256 return 1
257}
258
259// Incompatibility with casein: xanthan > guar > LBG. Higher drives more
260// depletion flocculation and therefore more need for a secondary colloid.
261func stab_incompatibility_rank(gum: i64) -> i64 {
262 if gum == STAB_XANTHAN { return 3 }
263 if gum == STAB_GUAR { return 2 }
264 if gum == STAB_LBG { return 1 }
265 if gum == STAB_CMC { return 1 }
266 if gum == STAB_NONE { return 0 }
267 if gum == STAB_CARRAGEENAN { return 0 }
268 return STAB_INVALID
269}
270
271func stab_needs_secondary_colloid(primary_gum: i64) -> i64 {
272 let r: i64 = stab_incompatibility_rank(primary_gum)
273 if r == STAB_INVALID { return 0 }
274 if r >= 1 { return 1 }
275 return 0
276}
277
278// ===== Flores & Goff 1999: the full-product null result ==============
279//
280// X50 in um x100, hardened at -30 C then temperature-cycled.
281
282const STAB_FLORES_MILD: i64 = 0 // -15 +/- 2 C, 3 cycles
283const STAB_FLORES_HARSH: i64 = 1 // -15 +/- 5 C, 3 cycles
284const STAB_FLORES_ABUSED: i64 = 2 // -15 +/- 5 C, 9 cycles
285
286func stab_flores_x50_q2(regime: i64, gum: i64) -> i64 {
287 if regime == STAB_FLORES_MILD {
288 if gum == STAB_NONE { return STAB_MAGIC_2068 }
289 if gum == STAB_CMC { return STAB_MAGIC_1792 }
290 if gum == STAB_GUAR { return STAB_MAGIC_1724 }
291 if gum == STAB_XANTHAN { return STAB_MAGIC_1636 }
292 return STAB_UNKNOWN
293 }
294 if regime == STAB_FLORES_HARSH {
295 if gum == STAB_NONE { return STAB_MAGIC_2076 }
296 if gum == STAB_CMC { return STAB_MAGIC_1745 }
297 if gum == STAB_GUAR { return STAB_MAGIC_1541 }
298 if gum == STAB_XANTHAN { return STAB_MAGIC_1542 }
299 return STAB_UNKNOWN
300 }
301 if regime == STAB_FLORES_ABUSED {
302 if gum == STAB_NONE { return STAB_MAGIC_2979 }
303 if gum == STAB_CMC { return STAB_MAGIC_2706 }
304 if gum == STAB_GUAR { return STAB_MAGIC_2335 }
305 if gum == STAB_XANTHAN { return STAB_MAGIC_2520 }
306 return STAB_UNKNOWN
307 }
308 return STAB_INVALID
309}
310
311// The apparent improvement a reader would take off the means alone.
312func stab_flores_apparent_gain_permil(regime: i64, gum: i64) -> i64 {
313 let ctrl: i64 = stab_flores_x50_q2(regime, STAB_NONE)
314 let v: i64 = stab_flores_x50_q2(regime, gum)
315 if ctrl <= 0 { return STAB_INVALID }
316 if v == STAB_UNKNOWN { return STAB_UNKNOWN }
317 if v == STAB_INVALID { return STAB_INVALID }
318 return 1000 - v * 1000 / ctrl
319}
320
321// ★★★AND THE VERDICT THE STUDY ACTUALLY REACHED. Every value in every column
322// shares significance letter "a": no stabilised sample differed from the
323// control at alpha = 0.05. A function, not a comment, so a planner that
324// reads the means must also read this.
325func stab_flores_difference_is_significant() -> i64 { return 0 }
326
327// ★THE REFUSAL THAT MATTERS. Model-solution retardation is real and
328// finished-product retardation was not demonstrated. Carrying the former
329// into a product claim is the error this organ exists to block.
330func stab_model_result_transfers_to_product() -> i64 { return 0 }
331
332// The reference product's gum has NO protein-conditional data at all, and
333// the only full-product study covering it found no significant effect.
334// So "beat them by changing the gum" is unsupported, and this says so.
335func stab_gum_swap_is_a_supported_lever(gum: i64) -> i64 { return 0 }
336
337// ===== Mechanism and timing =========================================
338
339// Both gelling and non-gelling stabilisers retard recrystallisation, so the
340// mechanism cannot be gelation or bulk viscosity.
341func stab_mechanism_is_ice_adsorption() -> i64 { return 1 }
342func stab_viscosity_explains_retardation() -> i64 { return 0 }
343
344// After hardening, stabilised and control were not significantly different:
345// the stabiliser buys storage life, never a finer initial crystal.
346func stab_acts_on_storage_not_freezing() -> i64 { return 1 }
347func stab_improves_initial_crystal_size() -> i64 { return 0 }