code wiki / (root) / nx_ice_stabiliser.nx

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 }