code wiki / (root) / nx_labsci_svc.nx

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}