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}