code wiki / (root) / nx_kubelka_lib.nx

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}