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}