code wiki / (root) / nx_shelf_life.nx

nx_shelf_life.nx source

↩ module page · 122 lines · 5263 B

1// nx_shelf_life.nx -- FOOD-SCIENCE SUITE / SHELF-LIFE rung. Accelerated 2// shelf-life testing (ASLT) via the Q10 model, plus a cold-chain breach 3// accumulator that tracks the FRACTION of shelf life a product consumes 4// across its real temperature history -- so "best by" reflects how the 5// food was actually stored, not a fixed date. 6// 7// Q10 = ratio by which the quality-loss RATE changes per 10 C. Shelf life 8// scales as Q10^(-dT/10): 10 C warmer -> life / Q10; 10 C colder -> life x 9// Q10. INTEGER-EXACT at decade steps (same powers-of-integer trick as the 10// preservation z-value). 11// 12// THE exceed: a printed date assumes one storage temperature forever. This 13// computes life at any decade temperature AND accumulates consumed life 14// across segments, so a cold-chain breach shortens the remaining date by 15// construction (fraction consumed >= 1.000 -> expired). 16// 17// ===== THE DECADE-ONLY GAP, NOW CLOSED ============================ 18// 19// sl_at_temp returns -1 for any temperature difference that is not a 20// multiple of 10 C. That was documented as an honest gap, but it made the 21// organ unable to answer the question the whole category rests on: ICH 22// stability compares 25 C with 40 C, a FIFTEEN degree gap. The organ 23// refused the one comparison that matters commercially. 24// 25// sl_at_temp_frac closes it by moving into decade-log space, where a 26// fractional power is just a multiplication: 27// Q10^(-dT/10) = 10^(-dT*log10(Q10)/10) 28// Both halves already existed in nx_pow10 (this suite's own shared numeric 29// rung), so this is a CHANGE OF VARIABLE, not a second model. 30// 31// ADDITIVE, per the API-stability rule: sl_at_temp is untouched and every 32// existing caller and test is unaffected. Where both are defined the new 33// path DELEGATES to the exact integer one -- an interpolated logarithm is 34// never better than exact repeated multiplication. sl_scale_frac_q3 is 35// exposed separately so the gate can compare the interpolated path against 36// the exact one head-on; testing only the dispatcher would pass vacuously 37// at decade steps by measuring the exact path twice. 38// 39// grounded: q10_accelerated_shelf_life_testing + arrhenius_food_quality_kinetics 40// genealogy_id: aslt_q10_model + nishi_food_science_suite 41 42import "nx_syscalls.nx" 43import "nx_pow10.nx" 44const K_MAGIC_1000000: i64 = 1000000 45 46// Q10^steps for a non-negative integer number of decade steps (guarded). 47func sl_q10_pow(q10: i64, steps: i64) -> i64 { 48 if steps < 0 { return 0 } 49 if steps > 18 { return 0 } 50 var v: i64 = 1 51 var i: i64 = 0 52 while i < steps { 53 v = v * q10 54 i = i + 1 55 } 56 return v 57} 58 59// Shelf life (days) at temperature t_c given a reference life at t_ref_c. 60// Warmer shortens (divide), colder lengthens (multiply). Requires a 61// decade-aligned temperature difference; returns -1 otherwise. 62func sl_at_temp(sl_ref_days: i64, tref_c: i64, q10: i64, t_c: i64) -> i64 { 63 let delta: i64 = t_c - tref_c 64 if delta % 10 != 0 { return -1 } 65 let steps: i64 = delta / 10 66 if steps >= 0 { return sl_ref_days / sl_q10_pow(q10, steps) } 67 return sl_ref_days * sl_q10_pow(q10, 0 - steps) 68} 69 70// Shelf-life SCALE FACTOR (Q3, so 1000 = unchanged) for moving `delta_c` 71// degrees away from the reference, given a Q10 in Q3 (2000 = a Q10 of 2.0). 72// Warmer (positive delta) shrinks life, colder lengthens it. 73// 74// This is the interpolated path, exposed on purpose: the gate points it at 75// decade steps where the exact answer is independently known, so the two 76// paths can be compared instead of one of them being taken on trust. 77// Agreement there is measured, not claimed -- see nx_shelf_life_test. 78func sl_scale_frac_q3(q10_q3: i64, delta_c: i64) -> i64 { 79 var lg: i64 = 0 80 if q10_q3 <= 1000 { return 0 - 1 } 81 lg = ilog10_milli(q10_q3) 82 if lg == IP10_INVALID { return 0 - 1 } 83 return ipow10_q3(0 - delta_c * lg / 10) 84} 85 86// Shelf life (days) at ANY temperature, decade-aligned or not. 87// Exact wherever sl_at_temp is defined (it delegates), interpolated between. 88func sl_at_temp_frac(sl_ref_days: i64, tref_c: i64, q10_q3: i64, t_c: i64) -> i64 { 89 var delta: i64 = 0 90 var scale: i64 = 0 91 if sl_ref_days <= 0 { return 0 - 1 } 92 if q10_q3 <= 1000 { return 0 - 1 } 93 delta = t_c - tref_c 94 // Prefer the exact integer path wherever it is defined. 95 if q10_q3 % 1000 == 0 { 96 if delta % 10 == 0 { 97 return sl_at_temp(sl_ref_days, tref_c, q10_q3 / 1000, t_c) 98 } 99 } 100 scale = sl_scale_frac_q3(q10_q3, delta) 101 if scale == 0 - 1 { return 0 - 1 } 102 return sl_ref_days * scale / 1000 103} 104 105// Fraction of shelf life consumed (x1000) by holding `days` at a temperature 106// whose shelf life is sl_days. Absent/zero life -> fully consumed. 107func sl_life_consumed_milli(days: i64, sl_days: i64) -> i64 { 108 if sl_days <= 0 { return K_MAGIC_1000000 } 109 return days * 1000 / sl_days 110} 111 112// Expired once the accumulated consumed fraction reaches 1.000 (1000). 113func sl_is_expired(consumed_milli: i64) -> i64 { 114 if consumed_milli >= 1000 { return 1 } 115 return 0 116} 117 118// Remaining shelf-life fraction (x1000), floored at zero. 119func sl_remaining_milli(consumed_milli: i64) -> i64 { 120 if consumed_milli >= 1000 { return 0 } 121 return 1000 - consumed_milli 122}