nx_colorsci_gate.nx source
↩ module page · 424 lines · 26222 B
1// nx_colorsci_gate.nx -- THE GATE FOR THE COLOUR-SCIENCE RULER (nx_colorsci_lib + nx_fixq30_lib), 2026-08-24.
2//
3// SUBJECT: the libs in-process (they have no main; the ruler IS the code under test). Teeth, in order:
4// T1 the DERIVED constants against their published digits -- pi (Machin) and e to 1e-7, and the identities
5// sqrt(2)^2 = 2, atan2(0,-1) = pi, sin(pi/2) = 1, cos(pi/3) = 1/2, cbrt(27) = 3, ln(exp(3)) = 3. A CORDIC
6// whose table was mistyped cannot pass these; a series that stopped early cannot either.
7// T2 CIEDE2000 against Sharma-Wu-Dalal 2005 Table I: ALL 34 pairs (gv_subjects binds the denominator), both
8// directions (the formula is symmetric by construction, and pairs 7/8 exist to prove it), and the table's
9// h_bar_prime column (the arctangent branch handling pairs 9-16 exist to trap).
10// T3 NEG-CONTROL: pair 1 against a WRONG expectation (+0.5 dE00) must FAIL the tolerance -- a tolerance so
11// wide that anything passes would read green on every KAT row and prove nothing.
12// T4 ICC: the mirrored sRGB2014.icc (color.org) parses; rXYZ+gXYZ+bXYZ sum to wtpt within s15Fixed16
13// resolution (an ICC structural invariant, not a typed expectation); white reads L=100 a=b=0; black L=0;
14// EVERY one of the 256 greys reads a=b=0 (the whole population, not a sample); rgb->lab->rgb round-trips
15// within one code over all greys and the eight cube corners.
16// T5 the gamut descriptor: derived from the profile at the conf's sampling; every sample it was built from
17// reads INSIDE; a chroma no display reaches reads OUTSIDE; bite-proven both ways; chroma clipping brings
18// the outside probe inside at the same L and hue.
19// Thresholds: the KAT tolerance is the half-ulp of a four-decimal table plus Q30 headroom; everything else is a
20// structural identity. license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
21import "nx_syscalls.nx"
22import "nx_gate_verdict.nx"
23import "nx_colorsci_lib.nx"
24
25const CG_KAT_PATH: *u8 = "knowledge/colorsci_ciede2000_kat.tsv"
26const CG_PROFILE: *u8 = "knowledge/fetched/cmp_phototwin_srgb2014.b64" // base64 transport of sRGB2014.icc (Google skcms mirror of color.org)
27const CG_CONF: *u8 = "knowledge/colorsci.conf"
28const CG_KAT_ROWS: i64 = 34 // Table I declares 34 pairs; another count is a partial table, refused
29const CG_KAT_FIELDS: i64 = 9
30const CG_F_L1: i64 = 1
31const CG_F_A1: i64 = 2
32const CG_F_B1: i64 = 3
33const CG_F_L2: i64 = 4
34const CG_F_A2: i64 = 5
35const CG_F_B2: i64 = 6
36const CG_F_HBAR: i64 = 7
37const CG_F_DE: i64 = 8
38const CG_DE_TOL_MICRO: i64 = 60 // half-ulp of a 4-decimal table (50) + Q30 arithmetic headroom (10)
39const CG_H_TOL_MICRO: i64 = 1000 // MEASURED 2026-08-24: the table's h' column is NOT four-decimal exact -- pair 21's own printed
40 // a'=4.7596, b=0.5854 give h'=7.0120 by direct arctangent while the table prints 7.0113; so the
41 // column is good to ~1e-3 degree, and that is the bound applied (dE00 itself holds at 6e-5)
42const CG_NANO: i64 = 1000000000
43const CG_PI_NANO: i64 = 3141592654 // pi, published digits (the KAT oracle for the Machin derivation)
44const CG_E_NANO: i64 = 2718281828 // e, published digits
45const CG_NANO_TOL: i64 = 100 // 1e-7: the Q30 series accumulate ~4e-8, doubled
46const CG_THREE: i64 = 3
47const CG_TWO: i64 = 2
48const CG_SQRT_ARG: i64 = 2
49const CG_CBRT_ARG: i64 = 27
50const CG_LN_ARG: i64 = 3
51// MEASURED 2026-08-24 on the mirrored sRGB2014.icc itself: rXYZ+gXYZ+bXYZ = (0.964279, 0.999969, 0.825088) against
52// wtpt (0.964202, 1.000000, 0.824905) -- the canonical profile's own primaries sum to its white point only to 1.8e-4
53// (the Bradford-adapted primaries were rounded independently of the white tag). That discrepancy, not s15Fixed16
54// resolution, is the structural bound; 2^18 Q30 units = 2.4e-4 carries it with headroom and is still 1000x below
55// any colour anyone can see. It propagates through f'(1) = 1/3 and the 200x b-scale to |b| ~ 0.012 on white
56// (measured 0.017), so the neutral-axis tolerance is 0.05 with the same headroom.
57const CG_WHITE_TOL_MICRO: i64 = 50000
58const CG_L_WHITE: i64 = 100
59const CG_S15_SUM_TOL_Q30: i64 = 262144
60const CG_GREYS: i64 = 256
61const CG_CODE_MAX: i64 = 255
62const CG_CODE_TOL: i64 = 1
63const CG_CORNERS: i64 = 8
64const CG_TAB: i64 = 9
65const CG_NL: i64 = 10
66const CG_HASH: i64 = 35
67const CG_ROW_SLOTS: i64 = 16
68const CG_BITE_DE_OFFSET_MICRO: i64 = 500000 // the planted-wrong expectation: +0.5 dE00
69const CG_OUT_L: i64 = 50
70const CG_OUT_A: i64 = 150 // a chroma of 150 at L=50: outside every display-class gamut
71const CG_GREY_MID: i64 = 128 // a mid grey: strictly inside on every channel
72const CG_BOUNDARY_SLACK_PERMIL: i64 = 1 // a cube-face sample sits ON the boundary: 1000 permil plus/minus one unit of Q30 rounding
73const CG_PERMIL: i64 = 1000
74const CG_SLOT: i64 = 8
75const CG_KAT_CAP: i64 = 65536
76
77func cg_nano(x: i64) -> i64 { if x >= 0 { return (x * CG_NANO) >> FQ_SHIFT } return 0 - (((0 - x) * CG_NANO) >> FQ_SHIFT) }
78func cg_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
79// tab field idx of line [s,e) -> box[0]=off box[1]=len; 0 if absent
80func cg_field(buf: *u8, s: i64, e: i64, idx: i64, box: *i64) -> i64 {
81 var p: i64 = s
82 var f: i64 = 0
83 while f < idx {
84 var go: i64 = 1
85 while go == 1 {
86 if p >= e { go = 0 } else { if (buf[p] as i64) == CG_TAB { p = p + 1; go = 0 } else { p = p + 1 } }
87 }
88 f = f + 1
89 }
90 if p >= e { box[0] = 0; box[1] = 0; return 0 }
91 var q: i64 = p
92 var g2: i64 = 1
93 while g2 == 1 {
94 if q >= e { g2 = 0 } else { if (buf[q] as i64) == CG_TAB { g2 = 0 } else { q = q + 1 } }
95 }
96 box[0] = p
97 box[1] = q - p
98 return 1
99}
100func cg_fld_q30(buf: *u8, s: i64, e: i64, idx: i64, box: *i64) -> i64 {
101 cg_field(buf, s, e, idx, box)
102 return fq_from_micro(fq_parse_micro(buf, box[0], box[1]))
103}
104func cg_within(x: i64, y: i64, tol_micro: i64) -> i64 {
105 if cg_abs(fq_to_micro(x - y)) <= tol_micro { return 1 }
106 return 0
107}
108
109func main(argc: i64, argv: *i64) -> i64 {
110 let ctr: *i64 = gv_ctr()
111 gv_head("nx_colorsci gate -- CIELAB, CIEDE2000, ICC matrix/TRC and the gamut descriptor, KAT against the published table" as *u8)
112 let cs: *i64 = cs_ctx()
113 let fq: *i64 = cs_fq(cs)
114
115 // ---- T1: derived constants and identities ----
116 let pin: i64 = cg_nano(fq[FQ_C_PI])
117 gv_puts(" [T1] pi_nano=" as *u8); gv_num(pin); gv_puts(" e_nano=" as *u8)
118 let en: i64 = cg_nano(fq_exp(fq, FQ_ONE))
119 gv_num(en); gv_puts("\n" as *u8)
120 var t: i64 = 0
121 if cg_abs(pin - CG_PI_NANO) <= CG_NANO_TOL { t = 1 }
122 gv_check("pi-derived-by-machin-matches-published-digits-to-1e-7" as *u8, t, ctr)
123 // fq_div DOMAIN GUARD (2026-08-24): 1 / 2^24 with the denominator handed in at 2^54 (out of the 2^47 contract) must come
124 // back as exactly 64 (= 2^30 / 2^24). Before the guard the remainder step wrapped and the answer was garbage.
125 let big_den: i64 = FQ_DIV_MAX_DEN * 128
126 let dq: i64 = fq_div(FQ_ONE, big_den)
127 gv_puts(" fq_div_domain: den=2^54 got=" as *u8); gv_num(dq); gv_puts(" want=64\n" as *u8)
128 gv_check("fq_div-out-of-contract-denominator-reduces-instead-of-wrapping (1/2^24 == 64 in Q30)" as *u8, (dq == 64) as i64, ctr)
129 t = 0
130 if cg_abs(en - CG_E_NANO) <= CG_NANO_TOL { t = 1 }
131 gv_check("e-derived-by-series-matches-published-digits-to-1e-7" as *u8, t, ctr)
132 let s2: i64 = fq_sqrt(fq_from_int(CG_SQRT_ARG))
133 t = 0
134 if cg_abs(cg_nano(fq_mul(s2, s2) - fq_from_int(CG_SQRT_ARG))) <= CG_NANO_TOL { t = 1 }
135 gv_check("sqrt-two-squared-is-two" as *u8, t, ctr)
136 t = 0
137 if cg_abs(cg_nano(fq_atan2(fq, 0, 0 - FQ_ONE) - fq[FQ_C_PI])) <= CG_NANO_TOL { t = 1 }
138 gv_check("cordic-atan2-of-minus-one-is-pi" as *u8, t, ctr)
139 t = 0
140 if cg_abs(cg_nano(fq_sin(fq, fq[FQ_C_PI] / CG_TWO) - FQ_ONE)) <= CG_NANO_TOL { t = 1 }
141 gv_check("cordic-sin-half-pi-is-one" as *u8, t, ctr)
142 t = 0
143 if cg_abs(cg_nano(fq_cos(fq, fq[FQ_C_PI] / CG_THREE) - FQ_HALF)) <= CG_NANO_TOL { t = 1 }
144 gv_check("cordic-cos-third-pi-is-half" as *u8, t, ctr)
145 t = 0
146 if cg_abs(cg_nano(fq_cbrt(fq, fq_from_int(CG_CBRT_ARG)) - fq_from_int(CG_THREE))) <= CG_NANO_TOL { t = 1 }
147 gv_check("cbrt-27-is-3" as *u8, t, ctr)
148 t = 0
149 if cg_abs(cg_nano(fq_ln(fq, fq_exp(fq, fq_from_int(CG_LN_ARG))) - fq_from_int(CG_LN_ARG))) <= CG_NANO_TOL { t = 1 }
150 gv_check("ln-exp-3-is-3" as *u8, t, ctr)
151
152 // ---- T2: the published table, every row, both directions ----
153 let lp: *i64 = sys_mmap(CG_SLOT * CG_TWO) as *i64
154 let kb: *u8 = sys_read_file(CG_KAT_PATH, lp)
155 var have_kat: i64 = 0
156 if (kb as i64) != 0 { if lp[0] > 0 { have_kat = 1 } }
157 let box: *i64 = sys_mmap(CG_SLOT * CG_TWO) as *i64
158 let o4: *i64 = sys_mmap(CG_SLOT * CG_ROW_SLOTS) as *i64
159 let o4b: *i64 = sys_mmap(CG_SLOT * CG_ROW_SLOTS) as *i64
160 var rows: i64 = 0
161 var within: i64 = 0
162 var sym: i64 = 0
163 var hue_ok: i64 = 0
164 var worst_pair: i64 = 0
165 var worst_micro: i64 = 0
166 var bite_bad: i64 = 0
167 var bite_good: i64 = 1
168 if have_kat == 1 {
169 let n: i64 = lp[0]
170 var p: i64 = 0
171 while p < n {
172 var e: i64 = p
173 while e < n { if (kb[e] as i64) == CG_NL { break } e = e + 1 }
174 if e > p { if (kb[p] as i64) != CG_HASH {
175 let L1: i64 = cg_fld_q30(kb, p, e, CG_F_L1, box)
176 let a1: i64 = cg_fld_q30(kb, p, e, CG_F_A1, box)
177 let b1: i64 = cg_fld_q30(kb, p, e, CG_F_B1, box)
178 let L2: i64 = cg_fld_q30(kb, p, e, CG_F_L2, box)
179 let a2: i64 = cg_fld_q30(kb, p, e, CG_F_A2, box)
180 let b2: i64 = cg_fld_q30(kb, p, e, CG_F_B2, box)
181 let hb: i64 = cg_fld_q30(kb, p, e, CG_F_HBAR, box)
182 let de: i64 = cg_fld_q30(kb, p, e, CG_F_DE, box)
183 cg_field(kb, p, e, 0, box)
184 let pair: i64 = fq_parse_micro(kb, box[0], box[1]) / FQ_MICRO
185 cs_de2000_ex(cs, L1, a1, b1, L2, a2, b2, o4)
186 cs_de2000_ex(cs, L2, a2, b2, L1, a1, b1, o4b)
187 let dm: i64 = cg_abs(fq_to_micro(o4[0] - de))
188 if dm > worst_micro { worst_micro = dm; worst_pair = pair }
189 if dm <= CG_DE_TOL_MICRO { within = within + 1 }
190 if cg_abs(fq_to_micro(o4[0] - o4b[0])) <= CG_DE_TOL_MICRO { sym = sym + 1 }
191 if cg_within(o4[1], hb, CG_H_TOL_MICRO) == 1 { hue_ok = hue_ok + 1 }
192 if rows == 0 {
193 // neg-control on the first pair: a planted +0.5 expectation must be rejected, the real one accepted
194 if cg_abs(fq_to_micro(o4[0] - (de + fq_from_micro(CG_BITE_DE_OFFSET_MICRO)))) > CG_DE_TOL_MICRO { bite_bad = 1 }
195 if dm <= CG_DE_TOL_MICRO { bite_good = 0 }
196 }
197 gv_puts(" pair " as *u8); gv_num(pair); gv_puts(" de_micro=" as *u8); gv_num(fq_to_micro(o4[0]))
198 gv_puts(" expected_micro=" as *u8); gv_num(fq_to_micro(de)); gv_puts(" hbar_micro=" as *u8); gv_num(fq_to_micro(o4[1]))
199 gv_puts("\n" as *u8)
200 rows = rows + 1
201 } }
202 p = e + 1
203 }
204 }
205 gv_need("ciede2000 KAT table readable (knowledge/colorsci_ciede2000_kat.tsv)" as *u8, have_kat, ctr)
206 gv_subjects("ciede2000-kat-pairs" as *u8, rows, ctr)
207 gv_puts(" [T2] rows=" as *u8); gv_num(rows); gv_puts(" within=" as *u8); gv_num(within)
208 gv_puts(" symmetric=" as *u8); gv_num(sym); gv_puts(" hue_ok=" as *u8); gv_num(hue_ok)
209 gv_puts(" worst_pair=" as *u8); gv_num(worst_pair); gv_puts(" worst_micro=" as *u8); gv_num(worst_micro); gv_puts("\n" as *u8)
210 t = 0
211 if rows == CG_KAT_ROWS { t = 1 }
212 gv_check("kat-table-carries-all-34-published-pairs" as *u8, t, ctr)
213 t = 0
214 if rows > 0 { if within == rows { t = 1 } }
215 gv_check("de2000-matches-every-published-pair-within-half-ulp" as *u8, t, ctr)
216 t = 0
217 if rows > 0 { if sym == rows { t = 1 } }
218 gv_check("de2000-symmetric-on-every-pair" as *u8, t, ctr)
219 t = 0
220 if rows > 0 { if hue_ok == rows { t = 1 } }
221 gv_check("h-bar-prime-matches-every-published-pair" as *u8, t, ctr)
222 gv_bite("neg-control-de2000-planted-wrong-expectation-is-rejected" as *u8, bite_bad, bite_good, ctr)
223
224 // ---- T4: ICC matrix/TRC profile ----
225 let prof: *i64 = cs_icc_load(cs, CG_PROFILE)
226 gv_puts(" [T4] icc bytes=" as *u8); gv_num(prof[CS_P_BYTES]); gv_puts(" tags=" as *u8); gv_num(prof[CS_P_TAGS])
227 gv_puts(" have=" as *u8); gv_num(prof[CS_P_HAVE]); gv_puts(" ok=" as *u8); gv_num(prof[CS_P_OK]); gv_puts("\n" as *u8)
228 let icc_ok: i64 = gv_need("sRGB2014.icc readable with rXYZ gXYZ bXYZ wtpt rTRC gTRC bTRC (knowledge/fetched)" as *u8, prof[CS_P_OK], ctr)
229 let lab: *i64 = sys_mmap(CG_SLOT * CG_THREE) as *i64
230 let rgb: *i64 = sys_mmap(CG_SLOT * CG_THREE) as *i64
231 if icc_ok == 1 {
232 let sx: i64 = prof[CS_P_MAT] + prof[CS_P_MAT + 1] + prof[CS_P_MAT + 2]
233 let sy: i64 = prof[CS_P_MAT + 3] + prof[CS_P_MAT + 4] + prof[CS_P_MAT + 5]
234 let sz: i64 = prof[CS_P_MAT + 6] + prof[CS_P_MAT + 7] + prof[CS_P_MAT + 8]
235 gv_puts(" wtpt_micro=" as *u8); gv_num(fq_to_micro(prof[CS_P_WTPT])); gv_puts("," as *u8); gv_num(fq_to_micro(prof[CS_P_WTPT + 1])); gv_puts("," as *u8); gv_num(fq_to_micro(prof[CS_P_WTPT + 2]))
236 gv_puts(" colsum_micro=" as *u8); gv_num(fq_to_micro(sx)); gv_puts("," as *u8); gv_num(fq_to_micro(sy)); gv_puts("," as *u8); gv_num(fq_to_micro(sz)); gv_puts("\n" as *u8)
237 t = 0
238 if cg_abs(sx - prof[CS_P_WTPT]) <= CG_S15_SUM_TOL_Q30 { if cg_abs(sy - prof[CS_P_WTPT + 1]) <= CG_S15_SUM_TOL_Q30 { if cg_abs(sz - prof[CS_P_WTPT + 2]) <= CG_S15_SUM_TOL_Q30 { t = 1 } } }
239 gv_check("icc-primaries-sum-to-the-white-point-within-s15fixed16" as *u8, t, ctr)
240 // THE DIRECT DETECTOR for the 2026-08-24 defect: a TRC table parsed through an overflowing divide is not monotone.
241 let mlr: *i64 = prof[CS_P_LUT_R] as *i64
242 let mlg: *i64 = prof[CS_P_LUT_G] as *i64
243 let mlb: *i64 = prof[CS_P_LUT_B] as *i64
244 var mono: i64 = 1
245 var mono_break: i64 = 0 - 1
246 var tc: i64 = 1
247 while tc < CS_LUT_ENTRIES {
248 if mlr[tc] < mlr[tc - 1] { if mono == 1 { mono_break = tc } mono = 0 }
249 if mlg[tc] < mlg[tc - 1] { if mono == 1 { mono_break = tc } mono = 0 }
250 if mlb[tc] < mlb[tc - 1] { if mono == 1 { mono_break = tc } mono = 0 }
251 tc = tc + 1
252 }
253 gv_puts(" trc_monotone=" as *u8); gv_num(mono); gv_puts(" first_break_code=" as *u8); gv_num(mono_break); gv_puts(" lut_mid_micro=" as *u8); gv_num(fq_to_micro(mlr[CG_GREY_MID])); gv_puts("\n" as *u8)
254 gv_check("trc-table-monotone-non-decreasing-over-all-256-codes (a garbage LUT cannot pass)" as *u8, mono, ctr)
255 cs_prof_rgb_to_lab(cs, prof, CG_CODE_MAX, CG_CODE_MAX, CG_CODE_MAX, lab)
256 gv_puts(" white_lab_micro=" as *u8); gv_num(fq_to_micro(lab[0])); gv_puts("," as *u8); gv_num(fq_to_micro(lab[1])); gv_puts("," as *u8); gv_num(fq_to_micro(lab[2])); gv_puts("\n" as *u8)
257 t = 0
258 if cg_within(lab[0], fq_from_int(CG_L_WHITE), CG_WHITE_TOL_MICRO) == 1 { if cg_abs(fq_to_micro(lab[1])) <= CG_WHITE_TOL_MICRO { if cg_abs(fq_to_micro(lab[2])) <= CG_WHITE_TOL_MICRO { t = 1 } } }
259 gv_check("profile-white-reads-L100-a0-b0" as *u8, t, ctr)
260 cs_prof_rgb_to_lab(cs, prof, 0, 0, 0, lab)
261 t = 0
262 if cg_abs(fq_to_micro(lab[0])) <= CG_WHITE_TOL_MICRO { t = 1 }
263 gv_check("profile-black-reads-L0" as *u8, t, ctr)
264 var grey_ok: i64 = 0
265 var rt_ok: i64 = 0
266 var diag: i64 = 0
267 var c: i64 = 0
268 while c < CG_GREYS {
269 cs_prof_rgb_to_lab(cs, prof, c, c, c, lab)
270 if cg_abs(fq_to_micro(lab[1])) <= CG_WHITE_TOL_MICRO { if cg_abs(fq_to_micro(lab[2])) <= CG_WHITE_TOL_MICRO { grey_ok = grey_ok + 1 } }
271 cs_prof_lab_to_rgb(cs, prof, lab[0], lab[1], lab[2], rgb)
272 var okc: i64 = 0
273 if cg_abs(rgb[0] - c) <= CG_CODE_TOL { if cg_abs(rgb[1] - c) <= CG_CODE_TOL { if cg_abs(rgb[2] - c) <= CG_CODE_TOL { okc = 1 } } }
274 if okc == 1 { rt_ok = rt_ok + 1 } else {
275 // PRINT THE VALUES, NOT JUST PASS/FAIL: the first few failing greys, with their Lab and the codes that came back
276 if diag < CG_THREE {
277 gv_puts(" roundtrip-miss code=" as *u8); gv_num(c); gv_puts(" lab_micro=" as *u8); gv_num(fq_to_micro(lab[0])); gv_puts("," as *u8); gv_num(fq_to_micro(lab[1])); gv_puts("," as *u8); gv_num(fq_to_micro(lab[2]))
278 gv_puts(" back=" as *u8); gv_num(rgb[0]); gv_puts("," as *u8); gv_num(rgb[1]); gv_puts("," as *u8); gv_num(rgb[2])
279 let dxyz: *i64 = sys_mmap(CG_SLOT * CG_THREE) as *i64
280 cs_xyz_from_lab(cs, lab[0], lab[1], lab[2], prof[CS_P_WTPT], prof[CS_P_WTPT + 1], prof[CS_P_WTPT + 2], dxyz)
281 gv_puts(" xyz_micro=" as *u8); gv_num(fq_to_micro(dxyz[0])); gv_puts("," as *u8); gv_num(fq_to_micro(dxyz[1])); gv_puts("," as *u8); gv_num(fq_to_micro(dxyz[2]))
282 let dlin: *i64 = sys_mmap(CG_SLOT * CG_THREE) as *i64
283 cs_prof_lab_to_lin(cs, prof, lab[0], lab[1], lab[2], dlin)
284 gv_puts(" lin_micro=" as *u8); gv_num(fq_to_micro(dlin[0])); gv_puts("," as *u8); gv_num(fq_to_micro(dlin[1])); gv_puts("," as *u8); gv_num(fq_to_micro(dlin[2]))
285 let fyd: i64 = fq_div(lab[0] + fq_from_int(CS_LAB_L_OFFSET), fq_from_int(CS_LAB_L_SCALE))
286 gv_puts(" fy_micro=" as *u8); gv_num(fq_to_micro(fyd)); gv_puts(" finv_micro=" as *u8); gv_num(fq_to_micro(cs_lab_finv(cs, fyd)))
287 gv_puts(" inv0_micro=" as *u8); gv_num(fq_to_micro(prof[CS_P_INV])); gv_puts("," as *u8); gv_num(fq_to_micro(prof[CS_P_INV + 1])); gv_puts("," as *u8); gv_num(fq_to_micro(prof[CS_P_INV + 2])); gv_puts("\n" as *u8)
288 diag = diag + 1
289 }
290 }
291 c = c + 1
292 }
293 var corner_ok: i64 = 0
294 var k: i64 = 0
295 while k < CG_CORNERS {
296 let cr: i64 = (k & 1) * CG_CODE_MAX
297 let cg: i64 = ((k >> 1) & 1) * CG_CODE_MAX
298 let cb: i64 = ((k >> CG_TWO) & 1) * CG_CODE_MAX
299 cs_prof_rgb_to_lab(cs, prof, cr, cg, cb, lab)
300 cs_prof_lab_to_rgb(cs, prof, lab[0], lab[1], lab[2], rgb)
301 if cg_abs(rgb[0] - cr) <= CG_CODE_TOL { if cg_abs(rgb[1] - cg) <= CG_CODE_TOL { if cg_abs(rgb[2] - cb) <= CG_CODE_TOL { corner_ok = corner_ok + 1 } } }
302 k = k + 1
303 }
304 gv_puts(" greys_neutral=" as *u8); gv_num(grey_ok); gv_puts("/" as *u8); gv_num(CG_GREYS); gv_puts(" roundtrip_greys=" as *u8); gv_num(rt_ok)
305 gv_puts(" roundtrip_corners=" as *u8); gv_num(corner_ok); gv_puts("/" as *u8); gv_num(CG_CORNERS); gv_puts("\n" as *u8)
306 t = 0
307 if grey_ok == CG_GREYS { t = 1 }
308 gv_check("every-grey-code-reads-neutral-a0-b0 (256 of 256, no sample)" as *u8, t, ctr)
309 t = 0
310 if rt_ok == CG_GREYS { if corner_ok == CG_CORNERS { t = 1 } }
311 gv_check("rgb-lab-rgb-round-trips-within-one-code (greys and corners)" as *u8, t, ctr)
312 }
313
314 // ---- T5: gamut descriptor from the profile, conf-driven ----
315 let hs: i64 = cs_conf_int(CG_CONF, "gbd_hue_sectors" as *u8)
316 let es: i64 = cs_conf_int(CG_CONF, "gbd_elev_sectors" as *u8)
317 let steps: i64 = cs_conf_int(CG_CONF, "gbd_cube_steps" as *u8)
318 let iters: i64 = cs_conf_int(CG_CONF, "clip_iters" as *u8)
319 var conf_ok: i64 = 1
320 if hs == CS_CONF_MISS { conf_ok = 0 }
321 if es == CS_CONF_MISS { conf_ok = 0 }
322 if steps == CS_CONF_MISS { conf_ok = 0 }
323 if iters == CS_CONF_MISS { conf_ok = 0 }
324 let have_conf: i64 = gv_need("knowledge/colorsci.conf carries gbd_hue_sectors gbd_elev_sectors gbd_cube_steps clip_iters" as *u8, conf_ok, ctr)
325 if icc_ok == 1 { if have_conf == 1 {
326 let g: *i64 = cs_gbd_alloc(hs, es)
327 let added: i64 = cs_gbd_from_profile(cs, prof, g, steps)
328 let expect_n: i64 = CS_CUBE_FACES * (steps + 1) * (steps + 1)
329 let unsampled: i64 = cs_gbd_unsampled(g)
330 gv_puts(" [T5] gbd sectors=" as *u8); gv_num(hs * es); gv_puts(" samples=" as *u8); gv_num(added); gv_puts(" unsampled_sectors=" as *u8); gv_num(unsampled); gv_puts("\n" as *u8)
331 t = 0
332 if added == expect_n { t = 1 }
333 gv_check("gbd-sampled-every-cube-face-point (6 faces x (steps+1)^2)" as *u8, t, ctr)
334 var inside_n: i64 = 0
335 var face: i64 = 0
336 while face < CS_CUBE_FACES {
337 var u: i64 = 0
338 while u <= steps {
339 var v: i64 = 0
340 while v <= steps {
341 let cu: i64 = (u * CG_CODE_MAX) / steps
342 let cv: i64 = (v * CG_CODE_MAX) / steps
343 var r: i64 = cu
344 var gg: i64 = cv
345 var bb: i64 = 0
346 if face == 0 { r = 0; gg = cu; bb = cv }
347 if face == 1 { r = CG_CODE_MAX; gg = cu; bb = cv }
348 if face == 2 { r = cu; gg = 0; bb = cv }
349 if face == 3 { r = cu; gg = CG_CODE_MAX; bb = cv }
350 if face == 4 { r = cu; gg = cv; bb = 0 }
351 if face == 5 { r = cu; gg = cv; bb = CG_CODE_MAX }
352 cs_prof_rgb_to_lab(cs, prof, r, gg, bb, lab)
353 let pm: i64 = cs_gbd_permil(cs, g, lab[0], lab[1], lab[2])
354 if pm != CS_G_UNSAMPLED { if pm <= CG_PERMIL { inside_n = inside_n + 1 } }
355 v = v + 1
356 }
357 u = u + 1
358 }
359 face = face + 1
360 }
361 gv_puts(" samples_inside=" as *u8); gv_num(inside_n); gv_puts("/" as *u8); gv_num(added); gv_puts("\n" as *u8)
362 t = 0
363 if inside_n == added { t = 1 }
364 gv_check("every-sample-the-descriptor-was-built-from-reads-inside" as *u8, t, ctr)
365 // ---- the EXACT matrix-gamut test (the ruler phototwin uses); the descriptor above is informational for matrix
366 // profiles and load-bearing only for measured swatch gamuts. MEASURED 2026-08-24: 536 of 648 sectors unsampled
367 // at 16 steps, and the (50,150,0) probe fell in an unsampled sector -- a descriptor cannot be the oracle here.
368 let pout: i64 = cs_prof_gamut_permil(cs, prof, fq_from_int(CG_OUT_L), fq_from_int(CG_OUT_A), 0)
369 cs_prof_rgb_to_lab(cs, prof, CG_CODE_MAX, 0, 0, lab)
370 let pred: i64 = cs_prof_gamut_permil(cs, prof, lab[0], lab[1], lab[2])
371 cs_prof_rgb_to_lab(cs, prof, CG_GREY_MID, CG_GREY_MID, CG_GREY_MID, lab)
372 let pgrey: i64 = cs_prof_gamut_permil(cs, prof, lab[0], lab[1], lab[2])
373 gv_puts(" exact: outside_probe_permil=" as *u8); gv_num(pout); gv_puts(" red_corner_permil=" as *u8); gv_num(pred); gv_puts(" mid_grey_permil=" as *u8); gv_num(pgrey); gv_puts("\n" as *u8)
374 var fired_bad: i64 = 0
375 if pout > CG_PERMIL { fired_bad = 1 }
376 var fired_good: i64 = 0
377 if pred > CG_PERMIL + CG_BOUNDARY_SLACK_PERMIL { fired_good = 1 }
378 gv_bite("neg-control-exact-gamut-fires-outside-silent-on-red-corner" as *u8, fired_bad, fired_good, ctr)
379 t = 0
380 if pgrey < CG_PERMIL { if pgrey > 0 { t = 1 } }
381 gv_check("exact-gamut-mid-grey-reads-strictly-inside (headroom on every channel)" as *u8, t, ctr)
382 // every cube-face sample reads on or inside the boundary through the exact test (the cube is the gamut)
383 var exact_in: i64 = 0
384 var f2: i64 = 0
385 while f2 < CS_CUBE_FACES {
386 var u2: i64 = 0
387 while u2 <= steps {
388 var v2: i64 = 0
389 while v2 <= steps {
390 let cu2: i64 = (u2 * CG_CODE_MAX) / steps
391 let cv2: i64 = (v2 * CG_CODE_MAX) / steps
392 var r2: i64 = cu2
393 var g3: i64 = cv2
394 var b3: i64 = 0
395 if f2 == 0 { r2 = 0; g3 = cu2; b3 = cv2 }
396 if f2 == 1 { r2 = CG_CODE_MAX; g3 = cu2; b3 = cv2 }
397 if f2 == 2 { r2 = cu2; g3 = 0; b3 = cv2 }
398 if f2 == 3 { r2 = cu2; g3 = CG_CODE_MAX; b3 = cv2 }
399 if f2 == 4 { r2 = cu2; g3 = cv2; b3 = 0 }
400 if f2 == 5 { r2 = cu2; g3 = cv2; b3 = CG_CODE_MAX }
401 cs_prof_rgb_to_lab(cs, prof, r2, g3, b3, lab)
402 if cs_prof_gamut_permil(cs, prof, lab[0], lab[1], lab[2]) <= CG_PERMIL + CG_BOUNDARY_SLACK_PERMIL { exact_in = exact_in + 1 }
403 v2 = v2 + 1
404 }
405 u2 = u2 + 1
406 }
407 f2 = f2 + 1
408 }
409 gv_puts(" exact_samples_inside=" as *u8); gv_num(exact_in); gv_puts("/" as *u8); gv_num(added); gv_puts("\n" as *u8)
410 t = 0
411 if exact_in == added { t = 1 }
412 gv_check("exact-gamut-reads-every-cube-face-sample-on-or-inside (the cube is the gamut)" as *u8, t, ctr)
413 let mapped: *i64 = sys_mmap(CG_SLOT * CG_THREE) as *i64
414 let before: i64 = cs_prof_gamut_clip(cs, prof, fq_from_int(CG_OUT_L), fq_from_int(CG_OUT_A), 0, iters, mapped)
415 let after: i64 = cs_prof_gamut_permil(cs, prof, mapped[0], mapped[1], mapped[2])
416 gv_puts(" clip before_permil=" as *u8); gv_num(before); gv_puts(" after_permil=" as *u8); gv_num(after)
417 gv_puts(" mapped_micro=" as *u8); gv_num(fq_to_micro(mapped[0])); gv_puts("," as *u8); gv_num(fq_to_micro(mapped[1])); gv_puts("," as *u8); gv_num(fq_to_micro(mapped[2])); gv_puts("\n" as *u8)
418 t = 0
419 if before > CG_PERMIL { if after <= CG_PERMIL { if mapped[0] == fq_from_int(CG_OUT_L) { if mapped[2] == 0 { if mapped[1] > 0 { t = 1 } } } } }
420 gv_check("chroma-clip-brings-the-outside-probe-inside-at-the-same-L-and-hue" as *u8, t, ctr)
421 } }
422
423 return gv_verdict("colorsci" as *u8, ctr, "CIELAB + CIEDE2000 + ICC + gamut descriptor proven against the published table and the mirrored profile" as *u8)
424}