code wiki / (root) / nx_thermal_profile.nx

nx_thermal_profile.nx source

↩ module page · 177 lines · 6574 B

1// nx_thermal_profile.nx -- FOOD-SCIENCE SUITE / PROCESS INTEGRATION rung. 2// nx_thermal_process evaluates a single hold at a single temperature. A real 3// retort or HTST run is a CURVE: it comes up to temperature, holds, and cools 4// back down, and the product is being killed the whole time. This integrates 5// lethality over that whole profile -- the General Method. 6// 7// F = SUM over segments of dt_i * 10^((T_i - T_ref)/z) 8// 9// WHY IT MATTERS, AND IT IS NOT A ROUNDING CORRECTION. Treating a process 10// as hold-only is conservative, which sounds harmless -- but the come-up on 11// a typical retort contributes lethality COMPARABLE TO THE ENTIRE HOLD. Ten 12// minutes ramping from 102 C to 120 C delivers roughly 2 minutes of F0 on 13// its own, next to a 2.52 minute 12D cook. A process authority that ignores 14// it does not get a safer product; it gets an OVER-processed one, and 15// over-processing is exactly how a food is made safe and inedible at the 16// same time. So the honest direction of the hold-only error is "wasteful", 17// not "fine". 18// 19// SEGMENTS LIVE IN MMAP'D ARRAYS, NOT BSS. A module-level `static [N]i64` 20// silently kills a handler-module process on startup in this toolchain, so 21// the arrays are lazily mmap'd and reached through pointers -- the standard 22// workaround here, not a stylistic choice. 23// 24// EVERY BOUND IS FAIL-CLOSED. A segment past capacity is REFUSED rather 25// than dropped, because a silently-truncated profile reports LESS lethality 26// than the process really delivers, and an operator would respond by 27// cooking longer -- the failure would hide behind a plausible number. 28// 29// Units match nx_thermal_process exactly: milli-Celsius, milliseconds. 30// 31// Grounding (cited; researcher-groundable): 32// bigelow_general_method_thermal_process_integration 33// stumbo_thermobacteriology_d_z_f0_model 34// c_botulinum_proteolytic_d121_0_21min_z10 35// 36// genealogy_id: thermal_process_authority + nishi_food_science_suite 37 38import "nx_syscalls.nx" 39import "nx_pow10.nx" 40import "nx_thermal_process.nx" 41 42const TPR_MAX_SEGMENTS: i64 = 256 43const TPR_REFUSED: i64 = 0 - 1 44 45struct NxThermalProfile { 46 temps: *i64, 47 durs: *i64, 48 n: i64, 49 cap: i64, 50 valid: i64, 51} 52 53func nx_thermal_profile_new(cap: i64) -> *NxThermalProfile { 54 let p: *NxThermalProfile = (sys_mmap(64)) as *NxThermalProfile 55 var c: i64 = cap 56 if c <= 0 { c = 1 } 57 if c > TPR_MAX_SEGMENTS { c = TPR_MAX_SEGMENTS } 58 p.temps = (sys_mmap(c * 8)) as *i64 59 p.durs = (sys_mmap(c * 8)) as *i64 60 p.n = 0 61 p.cap = c 62 p.valid = 1 63 return p 64} 65 66// Append one isothermal segment. A zero or negative duration is REFUSED 67// rather than ignored: it means the caller's profile is malformed, and 68// quietly skipping it would under-report lethality. 69func tpr_add_segment(p: *NxThermalProfile, temp_milli: i64, dur_ms: i64) -> i64 { 70 if p.valid != 1 { return TPR_REFUSED } 71 if dur_ms <= 0 { p.valid = 0; return TPR_REFUSED } 72 if p.n >= p.cap { p.valid = 0; return TPR_REFUSED } 73 let t: *i64 = p.temps 74 let d: *i64 = p.durs 75 t[p.n] = temp_milli 76 d[p.n] = dur_ms 77 p.n = p.n + 1 78 return p.n 79} 80 81// Approximate a linear ramp with n_steps isothermal segments, each held at 82// the MIDPOINT of its interval. Midpoint rather than endpoint because 83// lethality is convex in temperature: sampling at the start understates it 84// and at the end overstates it, while the midpoint is the standard 85// compromise and is what the General Method assumes. 86func tpr_add_ramp(p: *NxThermalProfile, t_start_milli: i64, t_end_milli: i64, dur_ms: i64, n_steps: i64) -> i64 { 87 if n_steps <= 0 { p.valid = 0; return TPR_REFUSED } 88 if dur_ms <= 0 { p.valid = 0; return TPR_REFUSED } 89 let seg_ms: i64 = dur_ms / n_steps 90 if seg_ms <= 0 { p.valid = 0; return TPR_REFUSED } 91 let span: i64 = t_end_milli - t_start_milli 92 var i: i64 = 0 93 while i < n_steps { 94 let num: i64 = span * (2 * i + 1) 95 let mid: i64 = t_start_milli + num / (2 * n_steps) 96 tpr_add_segment(p, mid, seg_ms) 97 i = i + 1 98 } 99 return p.n 100} 101 102func tpr_total_time_ms(p: *NxThermalProfile) -> i64 { 103 if p.valid != 1 { return 0 } 104 let d: *i64 = p.durs 105 var i: i64 = 0 106 var sum: i64 = 0 107 while i < p.n { 108 sum = sum + d[i] 109 i = i + 1 110 } 111 return sum 112} 113 114func tpr_peak_temp_milli(p: *NxThermalProfile) -> i64 { 115 if p.valid != 1 { return 0 } 116 let t: *i64 = p.temps 117 var i: i64 = 0 118 var hi: i64 = 0 119 while i < p.n { 120 if t[i] > hi { hi = t[i] } 121 i = i + 1 122 } 123 return hi 124} 125 126// ===== The integral =================================================== 127// 128// Accumulated equivalent time at the reference temperature. Each segment is 129// converted by nx_thermal_process's own equivalence routine, so the 130// precision discipline there (divide by the positive power for a negative 131// exponent, never multiply a Q3 reciprocal) is inherited rather than 132// re-derived. 133 134func tpr_lethality_ms(p: *NxThermalProfile, t_ref_milli: i64, z_milli: i64) -> i64 { 135 if p.valid != 1 { return TPR_REFUSED } 136 if p.n <= 0 { return TPR_REFUSED } 137 let t: *i64 = p.temps 138 let d: *i64 = p.durs 139 var i: i64 = 0 140 var acc: i64 = 0 141 while i < p.n { 142 let e: i64 = tp_equiv_time_at_ref_ms(d[i], t[i], t_ref_milli, z_milli) 143 acc = acc + e 144 i = i + 1 145 } 146 return acc 147} 148 149// F0 of the whole profile: 121.1 C, z = 10 C. 150func tpr_f0_ms(p: *NxThermalProfile) -> i64 { 151 return tpr_lethality_ms(p, TP_REF_STERIL_MILLI_C, TP_BOT_Z_MILLI) 152} 153 154func tpr_bot_cook_adequate(p: *NxThermalProfile) -> i64 { 155 let f0: i64 = tpr_f0_ms(p) 156 if f0 == TPR_REFUSED { return 0 } 157 return tp_bot_cook_adequate(f0) 158} 159 160// How much lethality a hold-only calculation THROWS AWAY: the profile's 161// true F0 minus the F0 credited to its peak-temperature hold alone. 162// Positive means the ramps are doing real work that hold-only ignores. 163func tpr_ramp_credit_ms(p: *NxThermalProfile, hold_ms: i64) -> i64 { 164 let full: i64 = tpr_f0_ms(p) 165 if full == TPR_REFUSED { return 0 } 166 let peak: i64 = tpr_peak_temp_milli(p) 167 let holdonly: i64 = tp_f0_ms(hold_ms, peak) 168 return full - holdonly 169} 170 171// Log reductions the whole profile delivers against an organism given by its 172// reference D-value at the reference temperature. 173func tpr_log_reductions_q3(p: *NxThermalProfile, d_ref_ms: i64, t_ref_milli: i64, z_milli: i64) -> i64 { 174 let f: i64 = tpr_lethality_ms(p, t_ref_milli, z_milli) 175 if f == TPR_REFUSED { return 0 } 176 return tp_log_reductions_q3(f, d_ref_ms) 177}