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}