nx_labsci_svc.nx source
↩ module page · 541 lines · 24538 B
1// nx_labsci_svc.nx -- LAB-SCIENCE SERVICE FACADE. The second organ built on
2// the nx_service base, over a COMPLETELY DIFFERENT domain from nx_gtm_svc --
3// peptide chemistry, frozen-dessert physics, thermal death kinetics, and
4// water activity. Its existence is the proof the base generalises: the
5// envelope, escaping, error shape, dispatch and self-description are all
6// inherited unchanged; only the verbs and their handlers differ.
7//
8// Two facades, one base, two unrelated sciences = the architecture scales to
9// all seventeen organs by the same recipe, not just the go-to-market subset.
10//
11// USAGE: nx_labsci_svc <verb> [args...]
12// describe
13// peptide.analyze <sequence> (1-letter code, e.g. DRVYIHPF)
14// icecream.freeze <fat_g> <msnf_g> <sugar_g> <sugar_mw_q2> <total_g>
15// thermal.pasteurize <temp_milli_c> <hold_ms> <highfat 0|1>
16// thermal.barrier <ph_milli> <ambient_stable 0|1>
17// water.activity <water_g> <solute_umol>
18// stability.shelf <ref_days> <ref_temp_c> <target_temp_c> <months> <loss_pct>
19// stability.mkt <temp_c>... (2-8 whole Celsius readings, equal duration)
20// stability.separate <t1> <rh1> <months1> <t2> <rh2> <months2> <t3> <rh3> <months3>
21//
22// Same discipline as the sibling facade: NO new domain logic, every number
23// comes from a gated library, structured errors, fail-closed on bad input.
24//
25// stability.separate is the verb that answers what stability.shelf can only
26// flag. ICH gives two storage conditions differing in BOTH temperature and
27// humidity, so the activation energy recovered from them is lumped; three
28// conditions break it apart, and the verb REFUSES a design that cannot --
29// reporting the conditioning separately, because a design can be solvable
30// and still useless.
31//
32// The stability verbs report GND_VALIDATED, a tier above the rest of
33// this facade's business-adjacent output, and the reason is specific: their
34// activation energy is RECOVERED from ICH's own equivalence claim and then
35// lands inside published Q10 and Ea bands that were never inputs to the
36// derivation. That is the same evidence class as the pasteurisation z-value,
37// which is why thermal.* already reports VALIDATED.
38//
39// genealogy_id: nishi_food_science_suite + service_infrastructure
40
41import "nx_syscalls.nx"
42import "nx_service.nx"
43import "nx_grounding.nx"
44import "nx_peptide.nx"
45import "nx_icecream.nx"
46import "nx_thermal_process.nx"
47import "nx_water_activity.nx"
48import "nx_pow10.nx"
49import "nx_shelf_life.nx"
50import "nx_stability.nx"
51import "nx_stability_rh.nx"
52import "nx_ice_recrystal.nx"
53import "nx_ice_distribution.nx"
54const LAB_MAGIC_13000: i64 = 13000
55const LAB_MAGIC_8192: i64 = 8192
56
57const LAB_SVC: *u8 = "labsci" as *u8
58
59// ===== describe =======================================================
60
61func lab_describe(j: *NxJson) -> i64 {
62 nsvc_ok_open(j, LAB_SVC, "describe" as *u8)
63 nj_kv_str(j, "service" as *u8, "peptide + frozen dessert + thermal + water activity" as *u8)
64 nj_comma(j)
65 nj_kv_int(j, "verb_count" as *u8, 9)
66 nj_comma(j)
67 nj_key(j, "verbs" as *u8)
68 nj_puts(j, "[" as *u8)
69 nj_puts(j, "{\"verb\":\"peptide.analyze\",\"args\":[\"sequence:string\"]}," as *u8)
70 nj_puts(j, "{\"verb\":\"icecream.freeze\",\"args\":[\"fat_g:int\",\"msnf_g:int\",\"sugar_g:int\",\"sugar_mw_q2:int\",\"total_g:int\"]}," as *u8)
71 nj_puts(j, "{\"verb\":\"thermal.pasteurize\",\"args\":[\"temp_milli_c:int\",\"hold_ms:int\",\"highfat:int\"]}," as *u8)
72 nj_puts(j, "{\"verb\":\"thermal.barrier\",\"args\":[\"ph_milli:int\",\"ambient_stable:int\"]}," as *u8)
73 nj_puts(j, "{\"verb\":\"water.activity\",\"args\":[\"water_g:int\",\"solute_umol:int\"]}," as *u8)
74 nj_puts(j, "{\"verb\":\"stability.shelf\",\"args\":[\"ref_days:int\",\"ref_temp_c:int\",\"target_temp_c:int\",\"months:int\",\"loss_pct:int\"]}," as *u8)
75 nj_puts(j, "{\"verb\":\"stability.mkt\",\"args\":[\"temp_c:int...\"]}," as *u8)
76 nj_puts(j, "{\"verb\":\"stability.separate\",\"args\":[\"t1\",\"rh1\",\"months1\",\"t2\",\"rh2\",\"months2\",\"t3\",\"rh3\",\"months3\"]}," as *u8)
77 nj_puts(j, "{\"verb\":\"icecream.recrystal\",\"args\":[\"fat_g\",\"msnf_g\",\"sugar_g\",\"total_g\",\"t_cold_milli_c\",\"t_warm_milli_c\",\"cycles\",\"days\"]}" as *u8)
78 nj_puts(j, "]" as *u8)
79 nsvc_ok_close_g(j, GND_DERIVED)
80 return 0
81}
82
83// ===== peptide.analyze ================================================
84
85func lab_peptide_analyze(j: *NxJson, seq: *u8) -> i64 {
86 if seq[0] == (0 as u8) {
87 nsvc_error(j, LAB_SVC, "peptide.analyze" as *u8, NSVC_ERR_BAD_ARGS, "empty sequence" as *u8)
88 return 0
89 }
90 if pep_seq_valid(seq) != 1 {
91 nsvc_error(j, LAB_SVC, "peptide.analyze" as *u8, NSVC_ERR_REFUSED, "sequence has a non-standard residue (refused, not approximated)" as *u8)
92 return 0
93 }
94 let mono: i64 = pep_mass_mono_q4(seq)
95 nsvc_ok_open(j, LAB_SVC, "peptide.analyze" as *u8)
96 nj_kv_str(j, "sequence" as *u8, seq)
97 nj_comma(j)
98 nj_kv_int(j, "length" as *u8, pep_len(seq))
99 nj_comma(j)
100 nj_kv_int(j, "monoisotopic_mass_q4" as *u8, mono)
101 nj_comma(j)
102 nj_kv_int(j, "average_mass_q4" as *u8, pep_mass_avg_q4(seq))
103 nj_comma(j)
104 nj_kv_int(j, "mh_plus_q4" as *u8, pep_mz_q4(mono, 1))
105 nj_comma(j)
106 nj_kv_int(j, "m2h_plus_q4" as *u8, pep_mz_q4(mono, 2))
107 nj_comma(j)
108 nj_kv_int(j, "pi_milli" as *u8, pep_pi_milli(seq))
109 nj_comma(j)
110 nj_kv_int(j, "gravy_q3" as *u8, pep_gravy_q3(seq))
111 nj_comma(j)
112 nj_kv_int(j, "ext_coeff_280" as *u8, pep_ext_coeff_280(seq, 0))
113 nj_comma(j)
114 nj_kv_bool(j, "a280_quantifiable" as *u8, pep_a280_quantifiable(seq, 0))
115 // mass/formula are ANCHORED but pI/GRAVY are DERIVED -> weakest link DERIVED
116 nsvc_ok_close_g(j, GND_DERIVED)
117 return 0
118}
119
120// ===== icecream.freeze ================================================
121
122func lab_icecream_freeze(j: *NxJson, fat: i64, msnf: i64, sugar: i64, mw: i64, total: i64) -> i64 {
123 if total <= 0 {
124 nsvc_error(j, LAB_SVC, "icecream.freeze" as *u8, NSVC_ERR_BAD_ARGS, "total_g must be positive" as *u8)
125 return 0
126 }
127 let m: *NxIceMix = nx_ice_mix_new()
128 m.fat_g = fat
129 m.msnf_g = msnf
130 m.sugar_g = sugar
131 if mw > 0 { m.sugar_mw_q2 = mw }
132 m.total_g = total
133 let water: i64 = ic_water_g(m)
134 if water <= 0 {
135 nsvc_error(j, LAB_SVC, "icecream.freeze" as *u8, NSVC_ERR_REFUSED, "solids exceed batch mass (incoherent mix)" as *u8)
136 return 0
137 }
138 nsvc_ok_open(j, LAB_SVC, "icecream.freeze" as *u8)
139 nj_kv_int(j, "water_g" as *u8, water)
140 nj_comma(j)
141 nj_kv_int(j, "freezing_point_milli_c" as *u8, ic_freezing_point_milli_c(m))
142 nj_comma(j)
143 nj_kv_int(j, "frozen_permil_at_minus13c" as *u8, ic_frozen_water_permil(m, 0 - LAB_MAGIC_13000))
144 nj_comma(j)
145 nj_kv_int(j, "scoop_temp_milli_c" as *u8, ic_scoop_temp_milli_c(m))
146 nj_comma(j)
147 nj_kv_int(j, "pac_index" as *u8, ic_pac_index(m))
148 nj_comma(j)
149 nj_kv_bool(j, "sandy_risk" as *u8, ic_sandy_risk(m))
150 // freezing point is VALIDATED vs published curves, PAC is DERIVED -> DERIVED
151 nsvc_ok_close_g(j, GND_DERIVED)
152 return 0
153}
154
155// ===== thermal.pasteurize =============================================
156
157func lab_thermal_pasteurize(j: *NxJson, temp: i64, hold: i64, highfat: i64) -> i64 {
158 if hold <= 0 {
159 nsvc_error(j, LAB_SVC, "thermal.pasteurize" as *u8, NSVC_ERR_BAD_ARGS, "hold_ms must be positive" as *u8)
160 return 0
161 }
162 nsvc_ok_open(j, LAB_SVC, "thermal.pasteurize" as *u8)
163 nj_kv_int(j, "temp_milli_c" as *u8, temp)
164 nj_comma(j)
165 nj_kv_int(j, "hold_ms" as *u8, hold)
166 nj_comma(j)
167 nj_kv_int(j, "equiv_lethality_ms" as *u8, tp_pasteurization_equiv_ms(temp, hold, highfat))
168 nj_comma(j)
169 nj_kv_bool(j, "compliant" as *u8, tp_pasteurization_compliant(temp, hold, highfat))
170 nj_comma(j)
171 nj_kv_int(j, "f0_ms" as *u8, tp_f0_ms(hold, temp))
172 // z recovered from two legal PMO schedules + lands in published band
173 nsvc_ok_close_g(j, GND_VALIDATED)
174 return 0
175}
176
177// ===== thermal.barrier ================================================
178
179func lab_thermal_barrier(j: *NxJson, ph: i64, ambient: i64) -> i64 {
180 nsvc_ok_open(j, LAB_SVC, "thermal.barrier" as *u8)
181 nj_kv_int(j, "ph_milli" as *u8, ph)
182 nj_comma(j)
183 nj_kv_bool(j, "high_acid" as *u8, cs_is_high_acid(ph))
184 nj_comma(j)
185 nj_kv_int(j, "barrier_required" as *u8, tp_barrier_required(ph, ambient))
186 // the 4.6 botulinum line is an established regulatory threshold
187 nsvc_ok_close_g(j, GND_VALIDATED)
188 return 0
189}
190
191// ===== water.activity =================================================
192
193func lab_water_activity(j: *NxJson, water: i64, solute: i64) -> i64 {
194 let aw: i64 = wa_from_solutes_q4(water, solute)
195 if aw == WA_INVALID {
196 nsvc_error(j, LAB_SVC, "water.activity" as *u8, NSVC_ERR_REFUSED, "no water / bad solute -- aw undefined (refused, not reported as dry-and-safe)" as *u8)
197 return 0
198 }
199 nsvc_ok_open(j, LAB_SVC, "water.activity" as *u8)
200 nj_kv_int(j, "aw_q4" as *u8, aw)
201 nj_comma(j)
202 nj_kv_bool(j, "potentially_hazardous" as *u8, wa_is_potentially_hazardous(aw))
203 nj_comma(j)
204 nj_kv_int(j, "organisms_supported" as *u8, wa_organisms_supported(aw))
205 nj_comma(j)
206 nj_kv_bool(j, "botulinum_can_grow" as *u8, wa_supports_growth(aw, WA_ORG_CBOT))
207 // aw is DERIVED (Raoult) but the organism growth thresholds are ASSERTED
208 nsvc_ok_close_g(j, GND_ASSERTED)
209 return 0
210}
211
212// ===== stability.shelf ================================================
213
214// Projects a shelf life from one storage temperature to another and reports
215// what survives to the end of it. The temperature move uses the fractional
216// Q10 path, so a non-decade target (the 25 C -> 40 C case the whole category
217// rests on) is answerable instead of refused.
218func lab_stability_shelf(j: *NxJson, ref_days: i64, ref_c: i64, target_c: i64, months: i64, loss_pct: i64) -> i64 {
219 var q10: i64 = 0
220 var ea: i64 = 0
221 var life: i64 = 0
222 var retained: i64 = 0
223 if ref_days <= 0 {
224 nsvc_error(j, LAB_SVC, "stability.shelf" as *u8, NSVC_ERR_BAD_ARGS, "ref_days must be positive" as *u8)
225 return 0
226 }
227 if months <= 0 {
228 nsvc_error(j, LAB_SVC, "stability.shelf" as *u8, NSVC_ERR_BAD_ARGS, "months must be positive" as *u8)
229 return 0
230 }
231 q10 = stab_ich_q10_q3()
232 ea = stab_ich_ea_j()
233 life = sl_at_temp_frac(ref_days, ref_c, q10, target_c)
234 if life <= 0 {
235 nsvc_error(j, LAB_SVC, "stability.shelf" as *u8, NSVC_ERR_REFUSED, "shelf life not computable for this temperature pair (refused, not extrapolated)" as *u8)
236 return 0
237 }
238 retained = stab_potency_retained_permil(months, months, loss_pct)
239 if retained == STAB_INVALID {
240 nsvc_error(j, LAB_SVC, "stability.shelf" as *u8, NSVC_ERR_BAD_ARGS, "loss_pct must be in 1..99" as *u8)
241 return 0
242 }
243 nsvc_ok_open(j, LAB_SVC, "stability.shelf" as *u8)
244 nj_kv_int(j, "shelf_days_at_target" as *u8, life)
245 nj_comma(j)
246 nj_kv_int(j, "q10_q3_recovered" as *u8, q10)
247 nj_comma(j)
248 nj_kv_int(j, "activation_energy_j_per_mol" as *u8, ea)
249 nj_comma(j)
250 nj_kv_int(j, "potency_retained_permil" as *u8, retained)
251 nj_comma(j)
252 nj_kv_int(j, "overage_needed_permil" as *u8, stab_overage_permil(retained))
253 nj_comma(j)
254 nj_kv_bool(j, "significant_change" as *u8, stab_is_significant_change(retained))
255 nj_comma(j)
256 // A hard limit the consumer must not be able to route around.
257 nj_kv_bool(j, "accelerated_alone_sets_shelf_life" as *u8, stab_accelerated_alone_sets_shelf_life())
258 nj_comma(j)
259 // ICH's two conditions differ in humidity as well as temperature, so the
260 // recovered figure is an EFFECTIVE Ea. Surfaced at the wire rather than
261 // buried in a header, because the number is only safe along that path.
262 nj_kv_bool(j, "activation_energy_is_lumped_with_humidity" as *u8, stab_ea_is_lumped_with_humidity())
263 nj_comma(j)
264 nj_kv_str(j, "valid_along" as *u8, "the ICH storage path (60-75% RH); off-path use over-states degradation for a drier product" as *u8)
265 nsvc_ok_close_g(j, GND_VALIDATED)
266 return 0
267}
268
269// ===== stability.mkt ==================================================
270
271// Mean kinetic temperature of an equal-duration temperature history, beside
272// the arithmetic mean it is so often confused with. Reporting both in one
273// response is the point: the gap between them IS the finding.
274func lab_stability_mkt(j: *NxJson, argc: i64, argv: *i64) -> i64 {
275 var n: i64 = 0
276 var i: i64 = 0
277 var ea: i64 = 0
278 var mkt: i64 = 0
279 var mean: i64 = 0
280 let temps: *i64 = sys_mmap(16 * 8)
281 let hours: *i64 = sys_mmap(16 * 8)
282 while i < 8 {
283 if argc > i + 2 {
284 temps[n] = nsvc_arg_int(argc, argv, i + 2)
285 hours[n] = 1
286 n = n + 1
287 }
288 i = i + 1
289 }
290 if n < 2 {
291 nsvc_error(j, LAB_SVC, "stability.mkt" as *u8, NSVC_ERR_BAD_ARGS, "give 2..8 whole-Celsius readings" as *u8)
292 return 0
293 }
294 ea = stab_ich_ea_j()
295 mkt = stab_mkt_centi_kelvin(temps, hours, n, ea)
296 mean = stab_mean_centi_kelvin(temps, hours, n)
297 if mkt == STAB_INVALID {
298 nsvc_error(j, LAB_SVC, "stability.mkt" as *u8, NSVC_ERR_REFUSED, "history not computable (refused, not averaged)" as *u8)
299 return 0
300 }
301 nsvc_ok_open(j, LAB_SVC, "stability.mkt" as *u8)
302 nj_kv_int(j, "readings" as *u8, n)
303 nj_comma(j)
304 nj_kv_int(j, "mkt_centi_kelvin" as *u8, mkt)
305 nj_comma(j)
306 nj_kv_int(j, "mkt_centi_c" as *u8, mkt - STAB_KELVIN_OFF_CENTI)
307 nj_comma(j)
308 nj_kv_int(j, "arithmetic_mean_centi_c" as *u8, mean - STAB_KELVIN_OFF_CENTI)
309 nj_comma(j)
310 nj_kv_int(j, "understatement_centi_c" as *u8, mkt - mean)
311 nj_comma(j)
312 nj_kv_int(j, "activation_energy_j_per_mol" as *u8, ea)
313 nj_comma(j)
314 nj_kv_str(j, "note" as *u8, "degradation is convex in temperature, so the arithmetic mean UNDER-states it" as *u8)
315 nsvc_ok_close_g(j, GND_VALIDATED)
316 return 0
317}
318
319// ===== stability.separate =============================================
320
321// Separates the temperature axis from the moisture axis given three real
322// storage conditions. This is the verb that answers what stability.shelf
323// can only flag: the lumped parameter is only lumped because ICH gives two
324// conditions, and three break it apart.
325//
326// REFUSES rather than guesses on a design that cannot support the split,
327// and reports the CONDITIONING separately -- a design can be solvable and
328// still useless, which is the failure mode that produces a confident,
329// specific, meaningless activation energy.
330func lab_stability_separate(j: *NxJson, argc: i64, argv: *i64) -> i64 {
331 var t1: i64 = 0
332 var r1: i64 = 0
333 var m1: i64 = 0
334 var t2: i64 = 0
335 var r2: i64 = 0
336 var m2: i64 = 0
337 var t3: i64 = 0
338 var r3: i64 = 0
339 var m3: i64 = 0
340 var rc: i64 = 0
341 var cond: i64 = 0
342 let out: *i64 = sys_mmap(8 * 4)
343 if argc < 11 {
344 nsvc_error(j, LAB_SVC, "stability.separate" as *u8, NSVC_ERR_BAD_ARGS, "need 9 args: t1 rh1 months1 t2 rh2 months2 t3 rh3 months3" as *u8)
345 return 0
346 }
347 t1 = nsvc_arg_int(argc, argv, 2); r1 = nsvc_arg_int(argc, argv, 3); m1 = nsvc_arg_int(argc, argv, 4)
348 t2 = nsvc_arg_int(argc, argv, 5); r2 = nsvc_arg_int(argc, argv, 6); m2 = nsvc_arg_int(argc, argv, 7)
349 t3 = nsvc_arg_int(argc, argv, 8); r3 = nsvc_arg_int(argc, argv, 9); m3 = nsvc_arg_int(argc, argv, 10)
350 cond = srh_design_conditioning_permil(t1, r1, t2, r2, t3, r3)
351 rc = srh_separate(t1, r1, m1, t2, r2, m2, t3, r3, m3, out)
352 if rc == SRH_UNDERDETERMINED {
353 nsvc_error(j, LAB_SVC, "stability.separate" as *u8, NSVC_ERR_REFUSED, "these three conditions cannot separate temperature from humidity (singular design -- one axis never varies, or the points are collinear in 1/T vs RH)" as *u8)
354 return 0
355 }
356 if rc != 0 {
357 nsvc_error(j, LAB_SVC, "stability.separate" as *u8, NSVC_ERR_BAD_ARGS, "lifetimes must all be positive" as *u8)
358 return 0
359 }
360 nsvc_ok_open(j, LAB_SVC, "stability.separate" as *u8)
361 nj_kv_int(j, "thermal_activation_energy_j_per_mol" as *u8, srh_ea_from_e10(out[0]))
362 nj_comma(j)
363 nj_kv_int(j, "moisture_sensitivity_b10_milli_per_pct_rh" as *u8, out[1])
364 nj_comma(j)
365 nj_kv_bool(j, "moisture_sensitivity_in_published_band" as *u8, srh_b10_in_published_band(out[1]))
366 nj_comma(j)
367 nj_kv_int(j, "lumped_activation_energy_j_per_mol" as *u8, stab_ich_ea_j())
368 nj_comma(j)
369 nj_kv_int(j, "design_conditioning_permil" as *u8, cond)
370 nj_comma(j)
371 // Solvable and usable are different questions, and only one of them is
372 // answered by the determinant being non-zero.
373 nj_kv_bool(j, "design_is_well_conditioned" as *u8, srh_design_is_well_conditioned(t1, r1, t2, r2, t3, r3))
374 nj_comma(j)
375 nj_kv_bool(j, "ich_pair_alone_could_separate" as *u8, srh_ich_pair_alone_is_separable())
376 nsvc_ok_close_g(j, GND_VALIDATED)
377 return 0
378}
379
380// ===== icecream.recrystal =============================================
381
382// When does this ice cream go coarse? Not a chemical question: frozen
383// dessert fails by ice crystal growth, so this verb answers in crystal
384// diameter and texture grade rather than in potency.
385//
386// It reports the steady-storage and cycled outcomes SIDE BY SIDE from the
387// same starting point, because the gap between them is the whole finding --
388// two histories at the same mean temperature do not give the same product,
389// and a consumer that only saw one number could not tell.
390func lab_icecream_recrystal(j: *NxJson, fat: i64, msnf: i64, sugar: i64, total: i64, tcold: i64, twarm: i64, cycles: i64, days: i64) -> i64 {
391 if total <= 0 {
392 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_BAD_ARGS, "total_g must be positive" as *u8)
393 return 0
394 }
395 if days < 0 {
396 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_BAD_ARGS, "days must not be negative" as *u8)
397 return 0
398 }
399 let m: *NxIceMix = nx_ice_mix_new()
400 m.fat_g = fat
401 m.msnf_g = msnf
402 m.sugar_g = sugar
403 m.total_g = total
404 if ic_water_g(m) <= 0 {
405 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_REFUSED, "solids exceed batch mass (incoherent mix)" as *u8)
406 return 0
407 }
408 let cyc: i64 = ir_cycled_fraction_permil(m, tcold, twarm)
409 if cyc == IR_INVALID {
410 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_REFUSED, "frozen fraction not computable at these temperatures (refused, not extrapolated)" as *u8)
411 return 0
412 }
413 let k: i64 = IR_K_DEFAULT
414 // Use the coupling DERIVED from a crystal size distribution rather than
415 // the asserted constant. Measurement showed the assertion optimistic by
416 // roughly 85%: it implicitly equates the number fraction of crystals lost
417 // with the mass fraction melted, and small crystals are numerous and
418 // light. Reporting the optimistic figure would under-state damage, which
419 // is the direction that calls a defective product acceptable.
420 let dist: *IceDist = dist_reference()
421 let growth: i64 = dist_volume_growth_permil(dist, cyc)
422 if growth == DIST_INVALID {
423 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_REFUSED, "cycled fraction outside the range the distribution can resolve (refused, not extrapolated)" as *u8)
424 return 0
425 }
426 let steady: i64 = ir_size_isothermal(IR_FRESH_D, k, days)
427 let cycled: i64 = ir_size_combined_growth(IR_FRESH_D, k, days, growth, cycles)
428 let optimistic: i64 = ir_size_combined(IR_FRESH_D, k, days, cyc, cycles)
429 if cycled == IR_INVALID {
430 nsvc_error(j, LAB_SVC, "icecream.recrystal" as *u8, NSVC_ERR_REFUSED, "crystal growth exceeded the model's representable range" as *u8)
431 return 0
432 }
433 nsvc_ok_open(j, LAB_SVC, "icecream.recrystal" as *u8)
434 nj_kv_int(j, "cycled_ice_fraction_permil" as *u8, cyc)
435 nj_comma(j)
436 nj_kv_int(j, "steady_diameter_deci_um" as *u8, steady)
437 nj_comma(j)
438 nj_kv_int(j, "cycled_diameter_deci_um" as *u8, cycled)
439 nj_comma(j)
440 nj_kv_int(j, "steady_texture_grade" as *u8, ir_texture_grade(steady))
441 nj_comma(j)
442 nj_kv_int(j, "cycled_texture_grade" as *u8, ir_texture_grade(cycled))
443 nj_comma(j)
444 nj_kv_int(j, "per_cycle_volume_growth_permil" as *u8, growth)
445 nj_comma(j)
446 nj_kv_int(j, "diameter_if_coupling_were_asserted" as *u8, optimistic)
447 nj_comma(j)
448 nj_kv_bool(j, "coupling_derived_from_distribution" as *u8, 1)
449 nj_comma(j)
450 nj_kv_bool(j, "distribution_is_reference_not_measured" as *u8, dist_profile_is_reference())
451 nj_comma(j)
452 // Say which numbers are load-bearing assumptions rather than physics.
453 nj_kv_bool(j, "rate_constant_is_recovered_from_data" as *u8, ir_k_is_recovered(k))
454 nj_comma(j)
455 nj_kv_bool(j, "geometric_coupling_is_asserted" as *u8, 0)
456 nj_comma(j)
457 nj_kv_bool(j, "mkt_describes_this_mechanism" as *u8, ir_mkt_describes_recrystallisation())
458 nj_comma(j)
459 nj_kv_str(j, "note" as *u8, "recrystallisation is driven by the temperature OSCILLATION, so no single effective temperature (including MKT) can represent it" as *u8)
460 // The cycled ice fraction is DERIVED from the validated freezing curve,
461 // but the geometric coupling and the default rate constant are asserted,
462 // and weakest-link governs.
463 nsvc_ok_close_g(j, GND_ASSERTED)
464 return 0
465}
466
467// ===== argv join (shared shape with the sibling facade) ===============
468
469func lab_join_args(argc: i64, argv: *i64, start: i64, out: *u8, cap: i64) -> i64 {
470 var w: i64 = 0
471 var k: i64 = start
472 while k < argc {
473 if w > 0 {
474 if w < cap { out[w] = 32 as u8; w = w + 1 }
475 }
476 let s: *u8 = argv[k] as *u8
477 var i: i64 = 0
478 while s[i] != (0 as u8) {
479 if w < cap { out[w] = s[i]; w = w + 1 }
480 i = i + 1
481 }
482 k = k + 1
483 }
484 if w < cap { out[w] = 0 as u8 } else { out[cap - 1] = 0 as u8 }
485 return w
486}
487
488// ===== dispatch =======================================================
489
490func lab_dispatch(argc: i64, argv: *i64, j: *NxJson) -> i64 {
491 let verb: *u8 = nsvc_arg(argc, argv, 1)
492 if verb[0] == (0 as u8) { lab_describe(j); return 0 }
493 if nsvc_streq(verb, "describe" as *u8) == 1 { lab_describe(j); return 0 }
494 if nsvc_streq(verb, "peptide.analyze" as *u8) == 1 {
495 let sbuf: *u8 = sys_mmap(512)
496 lab_join_args(argc, argv, 2, sbuf, 512)
497 lab_peptide_analyze(j, sbuf)
498 return 0
499 }
500 if nsvc_streq(verb, "icecream.freeze" as *u8) == 1 {
501 lab_icecream_freeze(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5), nsvc_arg_int(argc, argv, 6))
502 return 0
503 }
504 if nsvc_streq(verb, "thermal.pasteurize" as *u8) == 1 {
505 lab_thermal_pasteurize(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4))
506 return 0
507 }
508 if nsvc_streq(verb, "thermal.barrier" as *u8) == 1 {
509 lab_thermal_barrier(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3))
510 return 0
511 }
512 if nsvc_streq(verb, "stability.shelf" as *u8) == 1 {
513 lab_stability_shelf(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5), nsvc_arg_int(argc, argv, 6))
514 return 0
515 }
516 if nsvc_streq(verb, "stability.mkt" as *u8) == 1 {
517 lab_stability_mkt(j, argc, argv)
518 return 0
519 }
520 if nsvc_streq(verb, "stability.separate" as *u8) == 1 {
521 lab_stability_separate(j, argc, argv)
522 return 0
523 }
524 if nsvc_streq(verb, "icecream.recrystal" as *u8) == 1 {
525 lab_icecream_recrystal(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5), nsvc_arg_int(argc, argv, 6), nsvc_arg_int(argc, argv, 7), nsvc_arg_int(argc, argv, 8), nsvc_arg_int(argc, argv, 9))
526 return 0
527 }
528 if nsvc_streq(verb, "water.activity" as *u8) == 1 {
529 lab_water_activity(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3))
530 return 0
531 }
532 nsvc_error(j, LAB_SVC, verb, NSVC_ERR_UNKNOWN_VERB, "unknown verb; call describe for the catalog" as *u8)
533 return 1
534}
535
536func main(argc: i64, argv: *i64) -> i64 {
537 let j: *NxJson = nx_json_new(LAB_MAGIC_8192)
538 let rc: i64 = lab_dispatch(argc, argv, j)
539 nj_flush(j)
540 return rc
541}