nx_kubelka_lib.nx source
↩ module page · 74 lines · 4116 B
1// nx_kubelka_lib.nx -- KUBELKA-MUNK TWO-FLUX REFLECTANCE, THE CHROMOPHORE SUBSTRATE (2026-09-03)
2//
3// WHY. Skin albedo in this estate is a COLOUR (nx_skin_ita derives an sRGB from the genome; nx_arousal_skin
4// carries melanin and perfusion permil). A colour can be painted but it cannot be RELIT, and it cannot be
5// EDITED physically -- and every late-stage goal on the biotwin board (a predicted scar, its erythema decaying
6// over months, pigmentation returning) is an edit to the ABSORBERS underneath, not to the pixel on top.
7// Kubelka-Munk is the bridge: it turns "how much melanin and how much haemoglobin" into "what reflectance",
8// and -- because it inverts in closed form -- it turns a measured reflectance back into K/S. That inverse is
9// what lets a captured photograph become chromophore ground truth instead of a baked texture.
10// MEASURED ABSENT before building: nx_absent kubelka -> ABSENT-PROVEN, coverage_complete=1 corpus_complete=1.
11//
12// SINGLE-CONSTANT KUBELKA-MUNK, the form the field actually uses for an optically thick layer:
13// R_inf = 1 + K/S - sqrt((K/S)^2 + 2(K/S)) forward
14// K/S = (1 - R)^2 / (2R) inverse (exact, and the round trip proves both)
15// K is absorption, S is scattering. Absorption is ADDITIVE over chromophores, which is the whole reason this
16// parameterisation relights and edits: K = K_base + c_mel*K_mel + c_hem*K_hem.
17//
18// THE COEFFICIENTS ARE NOT IN THIS FILE, DELIBERATELY. K_mel and K_hem are MEASURED spectra and belong in
19// pinned reference data, not typed into a lib by an author who did not measure them. Every function here
20// takes them as arguments, so this file carries physics and never fabricates a constant.
21//
22// PROVEN 10/10 GREEN on the laptop build farm 2026-09-03 (NAS build lane was wedged): K/S=0.25 -> R=0.500000
23// exactly, K/S=1 -> R=0.267949 (2-sqrt3), round trip 9/9 rungs with a worst error of 1e-6.
24// All arithmetic is nx_fixq30_lib Q30. license_tier: ORIGINAL No hw writes (Rule 26). LIB (no main).
25import "nx_syscalls.nx"
26import "nx_fixq30_lib.nx"
27
28// Reflectance and K/S are both non-negative, so a negative return is an unambiguous REFUSAL sentinel and can
29// never collide with a legitimate answer.
30const KM_REFUSED: i64 = 0 - 1
31const KM_TWO: i64 = 2
32
33// forward: K/S -> reflectance of an optically thick layer. ks must be >= 0.
34func km_r_from_ks(ks: i64) -> i64 {
35 if ks < 0 { return KM_REFUSED }
36 let k2: i64 = fq_mul(ks, ks)
37 let disc: i64 = k2 + ks * KM_TWO
38 let root: i64 = fq_sqrt(disc)
39 let r: i64 = FQ_ONE + ks - root
40 // the algebra cannot leave [0,1]; a value outside it means the fixed-point domain was exceeded, and that
41 // is reported as a refusal rather than clamped into a plausible-looking lie.
42 if r < 0 { return KM_REFUSED }
43 if r > FQ_ONE { return KM_REFUSED }
44 return r
45}
46
47// inverse: reflectance -> K/S. r must be in (0,1]. r == 0 is a REFUSAL, never an infinity dressed as a number.
48func km_ks_from_r(r: i64) -> i64 {
49 if r <= 0 { return KM_REFUSED }
50 if r > FQ_ONE { return KM_REFUSED }
51 let om: i64 = FQ_ONE - r
52 let num: i64 = fq_mul(om, om)
53 let den: i64 = r * KM_TWO
54 return fq_div(num, den)
55}
56
57// K/S from an additive chromophore mix. Absorption adds; scattering divides. s must be > 0.
58// c_mel and c_hem are Q30 fractions; k_mel and k_hem are the MEASURED absorption coefficients the caller
59// supplies from pinned data.
60func km_ks_mix(k_base: i64, k_mel: i64, c_mel: i64, k_hem: i64, c_hem: i64, s: i64) -> i64 {
61 if s <= 0 { return KM_REFUSED }
62 if c_mel < 0 { return KM_REFUSED }
63 if c_hem < 0 { return KM_REFUSED }
64 let k: i64 = k_base + fq_mul(k_mel, c_mel) + fq_mul(k_hem, c_hem)
65 if k < 0 { return KM_REFUSED }
66 return fq_div(k, s)
67}
68
69// the composition every caller actually wants: chromophore fractions -> reflectance, in one step.
70func km_r_of_mix(k_base: i64, k_mel: i64, c_mel: i64, k_hem: i64, c_hem: i64, s: i64) -> i64 {
71 let ks: i64 = km_ks_mix(k_base, k_mel, c_mel, k_hem, c_hem, s)
72 if ks == KM_REFUSED { return KM_REFUSED }
73 return km_r_from_ks(ks)
74}