code wiki / (root) / nx_id_param_core.nx

nx_id_param_core.nx source

↩ module page · 200 lines · 11318 B

1// nx_id_param_core.nx -- THE PURE IDENTITY-PARAMETER CORE. /compare/procgen C36, symbol 2// id_param_core. Extracted 2026-08-28 on the pattern nx_worldpipe_core proved for TERRAIN the same 3// week: a pure, allocation-free arithmetic core that both the NAS-side generate/judge loop and the 4// shipping engine import, so the two can never disagree about who a character is. Terrain got this 5// treatment first; a body is the same problem with a different field. 6// 7// THE DEFECT IT CLOSES, MEASURED RATHER THAN ASSUMED. nx_breeding's gn_wild draws every trait as 8// g[t] = lo(t) + gn_hash3(seed, t, 1) % span 9// -- twelve INDEPENDENT uniform draws with ZERO covariance, of which exactly ONE (GN_T_HUE) is a 10// colour. Downstream, nx_skin_ita maps that single scalar onto a straight L* locus at FIXED a* and 11// FIXED b*, so the entire cast varies along ONE LINE in a perceptually uniform space. That is the 12// mechanical content of the operator's "our colour palette is barely used": not a palette that is 13// under-used, but an identity space that is one-dimensional and uncorrelated. A cast drawn that way 14// cannot read as deliberate, because nothing about any character relates to anything else about it. 15// 16// WHAT IS ALREADY RIGHT, SAID PLAINLY SO THIS CORE IS NOT CREDITED WITH IT: gn_hash3(seed, t, ...) 17// is ALREADY a split stream keyed on the trait id, so adding trait 12 cannot reshuffle traits 0..11 18// and every character is already bit-identically reproducible from its seed. That property is 19// INHERITED here, not invented here. What is new is CORRELATION. 20// 21// THE MODEL: ONE FACTOR, STATED RATHER THAN FITTED. An axis is 22// x = (m*load + r*sqrt(UNIT^2 - load^2)) / UNIT 23// with m the shared master variate, r the axis's own independent residual, and load in 0..UNIT. 24// This is the standard one-factor construction, and it has two properties a hand-rolled blend does 25// not: corr(x, m) is load/UNIT BY CONSTRUCTION, and the marginal spread of x is the SAME whatever 26// the load -- so tightening a family resemblance never quietly costs variety. Both are gate teeth, 27// measured with a real estimator rather than asserted. 28// 29// WHY A FACTOR MODEL AND NOT A COVARIANCE MATRIX. A full matrix needs entries fitted to a 30// population we have not measured -- numbers with no source, which is the defect rule 11 exists to 31// stop. A factor model needs ONE loading per axis and states what it means: how much of this axis 32// is the shared cause. For pigmentation that shared cause is not a statistical convenience, it is 33// physical -- skin, hair and iris colour are all produced by the same melanin chromophore -- so a 34// single latent factor is the honest structure and the loadings are the only free parameters. 35// 36// SETTING load TO ZERO REPRODUCES THE INCUMBENT EXACTLY: an independent uniform draw per axis. 37// The gate uses that as its control, so the defect and the fix are measured on ONE ruler. 38// 39// LIB, no main (ecosystem convention). license_tier: ORIGINAL No hw writes (Rule 26). 40import "nx_syscalls.nx" 41import "nx_breeding.nx" 42 43const IDP_UNIT: i64 = 1000 44const IDP_V: i64 = 1 45// stream salts. Distinct constants so the master draw and the residual draw can never collide on 46// one (seed, axis) pair -- a collision would silently correlate an axis with itself and the 47// correlation estimator would report a real-looking number for a broken field. 48const IDP_SALT_MASTER: i64 = 7001 49const IDP_SALT_RESID: i64 = 7002 50// a correlation is UNDEFINED, not zero, when a column has no spread. Reporting 0 there would be a 51// fabricated value wearing the shape of a measurement. 52const IDP_CORR_UNDEF: i64 = 0 - 100000 53 54// the core's generation stamp. Anything that emits an identity records it, so a later reader can 55// tell a v1 character from a later model instead of guessing -- the same discipline nx_skin_ita 56// applies to its colour locus. 57func id_param_core() -> i64 { return IDP_V } 58 59// integer square root by bisection. The bracket is grown from the value itself, so it is exact for 60// any input rather than correct only inside a clamped range. 61func idp_isqrt(v: i64) -> i64 { 62 if v <= 0 { return 0 } 63 var hi: i64 = 1 64 while hi * hi <= v { hi = hi * 2 } 65 var lo: i64 = hi / 2 66 while lo < hi { 67 let mid: i64 = (lo + hi + 1) / 2 68 if mid * mid > v { hi = mid - 1 } else { lo = mid } 69 } 70 return lo 71} 72 73// ONE OWNER FOR THE STREAM. Delegates to nx_breeding's gn_hash3 rather than carrying a second hash, 74// so a character's AXES and its genome TRAITS are drawn by the same function. A core that hashed 75// differently from the genome it serves would be a duplicate ruler in the one place it must not be. 76// THE FINALISER, AND WHY IT IS HERE. gn_hash3 is an add-multiply chain with NO avalanche step, and 77// nx_id_param_gate MEASURED the consequence on its first run: at loading ZERO -- where the axes are 78// by construction independent draws -- the Pearson estimator read corr(master,skin)=259 and 79// corr(skin,hair)=234 permil instead of the ~0 independence requires. The low bits of an 80// unavalanched multiply chain stay correlated across adjacent (seed, axis) triples, and a modulus 81// reads exactly those bits. 82// THIS IS A REAL DEFECT IN A SHIPPING PART, NOT ONLY IN THIS CORE: gn_wild draws every trait as 83// gn_hash3(seed, t, 1) % span, so nx_breeding's twelve genome traits already carry an UNINTENDED 84// correlation of roughly that size. Reported rather than silently worked around. 85// AND THE FIX BELONGS HERE, NOT IN gn_hash3. nx_breeding is a certified part whose contract is that 86// a seed reproduces a creature BIT FOR BIT; re-mixing its hash would silently re-roll every genome 87// ever generated. So nx_breeding stays the stream OWNER and this core avalanches what it is handed. 88// The shifts and the multiplier are the xxHash-class finalisation constants nx_relief_lib already 89// names for exactly this purpose -- the estate's one avalanche, not a second invention. 90const IDP_AVAL_MUL: i64 = 1274126177 91const IDP_AVAL_1: i64 = 13 92const IDP_AVAL_2: i64 = 16 93const IDP_AVAL_MASK: i64 = 4611686018427387903 94func idp_avalanche(h0: i64) -> i64 { 95 var h: i64 = h0 % IDP_AVAL_MASK 96 h = h ^ (h >> IDP_AVAL_1) 97 h = (h * IDP_AVAL_MUL) % IDP_AVAL_MASK 98 h = h ^ (h >> IDP_AVAL_2) 99 if h < 0 { h = 0 - h } 100 return h 101} 102// THE UNFINALISED STREAM, KEPT RUNNABLE as the control. A baseline you cannot re-run is a number 103// and not a control, so the gate measures the pre-fix correlation at runtime rather than quoting 104// the two figures above. 105func idp_stream_raw(seed: i64, axis: i64, salt: i64) -> i64 { return gn_hash3(seed, axis, salt) } 106func idp_unit_raw(seed: i64, axis: i64, salt: i64) -> i64 { return idp_stream_raw(seed, axis, salt) % (IDP_UNIT + 1) } 107func idp_centered_raw(seed: i64, axis: i64, salt: i64) -> i64 { return 2 * idp_unit_raw(seed, axis, salt) - IDP_UNIT } 108// ONE OWNER FOR THE STREAM, avalanched. nx_breeding still decides what a seed means; this only 109// spreads its bits so that a modulus of the result is independent across axes. 110func idp_stream(seed: i64, axis: i64, salt: i64) -> i64 { return idp_avalanche(gn_hash3(seed, axis, salt)) } 111 112// a uniform draw for one axis, 0..IDP_UNIT inclusive. 113func idp_unit(seed: i64, axis: i64, salt: i64) -> i64 { 114 return idp_stream(seed, axis, salt) % (IDP_UNIT + 1) 115} 116// the same draw centred on zero: -IDP_UNIT..+IDP_UNIT. 117func idp_centered(seed: i64, axis: i64, salt: i64) -> i64 { 118 return 2 * idp_unit(seed, axis, salt) - IDP_UNIT 119} 120// THE SHARED MASTER: one per (seed, factor), the latent cause every axis loading onto that factor 121// draws from. Its stream is keyed on the FACTOR id, so factors are split streams too and adding a 122// second factor later cannot disturb the first. 123func idp_master(seed: i64, factor: i64) -> i64 { 124 return idp_centered(seed, factor, IDP_SALT_MASTER) 125} 126// THE FAN-OUT. A centred axis value correlated with <master> at load/IDP_UNIT, carrying the same 127// marginal spread whatever the load. Pure and allocation-free: same inputs, same output, on the NAS 128// and in the engine alike -- which is the property that lets both sides import this one copy. 129// 130// DECLARED IMPRECISION -- THE CLAMP, AND THE ALTERNATIVE THAT WAS NOT TAKEN. w^2 + q^2 = UNIT^2 131// preserves VARIANCE, which is the property that matters here: raising a loading must not narrow 132// the cast. It does NOT preserve RANGE -- for bounded draws the weighted sum reaches w+q, which 133// exceeds UNIT -- so some values land on an endpoint and are clamped. That is a real distortion: 134// it piles a little extra mass at maximum-light and maximum-dark rather than losing it. 135// The alternative is to divide by (w+q) instead of UNIT. It removes the clamp entirely and, because 136// Pearson correlation is invariant under positive linear scaling, it leaves every correlation in 137// this file UNCHANGED -- so it is genuinely tempting. It was NOT taken because it shrinks the 138// spread by UNIT/(w+q), which at the shipped loadings is a 25% loss of variety: precisely the trade 139// this rung exists to refuse. The clamp is therefore kept, MEASURED and BOUNDED by a gate tooth 140// rather than assumed away, and this note is here so the next reader inherits the choice and its 141// reason instead of rediscovering the arithmetic. 142func idp_fan(master: i64, load: i64, seed: i64, axis: i64) -> i64 { 143 var w: i64 = load 144 if w < 0 { w = 0 } 145 if w > IDP_UNIT { w = IDP_UNIT } 146 let r: i64 = idp_centered(seed, axis, IDP_SALT_RESID) 147 let q: i64 = idp_isqrt(IDP_UNIT * IDP_UNIT - w * w) 148 var v: i64 = (master * w + r * q) / IDP_UNIT 149 if v < 0 - IDP_UNIT { v = 0 - IDP_UNIT } 150 if v > IDP_UNIT { v = IDP_UNIT } 151 return v 152} 153// a centred axis mapped onto a trait's own 0..span range -- the shape nx_breeding already stores, 154// so a correlated axis is a drop-in for the independent draw it supersedes. 155func idp_to_span(c: i64, span: i64) -> i64 { 156 var v: i64 = (c + IDP_UNIT) * span / (2 * IDP_UNIT) 157 if v < 0 { v = 0 } 158 if v > span { v = span } 159 return v 160} 161 162// THE RULER, so a correlation is MEASURED and never asserted. Pearson r in permil over n paired 163// samples. Returns IDP_CORR_UNDEF when either side has no spread at all. 164func idp_corr_permil(xs: *i64, ys: *i64, n: i64) -> i64 { 165 if n < 2 { return IDP_CORR_UNDEF } 166 var sx: i64 = 0 167 var sy: i64 = 0 168 var i: i64 = 0 169 while i < n { sx = sx + xs[i]; sy = sy + ys[i]; i = i + 1 } 170 let mx: i64 = sx / n 171 let my: i64 = sy / n 172 var cov: i64 = 0 173 var vx: i64 = 0 174 var vy: i64 = 0 175 i = 0 176 while i < n { 177 let dx: i64 = xs[i] - mx 178 let dy: i64 = ys[i] - my 179 cov = cov + dx * dy 180 vx = vx + dx * dx 181 vy = vy + dy * dy 182 i = i + 1 183 } 184 if vx <= 0 { return IDP_CORR_UNDEF } 185 if vy <= 0 { return IDP_CORR_UNDEF } 186 return cov * IDP_UNIT / (idp_isqrt(vx) * idp_isqrt(vy)) 187} 188// the spread of a column, in the same units as the values -- the quantity that must NOT shrink when 189// a loading rises. Population standard deviation, integer. 190func idp_spread(xs: *i64, n: i64) -> i64 { 191 if n < 2 { return 0 } 192 var s: i64 = 0 193 var i: i64 = 0 194 while i < n { s = s + xs[i]; i = i + 1 } 195 let m: i64 = s / n 196 var v: i64 = 0 197 i = 0 198 while i < n { let d: i64 = xs[i] - m; v = v + d * d; i = i + 1 } 199 return idp_isqrt(v / n) 200}