nx_ice_scale.nx source
↩ module page · 299 lines · 13904 B
1// nx_ice_scale.nx -- PERSONAL TO INDUSTRIAL: what changes when you make more,
2// and the direction that surprises people.
3//
4// ===== THE FINDING THIS ORGAN EXISTS TO CARRY ======================
5//
6// **SCALING UP IMPROVES ICE CRYSTAL SIZE. The artisan premium story is
7// backwards on this axis, and the mechanism is not subtle.**
8//
9// A batch freezer holds the mix in the barrel for five to fifteen minutes
10// against a barrel wall only a little colder than the draw temperature. A
11// continuous freezer pushes it through in about thirty seconds against a
12// wall at -26 to -28 C. Nucleation rate rises steeply with undercooling and
13// growth time is what lets crystals coarsen, so the industrial machine
14// produces MORE and SMALLER crystals, and the small-batch machine produces
15// fewer and larger ones. Cold extrusion pushes this further, drawing to
16// -15 to -18 C with crystals and air cells two to three times smaller again.
17//
18// So the quality-versus-scale trade that a formulator expects does not exist
19// here. It runs the other way, and a business plan that promises a superior
20// product BECAUSE it is made in small batches is promising the opposite of
21// what the freezer does.
22//
23// ===== WHERE THE SMALL PRODUCER ACTUALLY COMPETES ==================
24//
25// Not at the freezer. At the COLD CHAIN. nx_ice_recrystal established that
26// ripening is driven by temperature FLUCTUATION rather than mean temperature,
27// and nx_ice_stabiliser established that stabilisers act during STORAGE and
28// not during freezing. Both point the same way: what happens after the
29// product leaves the barrel dominates what happened inside it.
30//
31// A producer selling within days, through a freezer that is never opened to
32// a warm room, holds an advantage that no continuous freezer can take back --
33// and a national brand shipping through open-faced retail cabinets that reach
34// -9 C is carrying damage the plant cannot prevent. That is the real axis,
35// and it is the opposite of the one the marketing uses.
36//
37// ===== WHAT IS DERIVED AND WHAT IS SUPPLIED ========================
38//
39// **The thermodynamics is DERIVED.** Freezing energy per kilogram follows
40// from the latent heat of fusion, the water fraction, the frozen fraction and
41// two specific heats. Nothing about it is a quoted figure.
42//
43// **The money is SUPPLIED BY THE CALLER and flagged indicative.** This organ
44// holds no equipment prices, no ingredient prices and no labour rates,
45// because it has no verified source for any of them. It computes the SHAPE
46// of the economics from costs the caller provides, and isc_cost_is_indicative
47// reports that provenance so no downstream model can present a computed
48// margin as a quotation. A cost model built on invented prices produces
49// confident nonsense, which is the failure this lane exists to refuse.
50//
51// All INTEGER. _q1 = x10, _q2 = x100, _mc = milli-Celsius, _permil = x1000.
52//
53// Grounding (cited):
54// cook_hartel_2010_ice_cream_freezing_draw_and_hardening
55// goff_guelph_continuous_freezer_residence_and_barrel_wall
56// giudici_2021_foods_10_334_batch_freezer_larger_crystals
57// windhab_wildmoser_cold_extrusion_crystal_and_cell_size
58// latent_heat_fusion_water_334_kj_per_kg
59//
60// genealogy_id: frozen_dessert_science + nishi_food_science_suite
61
62import "nx_syscalls.nx"
63const ISC_MAGIC_1200: i64 = 1200
64const ISC_MAGIC_18000: i64 = 18000
65const ISC_MAGIC_22000: i64 = 22000
66const ISC_MAGIC_27000: i64 = 27000
67const ISC_MAGIC_5000: i64 = 5000
68const ISC_MAGIC_6000: i64 = 6000
69const ISC_MAGIC_5500: i64 = 5500
70const ISC_MAGIC_16000: i64 = 16000
71const ISC_MAGIC_35000: i64 = 35000
72const ISC_MAGIC_40000: i64 = 40000
73const ISC_MAGIC_3600: i64 = 3600
74const ISC_MAGIC_1000000: i64 = 1000000
75
76const ISC_INVALID: i64 = 0 - 1
77const ISC_UNKNOWN: i64 = 0 - 2
78
79// ===== Freezer classes ==============================================
80
81const ISC_BATCH_HOME: i64 = 0
82const ISC_BATCH_COMMERCIAL: i64 = 1
83const ISC_CONTINUOUS: i64 = 2
84const ISC_COLD_EXTRUSION: i64 = 3
85
86// ===== Thermophysical constants =====================================
87
88// Latent heat of fusion of water, J/kg.
89const ISC_LATENT_FUSION_J: i64 = 334000
90
91// Specific heats, J/(kg*K): mix above its freezing point, product below it.
92const ISC_CP_UNFROZEN_J: i64 = 3300
93const ISC_CP_FROZEN_J: i64 = 2000
94
95const ISC_J_PER_WH: i64 = 3600
96
97// Retail open-faced cabinets reach this, which is the abuse baseline a
98// national cold chain actually operates against.
99const ISC_RETAIL_CABINET_MC: i64 = 0 - 9000
100
101// ===== Freezer behaviour, from the cited process literature ==========
102
103// Residence time in the barrel, seconds.
104func isc_residence_s(class: i64) -> i64 {
105 if class == ISC_BATCH_HOME { return ISC_MAGIC_1200 }
106 if class == ISC_BATCH_COMMERCIAL { return 480 }
107 if class == ISC_CONTINUOUS { return 30 }
108 if class == ISC_COLD_EXTRUSION { return 30 }
109 return ISC_INVALID
110}
111
112// Barrel wall temperature, milli-Celsius. The undercooling that drives
113// nucleation is the gap between this and the draw temperature.
114func isc_barrel_wall_mc(class: i64) -> i64 {
115 if class == ISC_BATCH_HOME { return 0 - ISC_MAGIC_18000 }
116 if class == ISC_BATCH_COMMERCIAL { return 0 - ISC_MAGIC_22000 }
117 if class == ISC_CONTINUOUS { return 0 - ISC_MAGIC_27000 }
118 if class == ISC_COLD_EXTRUSION { return 0 - ISC_MAGIC_27000 }
119 return ISC_INVALID
120}
121
122// Draw temperature out of the barrel, milli-Celsius.
123func isc_draw_mc(class: i64) -> i64 {
124 if class == ISC_BATCH_HOME { return 0 - ISC_MAGIC_5000 }
125 if class == ISC_BATCH_COMMERCIAL { return 0 - ISC_MAGIC_6000 }
126 if class == ISC_CONTINUOUS { return 0 - ISC_MAGIC_5500 }
127 if class == ISC_COLD_EXTRUSION { return 0 - ISC_MAGIC_16000 }
128 return ISC_INVALID
129}
130
131// Undercooling at the wall: how hard the machine is driving nucleation.
132func isc_undercooling_mc(class: i64) -> i64 {
133 let wall: i64 = isc_barrel_wall_mc(class)
134 let draw: i64 = isc_draw_mc(class)
135 if wall == ISC_INVALID { return ISC_INVALID }
136 return draw - wall
137}
138
139// Mean crystal diameter at draw, tenths of a micrometre.
140//
141// !!THE BATCH FIGURE IS DELIBERATELY ABSENT. The source establishes that a
142// batch freezer yields LARGER crystals than a continuous one; it does not
143// give a number. Reporting the ORDERING as measured and the MAGNITUDE as
144// unknown is the honest split -- inventing a batch diameter would let a
145// caller compute a false quality gap to three digits.
146func isc_crystal_at_draw_q1(class: i64) -> i64 {
147 if class == ISC_CONTINUOUS { return 325 }
148 if class == ISC_COLD_EXTRUSION { return 162 }
149 if class == ISC_BATCH_HOME { return ISC_UNKNOWN }
150 if class == ISC_BATCH_COMMERCIAL { return ISC_UNKNOWN }
151 return ISC_INVALID
152}
153
154// The ordering IS measured, and it is the claim that matters.
155func isc_batch_makes_larger_crystals(batch: i64, continuous: i64) -> i64 {
156 if batch == ISC_BATCH_HOME { if continuous == ISC_CONTINUOUS { return 1 } }
157 if batch == ISC_BATCH_HOME { if continuous == ISC_COLD_EXTRUSION { return 1 } }
158 if batch == ISC_BATCH_COMMERCIAL { if continuous == ISC_CONTINUOUS { return 1 } }
159 if batch == ISC_BATCH_COMMERCIAL { if continuous == ISC_COLD_EXTRUSION { return 1 } }
160 return 0
161}
162
163// **The headline, as a function so a planner cannot route around it.
164func isc_scaling_improves_crystal_size() -> i64 { return 1 }
165func isc_small_batch_is_finer() -> i64 { return 0 }
166
167// Hardening: air temperature and the time to reach the >=80% frozen endpoint.
168func isc_hardening_air_mc(class: i64) -> i64 {
169 if class == ISC_BATCH_HOME { return 0 - ISC_MAGIC_18000 }
170 if class == ISC_BATCH_COMMERCIAL { return 0 - ISC_MAGIC_35000 }
171 if class == ISC_CONTINUOUS { return 0 - ISC_MAGIC_40000 }
172 if class == ISC_COLD_EXTRUSION { return 0 - ISC_MAGIC_40000 }
173 return ISC_INVALID
174}
175
176func isc_hardening_minutes(class: i64) -> i64 {
177 if class == ISC_BATCH_HOME { return 720 }
178 if class == ISC_BATCH_COMMERCIAL { return 120 }
179 if class == ISC_CONTINUOUS { return 25 }
180 if class == ISC_COLD_EXTRUSION { return 18 }
181 return ISC_INVALID
182}
183
184// !!A HOME FREEZER IS NOT A HARDENING TUNNEL. At -18 C it takes about half a
185// day to reach the endpoint a blast tunnel reaches in twenty minutes, and
186// every hour spent between the draw temperature and the endpoint is time the
187// crystals spend growing. This is the one place where the home tier is
188// genuinely and unavoidably worse.
189func isc_has_true_hardening(class: i64) -> i64 {
190 if class == ISC_BATCH_HOME { return 0 }
191 if class == ISC_INVALID { return 0 }
192 return 1
193}
194
195// ===== Throughput ====================================================
196
197func isc_throughput_l_per_h(class: i64, barrel_l: i64) -> i64 {
198 if barrel_l <= 0 { return ISC_INVALID }
199 let res: i64 = isc_residence_s(class)
200 if res == ISC_INVALID { return ISC_INVALID }
201 if res <= 0 { return ISC_INVALID }
202 return barrel_l * ISC_MAGIC_3600 / res
203}
204
205// ===== DERIVED: the energy it takes to freeze a kilogram ==============
206//
207// E = cp_unfrozen * (T_in - T_fp) sensible, above freezing
208// + water * frozen * L latent, the dominant term
209// + cp_frozen * (T_fp - T_final) sensible, below freezing
210//
211// frozen_permil should come from ic_frozen_water_permil so the freezing
212// curve of the ACTUAL mix drives the answer rather than a typical value.
213func isc_freeze_energy_j_per_kg(t_in_mc: i64, t_fp_mc: i64, t_final_mc: i64, water_permil: i64, frozen_permil: i64) -> i64 {
214 if water_permil < 0 { return ISC_INVALID }
215 if water_permil > 1000 { return ISC_INVALID }
216 if frozen_permil < 0 { return ISC_INVALID }
217 if frozen_permil > 1000 { return ISC_INVALID }
218 if t_in_mc <= t_fp_mc { return ISC_INVALID }
219 if t_final_mc >= t_fp_mc { return ISC_INVALID }
220 let sens_above: i64 = ISC_CP_UNFROZEN_J * (t_in_mc - t_fp_mc) / 1000
221 let latent: i64 = water_permil * frozen_permil * ISC_LATENT_FUSION_J / ISC_MAGIC_1000000
222 let sens_below: i64 = ISC_CP_FROZEN_J * (t_fp_mc - t_final_mc) / 1000
223 return sens_above + latent + sens_below
224}
225
226// **THE LATENT TERM DOMINATES, AND THAT IS THE DESIGN FACT. Most of the
227// energy is spent changing phase, not changing temperature, so a plant's
228// refrigeration is sized by how much WATER it freezes -- which is why raising
229// total solids lowers the energy bill as well as the freezing point.
230func isc_latent_share_permil(t_in_mc: i64, t_fp_mc: i64, t_final_mc: i64, water_permil: i64, frozen_permil: i64) -> i64 {
231 let total: i64 = isc_freeze_energy_j_per_kg(t_in_mc, t_fp_mc, t_final_mc, water_permil, frozen_permil)
232 if total == ISC_INVALID { return ISC_INVALID }
233 if total <= 0 { return ISC_INVALID }
234 let latent: i64 = water_permil * frozen_permil * ISC_LATENT_FUSION_J / ISC_MAGIC_1000000
235 return latent * 1000 / total
236}
237
238// Electrical energy, given a coefficient of performance x100. A refrigeration
239// plant lifting heat from -30 C runs a COP near 1.5 to 2.0, so the electrical
240// draw is materially larger than the thermal load.
241func isc_electrical_wh_per_kg(thermal_j: i64, cop_q2: i64) -> i64 {
242 if thermal_j <= 0 { return ISC_INVALID }
243 if cop_q2 <= 0 { return ISC_INVALID }
244 return thermal_j * 100 / (cop_q2 * ISC_J_PER_WH)
245}
246
247// ===== Cost STRUCTURE, on caller-supplied prices =====================
248//
249// !!EVERY PRICE HERE ARRIVES FROM THE CALLER. This organ has no verified
250// source for ingredient, energy, labour or packaging cost, so it holds none.
251
252func isc_cogs_per_l_cents(ingredient_c: i64, energy_c: i64, labour_c: i64, packaging_c: i64) -> i64 {
253 if ingredient_c < 0 { return ISC_INVALID }
254 if energy_c < 0 { return ISC_INVALID }
255 if labour_c < 0 { return ISC_INVALID }
256 if packaging_c < 0 { return ISC_INVALID }
257 return ingredient_c + energy_c + labour_c + packaging_c
258}
259
260// Energy cost per litre of MIX, from the derived thermal load and a supplied
261// tariff in cents per kilowatt-hour. mix_density_q3 converts per-kg to
262// per-litre, so a denser mix costs more per litre to freeze.
263func isc_energy_cost_per_l_cents_q2(wh_per_kg: i64, mix_density_q3: i64, tariff_c_per_kwh: i64) -> i64 {
264 if wh_per_kg <= 0 { return ISC_INVALID }
265 if mix_density_q3 <= 0 { return ISC_INVALID }
266 if tariff_c_per_kwh < 0 { return ISC_INVALID }
267 let wh_per_l: i64 = wh_per_kg * mix_density_q3 / 1000
268 return wh_per_l * tariff_c_per_kwh / 10
269}
270
271// **LABOUR PER LITRE IS WHERE SCALE ACTUALLY PAYS, and it is arithmetic, not
272// a learning curve: one operator supervising a machine that makes ten times
273// as much per hour costs a tenth as much per litre. Wright's law in
274// nx_market covers the unit COST of a manufactured good over cumulative
275// volume; this is the simpler and larger effect for a process plant.
276func isc_labour_per_l_cents_q2(operator_c_per_h: i64, operators: i64, throughput_l_per_h: i64) -> i64 {
277 if operator_c_per_h < 0 { return ISC_INVALID }
278 if operators <= 0 { return ISC_INVALID }
279 if throughput_l_per_h <= 0 { return ISC_INVALID }
280 return operator_c_per_h * operators * 100 / throughput_l_per_h
281}
282
283func isc_cost_is_indicative() -> i64 { return 1 }
284func isc_holds_any_verified_price() -> i64 { return 0 }
285
286// ===== The cold chain, which is the axis that actually decides quality ===
287
288// A retail cabinet reaching -9 C is warmer than the -18 C storage target, so
289// a national distribution chain operates in permanent partial abuse.
290func isc_retail_cabinet_is_abuse(storage_target_mc: i64) -> i64 {
291 if ISC_RETAIL_CABINET_MC > storage_target_mc { return 1 }
292 return 0
293}
294
295// **WHERE THE SMALL PRODUCER WINS. Short chain, few handoffs, no open-faced
296// cabinet. Since ripening is driven by fluctuation rather than mean
297// temperature, fewer excursions beats a colder average.
298func isc_short_chain_beats_plant_freezer() -> i64 { return 1 }
299func isc_quality_advantage_is_at_the_freezer() -> i64 { return 0 }