nx_colorsci_cat_gate.nx source
↩ module page · 186 lines · 10249 B
1// nx_colorsci_cat_gate.nx -- THE GATE FOR CHROMATIC ADAPTATION in the colour-science ruler, 2026-09-03.
2//
3// WHY THIS EXISTS. The ICC PCS is D50 by the spec and real captures are made under other illuminants, most
4// often D65. The profile path in nx_colorsci_lib applies matrix plus TRC only, so before this rung a colour
5// measured under a non-PCS white carried a white-point error into every downstream gamut and CIEDE2000
6// judgement. The error never announced itself: every number stayed in range and merely read slightly wrong.
7// cs_cat_* closes that, and this gate is what makes the closure evidence rather than a claim.
8//
9// SUBJECT: cs_cat_cone, cs_cat_build, cs_cat_apply and cs_cat_xyz in-process (the lib has no main).
10// Teeth, in order:
11// T1 Bradford D65 to D50 builds at all.
12// T2 KAT: all NINE coefficients against the published Bradford D65-to-D50 matrix. A mistyped cone matrix
13// or a transposed multiply cannot pass nine independent coefficients.
14// T3 THE DEFINING INVARIANT: the transform carries the SOURCE white exactly onto the DESTINATION white.
15// This one needs no external table -- it is what chromatic adaptation MEANS, so it holds even if every
16// published digit above were mistyped, and the two teeth therefore fail independently.
17// T4 adapting a white to ITSELF is the identity matrix (the diagonal gain degenerates to one).
18// T5 round trip: D65 to D50 and back returns the probe.
19// T6 ANTI-VACUITY NEG-CONTROL: CAT02 must NOT equal Bradford. Without this a cs_cat_cone that ignored its
20// argument and always wrote Bradford would pass every other tooth on this page.
21// T7 the invariant of T3 holds for CAT02 too, so T6 cannot pass by CAT02 being merely wrong.
22// T8 NEG-CONTROL: an unknown transform id REFUSES (returns 0) instead of defaulting to Bradford.
23// T9 NEG-CONTROL: a zero source white REFUSES instead of dividing by zero or emitting an identity.
24// Tolerance is the half-ulp of the published seven-decimal matrix widened for Q30 rounding; the coefficients
25// of the two transforms differ by ~1e-2, two orders above it, so T6 cannot pass on rounding.
26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
27import "nx_syscalls.nx"
28import "nx_gate_verdict.nx"
29import "nx_colorsci_lib.nx"
30
31const CGC_SLOT: i64 = 8
32const CGC_M: i64 = 9
33const CGC_V: i64 = 3
34const CGC_MICRO: i64 = 1000000
35const CGC_TOL_MICRO: i64 = 500
36const CGC_RT_TOL_MICRO: i64 = 500
37// CIE 2-degree observer white points, the same values the published matrix was derived from
38const CGC_D65_X: i64 = 950470
39const CGC_D65_Y: i64 = 1000000
40const CGC_D65_Z: i64 = 1088830
41const CGC_D50_X: i64 = 964220
42const CGC_D50_Y: i64 = 1000000
43const CGC_D50_Z: i64 = 825210
44// published Bradford D65 -> D50 adaptation matrix, carried at 1e-6
45const CGC_KAT_0: i64 = 1047811
46const CGC_KAT_1: i64 = 22887
47const CGC_KAT_2: i64 = 0 - 50127
48const CGC_KAT_3: i64 = 29542
49const CGC_KAT_4: i64 = 990484
50const CGC_KAT_5: i64 = 0 - 17049
51const CGC_KAT_6: i64 = 0 - 9235
52const CGC_KAT_7: i64 = 15044
53const CGC_KAT_8: i64 = 752132
54const CGC_BAD_ID: i64 = 99
55const CGC_PROBE_X: i64 = 400000
56const CGC_PROBE_Y: i64 = 350000
57const CGC_PROBE_Z: i64 = 300000
58
59func cgc_q(micro: i64) -> i64 { return fq_div(fq_from_int(micro), fq_from_int(CGC_MICRO)) }
60func cgc_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
61func cgc_within(q: i64, micro: i64, tol: i64) -> i64 {
62 if cgc_abs(fq_to_micro(q) - micro) <= tol { return 1 }
63 return 0
64}
65
66func main(argc: i64, argv: *i64) -> i64 {
67 let ctr: *i64 = gv_ctr()
68 gv_head("nx_colorsci CAT gate -- chromatic adaptation against the published Bradford matrix and the white-maps-to-white invariant" as *u8)
69
70 let mb: *i64 = sys_mmap(CGC_M * CGC_SLOT) as *i64
71 let mr: *i64 = sys_mmap(CGC_M * CGC_SLOT) as *i64
72 let mc: *i64 = sys_mmap(CGC_M * CGC_SLOT) as *i64
73 let mi: *i64 = sys_mmap(CGC_M * CGC_SLOT) as *i64
74 let mx: *i64 = sys_mmap(CGC_M * CGC_SLOT) as *i64
75 let v: *i64 = sys_mmap(CGC_V * CGC_SLOT) as *i64
76 let v2: *i64 = sys_mmap(CGC_V * CGC_SLOT) as *i64
77
78 let d65x: i64 = cgc_q(CGC_D65_X)
79 let d65y: i64 = cgc_q(CGC_D65_Y)
80 let d65z: i64 = cgc_q(CGC_D65_Z)
81 let d50x: i64 = cgc_q(CGC_D50_X)
82 let d50y: i64 = cgc_q(CGC_D50_Y)
83 let d50z: i64 = cgc_q(CGC_D50_Z)
84
85 // ---- T1 build ----
86 let okb: i64 = cs_cat_build(d65x, d65y, d65z, d50x, d50y, d50z, CS_CAT_BRADFORD, mb)
87 gv_puts(" [T1] bradford_build=" as *u8); gv_num(okb); gv_puts("\n" as *u8)
88 gv_check("bradford-d65-to-d50-builds" as *u8, (okb == 1) as i64, ctr)
89
90 // ---- T2 KAT over all nine coefficients ----
91 gv_puts(" [T2] bradford_micro=" as *u8)
92 var i: i64 = 0
93 while i < CGC_M {
94 gv_num(fq_to_micro(mb[i]))
95 gv_puts(" " as *u8)
96 i = i + 1
97 }
98 gv_puts("\n" as *u8)
99 var kt: i64 = 0
100 if cgc_within(mb[0], CGC_KAT_0, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
101 if cgc_within(mb[1], CGC_KAT_1, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
102 if cgc_within(mb[2], CGC_KAT_2, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
103 if cgc_within(mb[3], CGC_KAT_3, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
104 if cgc_within(mb[4], CGC_KAT_4, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
105 if cgc_within(mb[5], CGC_KAT_5, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
106 if cgc_within(mb[6], CGC_KAT_6, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
107 if cgc_within(mb[7], CGC_KAT_7, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
108 if cgc_within(mb[8], CGC_KAT_8, CGC_TOL_MICRO) == 1 { kt = kt + 1 }
109 gv_puts(" kat_coefficients_matched=" as *u8); gv_num(kt); gv_puts("/" as *u8); gv_num(CGC_M); gv_puts("\n" as *u8)
110 gv_check("bradford-d65-to-d50-matches-the-published-matrix-on-all-nine-coefficients" as *u8, (kt == CGC_M) as i64, ctr)
111
112 // ---- T3 the defining invariant ----
113 cs_cat_apply(mb, d65x, d65y, d65z, v)
114 gv_puts(" [T3] adapted_source_white_micro=" as *u8); gv_num(fq_to_micro(v[0])); gv_puts("," as *u8); gv_num(fq_to_micro(v[1])); gv_puts("," as *u8); gv_num(fq_to_micro(v[2]))
115 gv_puts(" want=" as *u8); gv_num(CGC_D50_X); gv_puts("," as *u8); gv_num(CGC_D50_Y); gv_puts("," as *u8); gv_num(CGC_D50_Z); gv_puts("\n" as *u8)
116 var wt: i64 = 0
117 if cgc_within(v[0], CGC_D50_X, CGC_TOL_MICRO) == 1 { if cgc_within(v[1], CGC_D50_Y, CGC_TOL_MICRO) == 1 { if cgc_within(v[2], CGC_D50_Z, CGC_TOL_MICRO) == 1 { wt = 1 } } }
118 gv_check("bradford-carries-the-source-white-onto-the-destination-white" as *u8, wt, ctr)
119
120 // ---- T4 identity ----
121 let oki: i64 = cs_cat_build(d65x, d65y, d65z, d65x, d65y, d65z, CS_CAT_BRADFORD, mi)
122 var it: i64 = 0
123 if oki == 1 {
124 if cgc_within(mi[0], CGC_MICRO, CGC_TOL_MICRO) == 1 {
125 if cgc_within(mi[4], CGC_MICRO, CGC_TOL_MICRO) == 1 {
126 if cgc_within(mi[8], CGC_MICRO, CGC_TOL_MICRO) == 1 {
127 if cgc_within(mi[1], 0, CGC_TOL_MICRO) == 1 {
128 if cgc_within(mi[5], 0, CGC_TOL_MICRO) == 1 { it = 1 }
129 }
130 }
131 }
132 }
133 }
134 gv_puts(" [T4] identity_diag_micro=" as *u8); gv_num(fq_to_micro(mi[0])); gv_puts("," as *u8); gv_num(fq_to_micro(mi[4])); gv_puts("," as *u8); gv_num(fq_to_micro(mi[8])); gv_puts("\n" as *u8)
135 gv_check("adapting-a-white-to-itself-is-the-identity-matrix" as *u8, it, ctr)
136
137 // ---- T5 round trip ----
138 let okr: i64 = cs_cat_build(d50x, d50y, d50z, d65x, d65y, d65z, CS_CAT_BRADFORD, mr)
139 let px: i64 = cgc_q(CGC_PROBE_X)
140 let py: i64 = cgc_q(CGC_PROBE_Y)
141 let pz: i64 = cgc_q(CGC_PROBE_Z)
142 cs_cat_apply(mb, px, py, pz, v)
143 cs_cat_apply(mr, v[0], v[1], v[2], v2)
144 gv_puts(" [T5] roundtrip_micro=" as *u8); gv_num(fq_to_micro(v2[0])); gv_puts("," as *u8); gv_num(fq_to_micro(v2[1])); gv_puts("," as *u8); gv_num(fq_to_micro(v2[2])); gv_puts("\n" as *u8)
145 var rt: i64 = 0
146 if okr == 1 {
147 if cgc_within(v2[0], CGC_PROBE_X, CGC_RT_TOL_MICRO) == 1 {
148 if cgc_within(v2[1], CGC_PROBE_Y, CGC_RT_TOL_MICRO) == 1 {
149 if cgc_within(v2[2], CGC_PROBE_Z, CGC_RT_TOL_MICRO) == 1 { rt = 1 }
150 }
151 }
152 }
153 gv_check("d65-to-d50-and-back-returns-the-probe" as *u8, rt, ctr)
154
155 // ---- T6 anti-vacuity: CAT02 is not silently Bradford ----
156 let okc: i64 = cs_cat_build(d65x, d65y, d65z, d50x, d50y, d50z, CS_CAT_CAT02, mc)
157 var dt: i64 = 0
158 var j: i64 = 0
159 while j < CGC_M {
160 if cgc_abs(fq_to_micro(mc[j]) - fq_to_micro(mb[j])) > CGC_TOL_MICRO { dt = 1 }
161 j = j + 1
162 }
163 gv_puts(" [T6] cat02_build=" as *u8); gv_num(okc); gv_puts(" differs_from_bradford=" as *u8); gv_num(dt); gv_puts("\n" as *u8)
164 var t6: i64 = 0
165 if okc == 1 { if dt == 1 { t6 = 1 } }
166 gv_check("neg-control-cat02-is-not-silently-bradford-so-the-transform-id-is-actually-read" as *u8, t6, ctr)
167
168 // ---- T7 the invariant holds for CAT02 too ----
169 cs_cat_apply(mc, d65x, d65y, d65z, v)
170 var ct: i64 = 0
171 if cgc_within(v[0], CGC_D50_X, CGC_TOL_MICRO) == 1 { if cgc_within(v[1], CGC_D50_Y, CGC_TOL_MICRO) == 1 { if cgc_within(v[2], CGC_D50_Z, CGC_TOL_MICRO) == 1 { ct = 1 } } }
172 gv_puts(" [T7] cat02_adapted_white_micro=" as *u8); gv_num(fq_to_micro(v[0])); gv_puts("," as *u8); gv_num(fq_to_micro(v[1])); gv_puts("," as *u8); gv_num(fq_to_micro(v[2])); gv_puts("\n" as *u8)
173 gv_check("cat02-also-carries-the-source-white-onto-the-destination-white" as *u8, ct, ctr)
174
175 // ---- T8 neg-control: unknown transform id refuses ----
176 let okbad: i64 = cs_cat_build(d65x, d65y, d65z, d50x, d50y, d50z, CGC_BAD_ID, mx)
177 gv_puts(" [T8] unknown_id_build=" as *u8); gv_num(okbad); gv_puts(" want=0\n" as *u8)
178 gv_check("neg-control-an-unknown-transform-id-refuses-instead-of-defaulting" as *u8, (okbad == 0) as i64, ctr)
179
180 // ---- T9 neg-control: a zero source white refuses ----
181 let okz: i64 = cs_cat_build(0, 0, 0, d50x, d50y, d50z, CS_CAT_BRADFORD, mx)
182 gv_puts(" [T9] zero_source_white_build=" as *u8); gv_num(okz); gv_puts(" want=0\n" as *u8)
183 gv_check("neg-control-a-zero-source-white-refuses" as *u8, (okz == 0) as i64, ctr)
184
185 return gv_verdict("colorsci_cat" as *u8, ctr, "chromatic adaptation proven against the published matrix, the white-maps-to-white invariant and its own refusals" as *u8)
186}