nx_strand_groom_gate.nx source
↩ module page · 154 lines · 7950 B
1// nx_strand_groom_gate.nx -- PG14: DO GUIDE CURVES PLUS INTERPOLATION MAKE A GROOM, HIERARCHICAL BY CONSTRUCTION?
2//
3// SUBJECT: nx_hairdyn_lib.gr_strand (in-process). Every geometric claim is recomputed from the groom's own positions.
4// The two load-bearing teeth are KATs on the interpolation: a child rooted ON a guide reproduces that guide exactly,
5// and a child between guides lies inside the box its guides span (a convex combination cannot leave it). Fixtures are
6// arithmetic; the gate leaks no filesystem state.
7import "nx_syscalls.nx"
8import "nx_gate_verdict.nx"
9import "nx_hairdyn_lib.nx"
10const SG_SEED: i64 = 20260906
11const SG_CX: i64 = 0
12const SG_CY: i64 = 40000 // Q8: a head 156 units up
13const SG_CZ: i64 = 0
14const SG_R: i64 = 2560 // scalp radius 10 units
15const SG_NG: i64 = 12
16const SG_NC: i64 = 180
17const SG_NSEG: i64 = 16
18const SG_SEGLEN: i64 = 512 // 2 units per segment
19const SG_JIT: i64 = 150
20const SG_ROOT_TOL: i64 = 8 // Q8: the integer normalisation's own error on the scalp radius
21const SG_LEN_DEV_PERMIL: i64 = 150
22
23func sg_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
24
25func main() -> i64 {
26 let ctr: *i64 = gv_ctr()
27 gv_head("=== NX-STRAND-GROOM gate (PG14): guides plus interpolation, hierarchical by construction ===" as *u8)
28 let n: i64 = gr_strand(SG_SEED, SG_CX, SG_CY, SG_CZ, SG_R, SG_NG, SG_NC, SG_NSEG, SG_SEGLEN, SG_JIT)
29 gv_check_eq("groom-grows: strand count is guides plus children" as *u8, n, SG_NG + SG_NC, ctr)
30 var lvl0: i64 = 0
31 var lvl1: i64 = 0
32 var k: i64 = 0
33 while k < n { if gr_level(k) == 0 { lvl0 = lvl0 + 1 } if gr_level(k) == 1 { lvl1 = lvl1 + 1 } k = k + 1 }
34 gv_check("hierarchy: level 0 is exactly the guides and level 1 exactly the children (partition sums)" as *u8, ((lvl0 == SG_NG) as i64) * ((lvl1 == SG_NC) as i64) * ((lvl0 + lvl1 == n) as i64), ctr)
35 // roots on the upper scalp cap
36 var on_cap: i64 = 1
37 var worst_r: i64 = 0
38 k = 0
39 while k < n {
40 let dx: i64 = gr_rootx(k) - SG_CX
41 let dy: i64 = gr_rooty(k) - SG_CY
42 let dz: i64 = gr_rootz(k) - SG_CZ
43 let d: i64 = sd_isqrt(dx*dx + dy*dy + dz*dz)
44 let e: i64 = sg_abs(d - SG_R)
45 if e > worst_r { worst_r = e }
46 if e > SG_ROOT_TOL { on_cap = 0 }
47 if dy < 0 { on_cap = 0 }
48 k = k + 1
49 }
50 gv_check("every-root-on-the-upper-scalp-cap (radius within tolerance, never below the centre plane)" as *u8, on_cap, ctr)
51 // KAT 1: a child rooted exactly on guide 3, jitter 0, reproduces guide 3 segment for segment
52 let out: *i64 = sys_mmap(GR_MAXSEG*3*8) as *i64
53 gr_child_at(gr_rootx(3), gr_rooty(3), gr_rootz(3), 0, 1, out)
54 var same: i64 = 1
55 var j: i64 = 0
56 while j < SG_NSEG {
57 if out[j*3] != gr_x(3, j) { same = 0 }
58 if out[j*3+1] != gr_y(3, j) { same = 0 }
59 if out[j*3+2] != gr_z(3, j) { same = 0 }
60 j = j + 1
61 }
62 gv_check("KAT-child-on-a-guide-IS-that-guide (jitter 0, every segment byte-equal)" as *u8, same, ctr)
63 // KAT 2: a child at the midpoint of guides 0 and 1, jitter 0, lies inside the offset box its nearest guides span
64 let mx: i64 = (gr_rootx(0) + gr_rootx(1))/2
65 let my: i64 = (gr_rooty(0) + gr_rooty(1))/2
66 let mz: i64 = (gr_rootz(0) + gr_rootz(1))/2
67 let kn: i64 = gr_child_at(mx, my, mz, 0, 1, out)
68 var inside: i64 = 1
69 j = 0
70 while j < SG_NSEG {
71 // bounds over ALL guides' offsets at this segment (a superset of the nearest three): a convex combination
72 // of any subset cannot leave the hull of the whole set
73 var lox: i64 = 1000000000
74 var hix: i64 = 0 - 1000000000
75 var loz: i64 = 1000000000
76 var hiz: i64 = 0 - 1000000000
77 var g: i64 = 0
78 while g < SG_NG {
79 let ox: i64 = gr_x(g, j) - gr_rootx(g)
80 let oz: i64 = gr_z(g, j) - gr_rootz(g)
81 if ox < lox { lox = ox }
82 if ox > hix { hix = ox }
83 if oz < loz { loz = oz }
84 if oz > hiz { hiz = oz }
85 g = g + 1
86 }
87 let cox: i64 = out[j*3] - mx
88 let coz: i64 = out[j*3+2] - mz
89 if cox < lox - 1 { inside = 0 }
90 if cox > hix + 1 { inside = 0 }
91 if coz < loz - 1 { inside = 0 }
92 if coz > hiz + 1 { inside = 0 }
93 j = j + 1
94 }
95 gv_check("KAT-child-between-guides-stays-inside-the-guides-hull (jitter 0, every segment)" as *u8, ((inside == 1) as i64) * ((kn == GR_KNN) as i64), ctr)
96 // segment lengths stay near seglen for every strand (the groom is a rest shape a chain can hold)
97 var worst_dev: i64 = 0
98 k = 0
99 while k < n {
100 j = 0
101 while j < SG_NSEG {
102 let l: i64 = gr_seglen_at(k, j)
103 let dev: i64 = sg_abs(l - SG_SEGLEN)*1000/SG_SEGLEN
104 if dev > worst_dev { worst_dev = dev }
105 j = j + 1
106 }
107 k = k + 1
108 }
109 gv_check("segment-lengths-hold: worst deviation from seglen within the declared band, all strands all segments" as *u8, (worst_dev <= SG_LEN_DEV_PERMIL) as i64, ctr)
110 // jitter does something and stays bounded: the same child root with jitter vs without
111 let out0: *i64 = sys_mmap(GR_MAXSEG*3*8) as *i64
112 let outj: *i64 = sys_mmap(GR_MAXSEG*3*8) as *i64
113 gr_child_at(mx, my, mz, 0, 7, out0)
114 gr_child_at(mx, my, mz, SG_JIT, 7, outj)
115 var moved: i64 = 0
116 var jmax: i64 = 0
117 j = 0
118 while j < SG_NSEG {
119 let dxj: i64 = sg_abs(outj[j*3] - out0[j*3])
120 let dzj: i64 = sg_abs(outj[j*3+2] - out0[j*3+2])
121 if dxj > 0 { moved = 1 }
122 if dzj > 0 { moved = 1 }
123 if dxj > jmax { jmax = dxj }
124 if dzj > jmax { jmax = dzj }
125 if outj[j*3+1] != out0[j*3+1] { moved = 0 - 100 }
126 j = j + 1
127 }
128 gv_check("jitter-frays-the-tip-and-never-the-hang: some x or z moves, y never, within the declared amplitude" as *u8, ((moved == 1) as i64) * ((jmax <= SG_SEGLEN*SG_JIT/1000) as i64), ctr)
129 // determinism and seed diversity
130 let hx: i64 = gr_x(SG_NG + 5, SG_NSEG - 1)
131 let hz: i64 = gr_z(SG_NG + 5, SG_NSEG - 1)
132 let g3x: i64 = gr_x(3, SG_NSEG - 1)
133 gr_strand(SG_SEED, SG_CX, SG_CY, SG_CZ, SG_R, SG_NG, SG_NC, SG_NSEG, SG_SEGLEN, SG_JIT)
134 gv_check("determinism: the same seed grooms the same strands (a child tip and a guide tip byte-equal)" as *u8, ((gr_x(SG_NG + 5, SG_NSEG - 1) == hx) as i64) * ((gr_z(SG_NG + 5, SG_NSEG - 1) == hz) as i64) * ((gr_x(3, SG_NSEG - 1) == g3x) as i64), ctr)
135 gr_strand(SG_SEED + 1, SG_CX, SG_CY, SG_CZ, SG_R, SG_NG, SG_NC, SG_NSEG, SG_SEGLEN, SG_JIT)
136 gv_check("two-seeds-two-grooms: a guide tip differs" as *u8, (gr_x(3, SG_NSEG - 1) != g3x) as i64, ctr)
137 // the groom seats into the chain state the dynamics run on
138 let st: *i64 = hd_alloc(SG_NSEG)
139 let seated: i64 = gr_to_chain(st, 2)
140 gv_check("groom-seats-into-the-softdyn-chain (nseg joints, tip matches, velocities zero)" as *u8, ((seated == SG_NSEG) as i64) * ((hd_tip_x_q8(st, SG_NSEG) == gr_x(2, SG_NSEG - 1)) as i64) * ((st[3] == 0) as i64), ctr)
141 // neg-controls: refusals by name
142 gv_check_eq("neg-control-zero-guides-REFUSES (-GR_E_ARG)" as *u8, gr_strand(SG_SEED, SG_CX, SG_CY, SG_CZ, SG_R, 0, 10, SG_NSEG, SG_SEGLEN, 0), 0 - GR_E_ARG, ctr)
143 gv_check_eq("neg-control-over-capacity-REFUSES (-GR_E_ARG)" as *u8, gr_strand(SG_SEED, SG_CX, SG_CY, SG_CZ, SG_R, 4, GR_MAXS, SG_NSEG, SG_SEGLEN, 0), 0 - GR_E_ARG, ctr)
144 gv_values_head()
145 gv_kv("strands" as *u8, n)
146 gv_kv("guides" as *u8, lvl0)
147 gv_kv("children" as *u8, lvl1)
148 gv_kv("worst_root_radius_error_q8" as *u8, worst_r)
149 gv_kv("worst_seglen_deviation_permil" as *u8, worst_dev)
150 gv_kv("jitter_max_q8" as *u8, jmax)
151 gv_kv("jitter_amp_q8" as *u8, SG_SEGLEN*SG_JIT/1000)
152 gv_kv("knn_used" as *u8, kn)
153 return gv_verdict("NX-STRAND-GROOM" as *u8, ctr, "a groom is guides plus a convex interpolation the gate can pin, and it seats into the one chain solver" as *u8)
154}