nx_icecream.nx
buildroot/runtime/nx_icecream.nx
about
nx_icecream.nx -- FOOD-SCIENCE SUITE / FROZEN DESSERT rung. Computes what
a frozen dessert mix will actually DO -- where it starts to freeze, how
much of its water is ice at any serving temperature, whether it will go
sandy, and whether it is legally ice cream -- from composition and
physical chemistry, instead of trusting a recipe's "churn until thick".
THE CORE IS ONE EQUATION, HONESTLY APPLIED. Freezing-point depression is
colligative: dT = Kf * molality. Everything expensive follows from it:
- As water freezes OUT, the remaining solution CONCENTRATES, so the
freezing point keeps dropping. At equilibrium at temperature T, the
unfrozen water is exactly the amount whose freezing point IS T.
Since depression is inversely proportional to water for fixed solute:
frozen_fraction = 1 - FPD_initial / |T|
That is derived, not tabulated -- ic_frozen_water_permil.
- Inverting it answers the question a recipe cannot: what cabinet
temperature gives the scoopable ~72% ice I want, FOR THIS MIX?
-- ic_serve_temp_for_frozen_permil.
MEASURED against published freezing curves for a typical mix
(FPD ~2.5 C): this model gives 51% frozen at -5 C (lit ~50%), 81% at
-13 C (lit ~80%), 86% at -18 C (lit ~90%). It tracks well through the
scooping range and runs ~4 points LOW in the deep-freeze tail, where
ideal-solution theory stops holding (activity coefficients depart from 1
in a highly concentrated serum). Stated, not hidden.
PAC IS DERIVED, POD CANNOT BE. The gelato trade's anti-freezing power
index (PAC, sucrose = 100) is just a molecular-weight ratio, so this
COMPUTES it -- ic_pac_from_mw reproduces the trade table exactly for the
crystalline sugars (sucrose 100, dextrose 190, fructose 190, lactose 100).
Sweetening power (POD) is psychophysical -- a measured human response, not
a colligative property -- so it stays a cited table. Confusing the two is
the classic formulation error: dextrose has ~1.9x the freezing power of
sucrose but only ~0.7x the sweetness, which is exactly why it is the lever
for a softer product without a sweeter one.
All INTEGER. Suffixes: _q1 = x10, _q2 = x100, _q3 = x1000,
_permil = x1000 (fraction), _milli_c = temperature x1000 in Celsius.
Grounding (cited; researcher-groundable):
dependencies 1 imports · 4 importers
imports: nx_syscalls.nx
imported by: nx_ice_recrystal.nxnx_icecream_bench_gate.nxnx_labsci_svc.nxnx_water_activity.nx
structs
| 117 | struct NxIceMix |
consts
| 54 | const IC_KF_WATER_Q3: i64 = 1860 |
| 57 | const IC_MW_SUCROSE_Q2: i64 = 34230 |
| 58 | const IC_MW_LACTOSE_Q2: i64 = 34230 |
| 59 | const IC_MW_MALTOSE_Q2: i64 = 34230 |
| 60 | const IC_MW_DEXTROSE_Q2: i64 = 18016 |
| 61 | const IC_MW_FRUCTOSE_Q2: i64 = 18016 |
| 62 | const IC_MW_INVERT_Q2: i64 = 18016 // 50/50 glucose+fructose, both 180.16 |
| 63 | const IC_MW_SORBITOL_Q2: i64 = 18217 |
| 64 | const IC_MW_NACL_Q2: i64 = 5844 |
| 68 | const IC_ANHYDROGLUCOSE_Q2: i64 = 16214 |
| 69 | const IC_WATER_MW_Q2: i64 = 1802 |
| 73 | const IC_MSNF_LACTOSE_PERMIL: i64 = 545 // 54.5% of MSNF is lactose |
| 74 | const IC_MSNF_ASH_PERMIL: i64 = 80 // 8.0% of MSNF is mineral ash |
| 80 | const IC_MILK_SALT_EFF_MW_Q2: i64 = 4000 |
| 84 | const IC_LACTOSE_SOL_Q1: i64 = 90 |
| 88 | const IC_FDA_MIN_MILKFAT_PERMIL: i64 = 100 // 10.0% milkfat |
| 89 | const IC_FDA_MIN_MILK_SOLIDS_PERMIL: i64 = 200 // 20.0% total milk solids |
| 90 | const IC_FDA_MIN_LB_PER_GAL_Q2: i64 = 450 // 4.5 lb/gal finished weight |
| 91 | const IC_FDA_MIN_FOOD_SOLIDS_Q2: i64 = 160 // 1.6 lb food solids/gal |
| 93 | const IC_ML_PER_GAL: i64 = 3785 // 1 US gallon = 3785.41 mL |
| 94 | const IC_G_PER_LB: i64 = 454 // 1 lb = 453.59 g |
| 98 | const IC_STYLE_UNCLASSIFIED: i64 = 0 |
| 99 | const IC_STYLE_SORBET: i64 = 1 |
| 100 | const IC_STYLE_GELATO: i64 = 2 |
| 101 | const IC_STYLE_ICE_CREAM: i64 = 3 |
| 102 | const IC_STYLE_LOWFAT: i64 = 4 |
| 104 | const IC_GELATO_MAX_OVERRUN_PCT: i64 = 50 |
| 105 | const IC_SORBET_MAX_FAT_PERMIL: i64 = 10 // under 1% fat = not a dairy ice |
| 106 | const IC_LOWFAT_MIN_FAT_PERMIL: i64 = 20 |
| 109 | const IC_SCOOPABLE_FROZEN_PERMIL: i64 = 720 |
functions
| 128 | func nx_ice_mix_new() -> *NxIceMix |
| 143 | func ic_total_solids_g(m: *NxIceMix) -> i64 |
| 152 | func ic_water_g(m: *NxIceMix) -> i64 |
| 159 | func ic_permil(part_g: i64, total_g: i64) -> i64 |
| 164 | func ic_fat_permil(m: *NxIceMix) -> i64 { return ic_permil(m.fat_g, m.total_g) } |
| 165 | func ic_msnf_permil(m: *NxIceMix) -> i64 { return ic_permil(m.msnf_g, m.total_g) } calls 1: ic_permil |
| 166 | func ic_sugar_permil(m: *NxIceMix) -> i64 { return ic_permil(m.sugar_g, m.total_g) } calls 1: ic_permil |
| 168 | func ic_total_solids_permil(m: *NxIceMix) -> i64 |
| 174 | func ic_milk_solids_permil(m: *NxIceMix) -> i64 |
| 186 | func ic_pac_from_mw(mw_q2: i64) -> i64 called by 1: ic_pac_index |
| 195 | func ic_syrup_mw_q2(de: i64) -> i64 |
| 204 | func ic_blend_mw_q2(g1: i64, mw1_q2: i64, g2: i64, mw2_q2: i64) -> i64 |
| 216 | func ic_blend_pod(g1: i64, pod1: i64, g2: i64, pod2: i64) -> i64 |
| 228 | func ic_pac_index(m: *NxIceMix) -> i64 |
| 235 | func ic_pod_index(m: *NxIceMix) -> i64 |
| 251 | func ic_sugar_umol(m: *NxIceMix) -> i64 called by 1: ic_total_solute_umol |
| 256 | func ic_lactose_g(m: *NxIceMix) -> i64 called by 1: ic_lactose_per_100_water_q1 |
| 260 | func ic_lactose_umol(m: *NxIceMix) -> i64 called by 1: ic_total_solute_umol |
| 265 | func ic_milk_salt_umol(m: *NxIceMix) -> i64 called by 1: ic_total_solute_umol |
| 270 | func ic_total_solute_umol(m: *NxIceMix) -> i64 |
| 279 | func ic_fpd_milli_c(m: *NxIceMix) -> i64 |
| 289 | func ic_freezing_point_milli_c(m: *NxIceMix) -> i64 |
| 301 | func ic_frozen_water_permil(m: *NxIceMix, temp_milli_c: i64) -> i64 |
| 316 | func ic_serve_temp_for_frozen_permil(m: *NxIceMix, target_permil: i64) -> i64 |
| 327 | func ic_scoop_temp_milli_c(m: *NxIceMix) -> i64 |
| 333 | func ic_hardness_index(m: *NxIceMix, temp_milli_c: i64) -> i64 calls 1: ic_frozen_water_permil |
| 340 | func ic_lactose_per_100_water_q1(m: *NxIceMix) -> i64 |
| 347 | func ic_sandy_risk(m: *NxIceMix) -> i64 |
| 358 | func ic_max_msnf_g(water_g: i64) -> i64 |
| 368 | func ic_overrun_pct(vol_mix: i64, vol_frozen: i64) -> i64 |
| 376 | func ic_overrun_pct_by_weight(wt_mix: i64, wt_frozen: i64) -> i64 |
| 383 | func ic_product_density_q3(density_q3: i64, overrun_pct: i64) -> i64 |
| 390 | func ic_weight_lb_per_gal_q2(density_q3: i64, overrun_pct: i64) -> i64 |
| 404 | func ic_max_legal_overrun_pct(density_q3: i64) -> i64 called by 1: t16 |
| 434 | func ic_food_solids_permil(m: *NxIceMix) -> i64 called by 1: ic_fda_food_solids_ok |
| 441 | func ic_min_density_solids_q3(ts_permil: i64) -> i64 |
| 450 | func ic_min_density_weight_q3() -> i64 |
| 457 | func ic_legal_floor_density_q3(ts_permil: i64) -> i64 |
| 478 | func ic_ceiling_weight_pct(density_q3: i64) -> i64 called by 1: ic_max_legal_overrun_pct2 |
| 485 | func ic_ceiling_solids_pct(density_q3: i64, ts_permil: i64) -> i64 called by 1: ic_max_legal_overrun_pct2 |
| 493 | func ic_max_legal_overrun_pct2(density_q3: i64, ts_permil: i64) -> i64 |
| 501 | func ic_fda_food_solids_ok(m: *NxIceMix, overrun_pct: i64) -> i64 |
| 515 | func ic_fda_is_ice_cream2(m: *NxIceMix, overrun_pct: i64) -> i64 |
| 525 | func ic_fda_milkfat_ok(m: *NxIceMix) -> i64 |
| 531 | func ic_fda_milk_solids_ok(m: *NxIceMix) -> i64 |
| 537 | func ic_fda_weight_ok(m: *NxIceMix, overrun_pct: i64) -> i64 |
| 545 | func ic_fda_is_ice_cream(m: *NxIceMix, overrun_pct: i64) -> i64 |
| 554 | func ic_classify(m: *NxIceMix, overrun_pct: i64) -> i64 calls 1: ic_fat_permil |