nx_water_activity_test.nx source
↩ module page · 201 lines · 10919 B
1// nx_water_activity_test.nx -- gate for nx_water_activity. The load-bearing
2// tests are T2 (a real mix lands on the published aw ~0.97 for ice cream mix)
3// and T8/T9, where the hurdle model must NAME the barrier and must refuse to
4// call an ice cream mix safe on water activity -- the reassuring wrong answer.
5// expect_exit: 0 license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_icecream.nx"
8import "nx_chem_solution.nx"
9import "nx_thermal_process.nx"
10import "nx_water_activity.nx"
11
12func t_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
13func t_putn(v: i64) -> i64 { let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) } let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } sys_write(1, bb, k); return 0 }
14
15func ref_mix() -> *NxIceMix {
16 let m: *NxIceMix = nx_ice_mix_new()
17 m.fat_g = 120
18 m.msnf_g = 100
19 m.sugar_g = 150
20 m.sugar_mw_q2 = IC_MW_SUCROSE_Q2
21 m.sugar_pod = 100
22 m.other_solids_g = 30
23 m.total_g = 1000
24 m.density_q3 = 1100
25 return m
26}
27
28func main() -> i64 {
29 var pass: i64 = 0
30 var total: i64 = 0
31
32 // --- T1 pure water is aw = 1.0000 exactly ---
33 total = total + 1
34 let pure: i64 = wa_from_solutes_q4(1000, 0)
35 t_puts("T1 pure water aw x10000 = " as *u8); t_putn(pure); t_puts(" want 10000: " as *u8)
36 if pure == 10000 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
37
38 // --- T2 a real ice cream mix lands on the published ~0.97 ---
39 total = total + 1
40 let m: *NxIceMix = ref_mix()
41 let aw: i64 = wa_from_mix_q4(m)
42 t_puts("T2 ice cream mix aw = " as *u8); t_putn(aw); t_puts(" x10000, published ~0.97 (band 9650..9850): " as *u8)
43 var ok2: i64 = 1
44 if aw < 9650 { ok2 = 0 }
45 if aw > 9850 { ok2 = 0 }
46 if ok2 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
47
48 // --- T3 aw and freezing point come from the SAME moles: more solute
49 // must both lower aw and deepen the depression ---
50 total = total + 1
51 let sweet: *NxIceMix = ref_mix()
52 sweet.sugar_g = 250
53 sweet.other_solids_g = 0
54 let aw_s: i64 = wa_from_mix_q4(sweet)
55 let fpd_ref: i64 = ic_fpd_milli_c(m)
56 let fpd_s: i64 = ic_fpd_milli_c(sweet)
57 t_puts("T3 more sugar: aw " as *u8); t_putn(aw); t_puts("->" as *u8); t_putn(aw_s); t_puts(" FPD " as *u8); t_putn(fpd_ref); t_puts("->" as *u8); t_putn(fpd_s); t_puts(" (aw down, FPD up): " as *u8)
58 var ok3: i64 = 1
59 if aw_s >= aw { ok3 = 0 }
60 if fpd_s <= fpd_ref { ok3 = 0 }
61 if ok3 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
62
63 // --- T4 growth thresholds are ordered as the literature has them ---
64 total = total + 1
65 let sal: i64 = wa_min_growth_q4(WA_ORG_SALMONELLA)
66 let cbot: i64 = wa_min_growth_q4(WA_ORG_CBOT)
67 let staph: i64 = wa_min_growth_q4(WA_ORG_STAPH)
68 let mold: i64 = wa_min_growth_q4(WA_ORG_MOLD)
69 let xero: i64 = wa_min_growth_q4(WA_ORG_XEROPHILE)
70 t_puts("T4 min aw salmonella/cbot/staph/mold/xerophile = " as *u8); t_putn(sal); t_puts("/" as *u8); t_putn(cbot); t_puts("/" as *u8); t_putn(staph); t_puts("/" as *u8); t_putn(mold); t_puts("/" as *u8); t_putn(xero); t_puts(" (descending): " as *u8)
71 var ok4: i64 = 1
72 if cbot >= sal { ok4 = 0 }
73 if staph >= cbot { ok4 = 0 }
74 if mold >= staph { ok4 = 0 }
75 if xero >= mold { ok4 = 0 }
76 if ok4 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
77
78 // --- T5 THE UNCOMFORTABLE TRUTH: the mix is NOT protected by water ---
79 total = total + 1
80 let grows_bot: i64 = wa_supports_growth(aw, WA_ORG_CBOT)
81 let grows_sal: i64 = wa_supports_growth(aw, WA_ORG_SALMONELLA)
82 let nsupp: i64 = wa_organisms_supported(aw)
83 let phf: i64 = wa_is_potentially_hazardous(aw)
84 t_puts("T5 mix aw supports C.bot=" as *u8); t_putn(grows_bot); t_puts(" salmonella=" as *u8); t_putn(grows_sal); t_puts(" organisms=" as *u8); t_putn(nsupp); t_puts("/6 PHF=" as *u8); t_putn(phf); t_puts(" want 1/1/6/1: " as *u8)
85 var ok5: i64 = 1
86 if grows_bot != 1 { ok5 = 0 }
87 if grows_sal != 1 { ok5 = 0 }
88 if nsupp != WA_ORG_N { ok5 = 0 }
89 if phf != 1 { ok5 = 0 }
90 if ok5 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
91
92 // --- T6 a dry product (honey-like, aw 0.60) stops everything ---
93 total = total + 1
94 let dry: i64 = 6000
95 let d_bot: i64 = wa_supports_growth(dry, WA_ORG_CBOT)
96 let d_mold: i64 = wa_supports_growth(dry, WA_ORG_MOLD)
97 let d_xero: i64 = wa_supports_growth(dry, WA_ORG_XEROPHILE)
98 let d_n: i64 = wa_organisms_supported(dry)
99 t_puts("T6 aw 0.60: cbot=" as *u8); t_putn(d_bot); t_puts(" mold=" as *u8); t_putn(d_mold); t_puts(" xerophile=" as *u8); t_putn(d_xero); t_puts(" total=" as *u8); t_putn(d_n); t_puts(" want 0/0/0/0: " as *u8)
100 if d_n == 0 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
101
102 // --- T7 FAIL-CLOSED: no water, bad solute, unknown organism ---
103 // An unknown hazard must read as ABLE to grow, never as absent.
104 total = total + 1
105 let nowater: i64 = wa_from_solutes_q4(0, 1000)
106 let negsol: i64 = wa_from_solutes_q4(100, 0 - 5)
107 let unk: i64 = wa_supports_growth(9000, 99)
108 let inv_grow: i64 = wa_supports_growth(WA_INVALID, WA_ORG_MOLD)
109 let inv_phf: i64 = wa_is_potentially_hazardous(WA_INVALID)
110 t_puts("T7 no-water=" as *u8); t_putn(nowater); t_puts(" neg-solute=" as *u8); t_putn(negsol); t_puts(" unknown-organism-grows=" as *u8); t_putn(unk); t_puts(" invalid-aw-grows=" as *u8); t_putn(inv_grow); t_puts(" invalid-PHF=" as *u8); t_putn(inv_phf); t_puts(": " as *u8)
111 var ok7: i64 = 1
112 if nowater != WA_INVALID { ok7 = 0 }
113 if negsol != WA_INVALID { ok7 = 0 }
114 if unk != 1 { ok7 = 0 }
115 if inv_grow != 1 { ok7 = 0 }
116 if inv_phf != 1 { ok7 = 0 }
117 if ok7 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
118
119 // --- T8 THE HURDLE MODEL NAMES THE BARRIER ---
120 total = total + 1
121 let v_dry: i64 = wa_hurdle_verdict(6600, 6000, 0, 0, 1)
122 let v_acid: i64 = wa_hurdle_verdict(3500, 9800, 0, 1, 1)
123 let v_retort: i64 = wa_hurdle_verdict(6600, 9800, TP_F0_STANDARD_MS, 0, 1)
124 let v_cold: i64 = wa_hurdle_verdict(6600, 9800, 0, 1, 0)
125 t_puts("T8 verdicts dry/acid/retort/chilled = " as *u8); t_putn(v_dry); t_puts("/" as *u8); t_putn(v_acid); t_puts("/" as *u8); t_putn(v_retort); t_puts("/" as *u8); t_putn(v_cold); t_puts(" want 1/2/3/4: " as *u8)
126 var ok8: i64 = 1
127 if v_dry != WA_SAFE_BY_AW { ok8 = 0 }
128 if v_acid != WA_SAFE_BY_ACID { ok8 = 0 }
129 if v_retort != WA_SAFE_BY_THERMAL { ok8 = 0 }
130 if v_cold != WA_SAFE_BY_COLD_CHAIN { ok8 = 0 }
131 if ok8 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
132
133 // --- T9 ICE CREAM MIX end to end: carried by the COLD CHAIN, and the
134 // model refuses every way of pretending otherwise ---
135 total = total + 1
136 let real_v: i64 = wa_hurdle_verdict(6600, aw, 0, 1, 0)
137 let no_past: i64 = wa_hurdle_verdict(6600, aw, 0, 0, 0)
138 let ambient: i64 = wa_hurdle_verdict(6600, aw, 0, 1, 1)
139 t_puts("T9 mix chilled+pasteurised=" as *u8); t_putn(real_v); t_puts(" unpasteurised=" as *u8); t_putn(no_past); t_puts(" left ambient=" as *u8); t_putn(ambient); t_puts(" want 4/0/0: " as *u8)
140 var ok9: i64 = 1
141 if real_v != WA_SAFE_BY_COLD_CHAIN { ok9 = 0 }
142 if no_past != WA_UNSAFE { ok9 = 0 }
143 if ambient != WA_UNSAFE { ok9 = 0 }
144 if ok9 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
145
146 // --- T10 the aw hurdle OVERRIDES a low pH and no heat at all ---
147 // Milk powder: low-acid, never retorted, ambient -- safe purely on water.
148 total = total + 1
149 let powder: i64 = wa_hurdle_verdict(6600, 2000, 0, 0, 1)
150 let safe_p: i64 = wa_is_safe(6600, 2000, 0, 0, 1)
151 t_puts("T10 milk powder aw 0.20 low-acid ambient unheated: verdict=" as *u8); t_putn(powder); t_puts(" is_safe=" as *u8); t_putn(safe_p); t_puts(" want 1/1: " as *u8)
152 if powder == WA_SAFE_BY_AW { if safe_p == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) } } else { t_puts("FAIL\n" as *u8) }
153
154 // --- T11 the PHF line is exactly 0.85, checked either side ---
155 total = total + 1
156 let under: i64 = wa_is_potentially_hazardous(8499)
157 let at: i64 = wa_is_potentially_hazardous(8500)
158 let vu: i64 = wa_hurdle_verdict(6600, 8499, 0, 0, 1)
159 let va: i64 = wa_hurdle_verdict(6600, 8500, 0, 0, 1)
160 t_puts("T11 aw 0.8499 PHF=" as *u8); t_putn(under); t_puts(" verdict=" as *u8); t_putn(vu); t_puts(" ; aw 0.8500 PHF=" as *u8); t_putn(at); t_puts(" verdict=" as *u8); t_putn(va); t_puts(" want 0/1 then 1/0: " as *u8)
161 var ok11: i64 = 1
162 if under != 0 { ok11 = 0 }
163 if at != 1 { ok11 = 0 }
164 if vu != WA_SAFE_BY_AW { ok11 = 0 }
165 if va != WA_UNSAFE { ok11 = 0 }
166 if ok11 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
167
168 // --- T12 inversion: solute needed for a target aw, and the honest
169 // answer when the target is unreachable by adding solute ---
170 total = total + 1
171 let need: i64 = wa_solute_umol_for_target(600, 8500)
172 let rt: i64 = wa_from_solutes_q4(600, need)
173 let bad_hi: i64 = wa_solute_umol_for_target(600, 10000)
174 let bad_lo: i64 = wa_solute_umol_for_target(600, 0)
175 t_puts("T12 solute for aw 0.85 in 600g water = " as *u8); t_putn(need); t_puts(" umol, round-trips to aw=" as *u8); t_putn(rt); t_puts(" ; aw=1.0 -> " as *u8); t_putn(bad_hi); t_puts(" aw=0 -> " as *u8); t_putn(bad_lo); t_puts(": " as *u8)
176 var d12: i64 = rt - 8500
177 if d12 < 0 { d12 = 0 - d12 }
178 var ok12: i64 = 1
179 if d12 > 5 { ok12 = 0 }
180 if bad_hi != WA_INVALID { ok12 = 0 }
181 if bad_lo != WA_INVALID { ok12 = 0 }
182 if ok12 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
183
184 // --- T13 aw is monotone decreasing in solute (no inversions) ---
185 total = total + 1
186 var s13: i64 = 0
187 var prev13: i64 = 10001
188 var ok13: i64 = 1
189 while s13 <= 20000000 {
190 let a: i64 = wa_from_solutes_q4(600, s13)
191 if a > prev13 { ok13 = 0 }
192 prev13 = a
193 s13 = s13 + 1000000
194 }
195 t_puts("T13 aw monotone non-increasing in solute (0..20 mol): " as *u8)
196 if ok13 == 1 { pass = pass + 1; t_puts("PASS\n" as *u8) } else { t_puts("FAIL\n" as *u8) }
197
198 t_puts("WATER-ACTIVITY-GATE passed " as *u8); t_putn(pass); t_puts("/" as *u8); t_putn(total)
199 if pass == total { t_puts(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
200 t_puts(" verdict=RED\n" as *u8); sys_exit(1); return 1
201}