nx_icecream_balance.nx
buildroot/runtime/nx_icecream_balance.nx
about
nx_icecream_balance.nx -- FOOD-SCIENCE SUITE / MIX BALANCING rung.
nx_icecream ANALYSES a mix that already exists. This SOLVES for the mix:
given targets, it returns the grams of each ingredient to weigh out.
That inversion is the formulator's actual job, and it is the difference
between a calculator and a design tool.
THREE SOLVERS, EACH EXACT -- no iteration, no search, no float.
1. THE DAIRY BALANCE is a TRIANGULAR system, so it solves in one pass.
Fat comes only from cream, so cream is determined first; cream drags
MSNF in with it, so the powder only makes up the shortfall; sugar and
stabiliser are direct; added water is whatever is left. Solving it as
a general matrix would be wrong-headed -- the dependency order IS the
dairy chemistry.
2. THE SUGAR BLEND is a 2x2 with a closed form. Two sugars, two
constraints (total mass, target PAC), so
g_lo = (PAC_hi * S - P) / (PAC_hi - PAC_lo)
and the PAC figures come from ic_pac_from_mw, not from constants, so
the solver works for ANY sugar pair rather than the one it was written
against.
3. THE SERVE-TEMPERATURE INVERSION runs the freezing curve backwards:
pick the temperature you want to scoop at and the ice fraction you
want there, and it returns the grams of sweetener that produce it.
This is where the dextrose lever becomes a number instead of an
opinion -- scooping at -12 C takes ~248 g of sucrose or ~130 g of
dextrose per kg, because PAC is inverse in molar mass.
EVERY SOLVER REFUSES RATHER THAN RETURNS A BAD MIX. Cream that already
overshoots the MSNF target, a PAC target outside what the two sugars can
reach, negative added water -- all set feasible = 0. A formulation that
cannot exist must not come back as numbers a person would then weigh out.
ROUNDING IS TO NEAREST, NOT TRUNCATED. Truncation cost a whole PAC unit
in nx_icecream (dextrose 190.009 read as 189); here it would silently
miss the MSNF target by a gram per stage.
Grounding (cited; researcher-groundable):
goff_hartel_ice_cream_mix_calculation_serum_point
dependencies 2 imports · 1 importers
imports: nx_syscalls.nxnx_icecream.nx
imported by: nx_icecream_balance_test.nx
structs
| 66 | struct NxMixSolution |
consts
| 51 | const ICB_CREAM_FAT_PERMIL: i64 = 400 // 40% heavy cream |
| 52 | const ICB_CREAM_MSNF_PERMIL: i64 = 54 // 5.4% MSNF rides along with it |
| 53 | const ICB_SMP_MSNF_PERMIL: i64 = 960 // 96% skim milk powder |
| 54 | const ICB_MILK_FAT_PERMIL: i64 = 35 // 3.5% whole milk |
| 55 | const ICB_MILK_MSNF_PERMIL: i64 = 87 |
| 57 | const ICB_INFEASIBLE: i64 = 0 - 1 |
functions
| 75 | func nx_mix_solution_new() -> *NxMixSolution |
| 87 | func icb_div_round(num: i64, den: i64) -> i64 |
| 95 | func icb_solve_dairy(batch_g: i64, fat_permil: i64, msnf_permil: i64, sugar_permil: i64, stab_g: i64) -> *NxMixSolution |
| 120 | func icb_solution_to_mix(s: *NxMixSolution, batch_g: i64, sugar_mw_q2: i64, sugar_pod: i64, density_q3: i64) -> *NxIceMix |
| 143 | func icb_pac_index_of(sugar_g: i64, batch_g: i64, mw_q2: i64) -> i64 |
| 149 | func icb_solve_sugar_blend(total_sugar_g: i64, batch_g: i64, pac_target: i64, mw_lo_q2: i64, mw_hi_q2: i64) -> i64 |
| 174 | func icb_fpd_needed_milli(serve_temp_milli: i64, target_frozen_permil: i64) -> i64 |
| 183 | func icb_sugar_for_serve_temp(msnf_g: i64, water_g: i64, sugar_mw_q2: i64, serve_temp_milli: i64, target_frozen_permil: i64) -> i64 |