2026-04-29 18:50:55 +02:00
#!/usr/bin/env node
/ * *
* tests / cpp / parity _diff . mjs — float - tolerant blob comparison .
*
* Reads two binary blobs in the format produced by parity _check . cpp and
* parity _wasm . mjs , and reports any pair of floats that differs by more than
* the allowed tolerance .
*
* Usage :
* node parity _diff . mjs < native . bin > < wasm . bin > [ tolerance ]
*
* Default tolerance is 1e-5 . Returns exit code 0 on match , 1 on mismatch ,
* 2 on file / format error .
* /
import { readFile } from 'node:fs/promises' ;
const MAGIC = 0x5450524e ;
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
const VERSION = 4 ; // v4 adds stage 6 (geometric dislike)
2026-04-29 18:50:55 +02:00
const DEFAULT _TOL = 1e-5 ;
// Layout for context in error messages — must match parity_check.cpp / parity_wasm.mjs.
const SECTIONS = [
{ name : 'stage1_outputs' , count : 126 } ,
{ name : 'weight_probe' , count : 12 } ,
{ name : 'stage2_outputs' , count : 126 } ,
{ name : 'final_loss' , count : 1 } ,
{ name : 'paf_synth_means' , count : 2 } ,
{ name : 'channel_strip_means' , count : 2 } ,
2026-06-28 04:14:12 +02:00
// Stage 5 (feedback): 126 enter-static + 126 reroll-static + 12 temp-net
// weight probes + 12 restored-net weight probes.
{ name : 'feedback_randout_enter' , count : 126 } ,
{ name : 'feedback_randout_reroll' , count : 126 } ,
{ name : 'feedback_randmlp_temp' , count : 12 } ,
{ name : 'feedback_randmlp_restored' , count : 12 } ,
// Stage 5d (ExploreAndPlace): 12 scratchpad-net probes (post enter/reroll/
// nudge) + 126 frozen placed output + 12 restored-net probes + 126 committed
// output.
{ name : 'feedback_ep_scratch' , count : 12 } ,
{ name : 'feedback_ep_placed' , count : 126 } ,
{ name : 'feedback_ep_restored' , count : 12 } ,
{ name : 'feedback_ep_committed' , count : 126 } ,
2026-04-29 18:50:55 +02:00
] ;
async function readBlob ( path ) {
const buf = await readFile ( path ) ;
if ( buf . length < 12 ) throw new Error ( ` ${ path } : file too short ` ) ;
const magic = buf . readUInt32LE ( 0 ) ;
if ( magic !== MAGIC ) {
throw new Error ( ` ${ path } : bad magic 0x ${ magic . toString ( 16 ) } , want 0x ${ MAGIC . toString ( 16 ) } ` ) ;
}
const version = buf . readUInt32LE ( 4 ) ;
if ( version !== VERSION ) {
throw new Error ( ` ${ path } : unsupported version ${ version } ` ) ;
}
const n = buf . readUInt32LE ( 8 ) ;
const expected = 12 + n * 4 ;
if ( buf . length < expected ) {
throw new Error ( ` ${ path } : truncated, header says ${ n } floats but file has ${ ( buf . length - 12 ) / 4 } ` ) ;
}
const arr = new Float32Array ( n ) ;
for ( let i = 0 ; i < n ; ++ i ) arr [ i ] = buf . readFloatLE ( 12 + i * 4 ) ;
return arr ;
}
function locate ( idx ) {
let off = 0 ;
for ( const s of SECTIONS ) {
if ( idx < off + s . count ) {
return ` ${ s . name } [ ${ idx - off } ] ` ;
}
off += s . count ;
}
return ` payload[ ${ idx } ] ` ;
}
async function main ( ) {
const [ , , nativePath , wasmPath , tolArg ] = process . argv ;
if ( ! nativePath || ! wasmPath ) {
console . error ( 'usage: parity_diff.mjs <native.bin> <wasm.bin> [tolerance]' ) ;
process . exit ( 2 ) ;
}
const tol = tolArg ? Number ( tolArg ) : DEFAULT _TOL ;
if ( ! ( tol > 0 ) ) {
console . error ( ` bad tolerance: ${ tolArg } ` ) ;
process . exit ( 2 ) ;
}
let native , wasm ;
try {
[ native , wasm ] = await Promise . all ( [ readBlob ( nativePath ) , readBlob ( wasmPath ) ] ) ;
} catch ( err ) {
console . error ( '[parity_diff]' , err . message ) ;
process . exit ( 2 ) ;
}
if ( native . length !== wasm . length ) {
console . error ( ` [parity_diff] length mismatch: native= ${ native . length } , wasm= ${ wasm . length } ` ) ;
process . exit ( 1 ) ;
}
const mismatches = [ ] ;
let maxDelta = 0 ;
let maxDeltaIdx = - 1 ;
for ( let i = 0 ; i < native . length ; ++ i ) {
const d = Math . abs ( native [ i ] - wasm [ i ] ) ;
if ( d > maxDelta ) { maxDelta = d ; maxDeltaIdx = i ; }
if ( d > tol ) {
mismatches . push ( { idx : i , native : native [ i ] , wasm : wasm [ i ] , delta : d } ) ;
}
}
if ( mismatches . length === 0 ) {
console . log ( ` [parity_diff] OK: ${ native . length } floats match within ${ tol . toExponential ( ) } (max delta ${ maxDelta . toExponential ( ) } at ${ locate ( maxDeltaIdx ) } ) ` ) ;
process . exit ( 0 ) ;
}
console . error ( ` [parity_diff] FAIL: ${ mismatches . length } / ${ native . length } floats differ by more than ${ tol . toExponential ( ) } ` ) ;
console . error ( ` max delta: ${ maxDelta . toExponential ( ) } at ${ locate ( maxDeltaIdx ) } ` ) ;
const head = mismatches . slice ( 0 , 8 ) ;
for ( const m of head ) {
console . error ( ` ${ locate ( m . idx ) . padEnd ( 28 ) } native= ${ m . native . toFixed ( 8 ) } wasm= ${ m . wasm . toFixed ( 8 ) } delta= ${ m . delta . toExponential ( 3 ) } ` ) ;
}
if ( mismatches . length > head . length ) {
console . error ( ` ... and ${ mismatches . length - head . length } more ` ) ;
}
process . exit ( 1 ) ;
}
main ( ) ;