code wiki / (root) / nx_colorsci_cat_gate.nx

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}