code wiki / _hdl_build / nx_contactforce_gate.nx
nx_contactforce_gate.nx source
↩ module page · 118 lines · 6649 B
1// nx_contactforce_gate.nx -- THE CONTACT READBACK IS REAL: it rises with depth, it tells firm tissue from
2// soft, and it is silent when nothing is touching. (softbody SB2 / hapticcare HC1, 2026-08-25.)
3//
4// WHY THESE TEETH AND NOT A NUMBER. The solver carries NO KILOGRAM -- inverse mass is a uniform q12 -- so
5// an absolute newton is not derivable and st_contact_force does not claim one. That makes a tooth of the
6// form "force == X" impossible to write honestly. What a haptic loop actually consumes is COMPARISON:
7// press harder and it must read harder, press firm tissue and it must read harder than soft at the SAME
8// depth. Both are unit-INDEPENDENT, both are exactly what the absent-unit still supports, and both fail
9// loudly for a readback that is a constant, a counter, or noise.
10//
11// THE PROBE RECIPE IS BORROWED, NOT INVENTED. nx_softtissue_gate already measured that teleporting the
12// probe to full depth puts its centre behind the surface and projects far-side vertices BACKWARDS through
13// the chest wall, inverting elements -- an artefact of instantaneous insertion, not of the solver. So the
14// probe approaches from OUTSIDE (z_touch = surface projection + radius) and ramps in over CF_RAMP substeps,
15// the same way that gate does it. Re-deriving the approach would have re-earned their bug.
16// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
17import "nx_syscalls.nx"
18import "nx_softtissue.nx"
19import "nx_gate_verdict.nx"
20
21const CF_G: i64 = 9800 // gravity magnitude, tissue standing in its normal loaded state
22const CF_ITERS: i64 = 8 // constraint iterations per substep
23const CF_SETTLE: i64 = 60 // substeps to settle before any probe touches it
24const CF_RAMP: i64 = 40 // ramp steps, matching nx_softtissue_gate's proven approach
25const CF_PROBE_R: i64 = 2500 // 25 mm probe, as the incumbent gate uses
26const CF_DEEP: i64 = 2000 // 20 mm press -- the incumbent's depth
27const CF_SHALLOW: i64 = 1000 // half of it; the monotonicity pair is DEEP vs exactly half
28const CF_H_MM: i64 = 10
29
30// Press `depth` into profile `prof` and return the contact force of the final substep.
31// out[0]=force out[1]=depth_cmm out[2]=engaged_count
32func cf_press(prof: i64, depth: i64, out: *i64) -> i64 {
33 let W: *i64 = st_new(prof, CF_H_MM)
34 st_run(W, 0, 0 - CF_G, 0, CF_SETTLE, ST_DT_REF_US, CF_ITERS)
35 let m: *i64 = sys_mmap(64*8) as *i64
36 st_measure(W, m)
37 let z_touch: i64 = m[ST_M_PROJ] + CF_PROBE_R
38 var p: i64 = 0
39 while p <= CF_RAMP {
40 st_set_touch(W, 1, 0, 0, z_touch - depth*p/CF_RAMP, CF_PROBE_R)
41 st_run(W, 0, 0 - CF_G, 0, 1, ST_DT_REF_US, CF_ITERS)
42 p = p + 1
43 }
44 out[0] = st_contact_force(W, ST_DT_REF_US)
45 out[1] = st_contact_depth(W)
46 out[2] = st_contact_n(W)
47 // Read the impulse from THIS press rather than repeating the whole press later just to compare
48 // the two channels. The first cut re-ran a fourth full ramp for T6 and the roster trial timed it
49 // out at 1809 ms against an 1800 ms deadline -- the work was redundant, so the fix is to stop
50 // doing it twice, not to move the gate to a slower lane.
51 out[3] = st_contact_impulse(W, ST_DT_REF_US)
52 return 0
53}
54
55func main() -> i64 {
56 let ctr: *i64 = gv_ctr()
57 gv_head("nx_contactforce_gate -- the contact readback rises with depth and tells firm from soft" as *u8)
58
59 let a: *i64 = sys_mmap(8*8) as *i64
60 let b: *i64 = sys_mmap(8*8) as *i64
61 let c: *i64 = sys_mmap(8*8) as *i64
62
63 cf_press(ST_PROF_LARGE_SOFT, CF_SHALLOW, a)
64 cf_press(ST_PROF_LARGE_SOFT, CF_DEEP, b)
65 cf_press(ST_PROF_SMALL_FIRM, CF_DEEP, c)
66
67 gv_puts(" soft shallow: force=" as *u8); gv_num(a[0])
68 gv_puts(" depth_cmm=" as *u8); gv_num(a[1])
69 gv_puts(" engaged=" as *u8); gv_num(a[2]); gv_puts("\n" as *u8)
70 gv_puts(" soft deep : force=" as *u8); gv_num(b[0])
71 gv_puts(" depth_cmm=" as *u8); gv_num(b[1])
72 gv_puts(" engaged=" as *u8); gv_num(b[2]); gv_puts("\n" as *u8)
73 gv_puts(" firm deep : force=" as *u8); gv_num(c[0])
74 gv_puts(" depth_cmm=" as *u8); gv_num(c[1])
75 gv_puts(" engaged=" as *u8); gv_num(c[2]); gv_puts("\n" as *u8)
76
77 // T1 the readback FIRES at all. Without this every comparison below could be comparing two zeros.
78 var t1: i64 = 0
79 if b[0] > 0 { if b[2] > 0 { t1 = 1 } }
80 gv_check("T1 a real press produces a NON-ZERO force AND a non-zero engaged count" as *u8, t1, ctr)
81
82 // T2 MONOTONIC IN DEPTH -- the property a haptic loop consumes and a constant cannot fake.
83 var t2: i64 = 0
84 if b[0] > a[0] { t2 = 1 }
85 gv_check("T2 MONOTONIC: pressing twice as deep reads STRICTLY HARDER (a constant readback fails this)" as *u8, t2, ctr)
86
87 // T3 FIRMNESS DISCRIMINATION at equal depth. This is the tooth that makes the readback mean
88 // "firmness" rather than "how far the probe moved" -- the two are identical for a fake.
89 var t3: i64 = 0
90 if c[0] > b[0] { t3 = 1 }
91 gv_check("T3 DISCRIMINATES: firm tissue reads harder than soft at the SAME depth" as *u8, t3, ctr)
92
93 // T4 neg-control: nothing touching must read exactly zero on BOTH channels. A magnitude alone
94 // cannot distinguish "a feather" from "no contact"; the count is what separates them.
95 let W0: *i64 = st_new(ST_PROF_LARGE_SOFT, CF_H_MM)
96 st_run(W0, 0, 0 - CF_G, 0, CF_SETTLE, ST_DT_REF_US, CF_ITERS)
97 var t4: i64 = 0
98 if st_contact_force(W0, ST_DT_REF_US) == 0 { if st_contact_n(W0) == 0 { t4 = 1 } }
99 gv_check("neg-control-no-probe-reads-zero-force-AND-zero-engaged-count" as *u8, t4, ctr)
100
101 // T5 neg-control: a zero timestep is UNDEFINED, not infinite. An unguarded divide would trap or
102 // publish a garbage magnitude, and a haptic consumer would act on it.
103 var t5: i64 = 0
104 if st_contact_force(W0, 0) == 0 { t5 = 1 }
105 gv_check("neg-control-zero-dt-returns-zero-rather-than-dividing" as *u8, t5, ctr)
106
107 // T6 the two derived channels must agree by construction: force is impulse per unit time, and if
108 // they ever diverge one of them is a second ruler. Read from the deep press already performed.
109 gv_puts(" impulse=" as *u8); gv_num(b[3])
110 gv_puts(" force=" as *u8); gv_num(b[0])
111 gv_puts(" (force must equal impulse per unit time)\n" as *u8)
112 var t6: i64 = 0
113 if b[0] == b[3]*1000000/ST_DT_REF_US { t6 = 1 }
114 gv_check("T6 force and impulse agree exactly -- one measured quantity, not two rulers" as *u8, t6, ctr)
115
116 return gv_verdict("CONTACT-FORCE" as *u8, ctr,
117 "contact readback: fires, rises with depth, discriminates firmness, silent when untouched -- in SOLVER units, never newtons" as *u8)
118}