@@ -598,6 +598,209 @@ impl BindNodeV2 {
598598 }
599599}
600600
601+ // ============================================================================
602+ // Per-Row Types: ThreePlaneRowBuffer, SoakingRowBuffer, BindNodeV2Row
603+ // ============================================================================
604+
605+ /// Three-plane fingerprint row buffer: holds S/P/O binary fingerprints for
606+ /// a single row, suitable for zero-copy Arrow interop.
607+ ///
608+ /// Total: 3 x 2048 = 6144 bytes per row.
609+ #[ derive( Debug , Clone ) ]
610+ pub struct ThreePlaneRowBuffer {
611+ /// Subject binary fingerprint (2048 bytes).
612+ pub s_binary : Vec < u8 > ,
613+ /// Predicate binary fingerprint (2048 bytes).
614+ pub p_binary : Vec < u8 > ,
615+ /// Object binary fingerprint (2048 bytes).
616+ pub o_binary : Vec < u8 > ,
617+ }
618+
619+ impl ThreePlaneRowBuffer {
620+ /// Create a zeroed three-plane row buffer.
621+ pub fn new ( ) -> Self {
622+ Self {
623+ s_binary : vec ! [ 0u8 ; PLANE_BINARY_BYTES ] ,
624+ p_binary : vec ! [ 0u8 ; PLANE_BINARY_BYTES ] ,
625+ o_binary : vec ! [ 0u8 ; PLANE_BINARY_BYTES ] ,
626+ }
627+ }
628+
629+ /// Create from three Plane references (copies their cached bit patterns).
630+ pub fn from_planes ( s : & mut Plane , p : & mut Plane , o : & mut Plane ) -> Self {
631+ s. ensure_cache ( ) ;
632+ p. ensure_cache ( ) ;
633+ o. ensure_cache ( ) ;
634+ Self {
635+ s_binary : s. bits_bytes_ref ( ) . to_vec ( ) ,
636+ p_binary : p. bits_bytes_ref ( ) . to_vec ( ) ,
637+ o_binary : o. bits_bytes_ref ( ) . to_vec ( ) ,
638+ }
639+ }
640+
641+ /// Compute S XOR P XOR O composite fingerprint.
642+ pub fn xor_spo ( & self ) -> Vec < u8 > {
643+ let mut result = vec ! [ 0u8 ; PLANE_BINARY_BYTES ] ;
644+ for i in 0 ..PLANE_BINARY_BYTES {
645+ result[ i] = self . s_binary [ i] ^ self . p_binary [ i] ^ self . o_binary [ i] ;
646+ }
647+ result
648+ }
649+
650+ /// Per-plane Hamming distance to another row buffer.
651+ ///
652+ /// Returns `(subject_dist, predicate_dist, object_dist)`.
653+ pub fn hamming_distance ( & self , other : & ThreePlaneRowBuffer ) -> ( u64 , u64 , u64 ) {
654+ let ds = hamming_distance_raw ( & self . s_binary , & other. s_binary ) ;
655+ let dp = hamming_distance_raw ( & self . p_binary , & other. p_binary ) ;
656+ let do_ = hamming_distance_raw ( & self . o_binary , & other. o_binary ) ;
657+ ( ds, dp, do_)
658+ }
659+
660+ /// Total byte size of this row buffer (always 3 * PLANE_BINARY_BYTES).
661+ pub fn total_bytes ( & self ) -> usize {
662+ 3 * PLANE_BINARY_BYTES
663+ }
664+ }
665+
666+ impl Default for ThreePlaneRowBuffer {
667+ fn default ( ) -> Self {
668+ Self :: new ( )
669+ }
670+ }
671+
672+ /// Soaking row buffer: nullable i8 accumulator for a single plane of a single row.
673+ ///
674+ /// When `data` is `Some`, the buffer is active (Form state).
675+ /// When `data` is `None`, the buffer has been crystallized or nulled.
676+ #[ derive( Debug , Clone ) ]
677+ pub struct SoakingRowBuffer {
678+ /// Soaking data (None = nulled/crystallized).
679+ pub data : Option < Vec < i8 > > ,
680+ /// Dimension count.
681+ pub dims : usize ,
682+ }
683+
684+ impl SoakingRowBuffer {
685+ /// Create a new active soaking row buffer, zeroed.
686+ pub fn new ( dims : usize ) -> Self {
687+ Self {
688+ data : Some ( vec ! [ 0i8 ; dims] ) ,
689+ dims,
690+ }
691+ }
692+
693+ /// Crystallize: convert soaking (int8) to binary fingerprint via sign().
694+ ///
695+ /// Consumes the soaking data and returns a binary vector.
696+ /// After crystallization, the buffer is nulled.
697+ pub fn crystallize ( & mut self ) -> Vec < u8 > {
698+ let soaking = match self . data . take ( ) {
699+ Some ( d) => d,
700+ None => return vec ! [ 0u8 ; ( self . dims + 7 ) / 8 ] ,
701+ } ;
702+ let n_bytes = ( soaking. len ( ) + 7 ) / 8 ;
703+ let mut bits = vec ! [ 0u8 ; n_bytes] ;
704+ for ( i, & val) in soaking. iter ( ) . enumerate ( ) {
705+ if val > 0 {
706+ bits[ i / 8 ] |= 1 << ( i % 8 ) ;
707+ }
708+ }
709+ bits
710+ }
711+
712+ /// Returns `true` when soaking is still active (not nulled/crystallized).
713+ pub fn is_active ( & self ) -> bool {
714+ self . data . is_some ( )
715+ }
716+
717+ /// Null out the soaking data (transition to inactive).
718+ pub fn null_out ( & mut self ) {
719+ self . data = None ;
720+ }
721+ }
722+
723+ /// Complete bind_nodes_v2 row type combining fingerprints, soaking, gate,
724+ /// and NARS truth values.
725+ ///
726+ /// This is a streamlined per-row type that pairs `ThreePlaneRowBuffer` with
727+ /// per-plane `SoakingRowBuffer`s for the full Lance schema.
728+ #[ derive( Debug , Clone ) ]
729+ pub struct BindNodeV2Row {
730+ /// Three-plane binary fingerprints.
731+ pub fingerprints : ThreePlaneRowBuffer ,
732+ /// Subject soaking accumulator.
733+ pub s_soaking : SoakingRowBuffer ,
734+ /// Predicate soaking accumulator.
735+ pub p_soaking : SoakingRowBuffer ,
736+ /// Object soaking accumulator.
737+ pub o_soaking : SoakingRowBuffer ,
738+ /// Gate lifecycle state.
739+ pub gate : GateState ,
740+ /// NARS frequency (u16 fixed-point).
741+ pub nars_frequency : u16 ,
742+ /// NARS confidence (u16 fixed-point).
743+ pub nars_confidence : u16 ,
744+ }
745+
746+ impl BindNodeV2Row {
747+ /// Create a new row in Form state with active soaking.
748+ pub fn new ( dims : usize ) -> Self {
749+ Self {
750+ fingerprints : ThreePlaneRowBuffer :: new ( ) ,
751+ s_soaking : SoakingRowBuffer :: new ( dims) ,
752+ p_soaking : SoakingRowBuffer :: new ( dims) ,
753+ o_soaking : SoakingRowBuffer :: new ( dims) ,
754+ gate : GateState :: Form ,
755+ nars_frequency : 32768 ,
756+ nars_confidence : 0 ,
757+ }
758+ }
759+
760+ /// Crystallize all three soaking buffers, folding sign bits into fingerprints.
761+ /// Transitions from Form to Flow.
762+ pub fn crystallize ( & mut self ) {
763+ if self . gate != GateState :: Form {
764+ return ;
765+ }
766+ // Fold each soaking into its binary plane
767+ if let Some ( ref soaking) = self . s_soaking . data {
768+ fold_sign_into_binary ( & mut self . fingerprints . s_binary , soaking) ;
769+ }
770+ if let Some ( ref soaking) = self . p_soaking . data {
771+ fold_sign_into_binary ( & mut self . fingerprints . p_binary , soaking) ;
772+ }
773+ if let Some ( ref soaking) = self . o_soaking . data {
774+ fold_sign_into_binary ( & mut self . fingerprints . o_binary , soaking) ;
775+ }
776+ self . s_soaking . null_out ( ) ;
777+ self . p_soaking . null_out ( ) ;
778+ self . o_soaking . null_out ( ) ;
779+ self . gate = GateState :: Flow ;
780+ }
781+
782+ /// Freeze: transition from Flow to Freeze.
783+ pub fn freeze ( & mut self ) {
784+ if self . gate == GateState :: Flow {
785+ self . gate = GateState :: Freeze ;
786+ }
787+ }
788+ }
789+
790+ /// Fold soaking sign bits into a binary fingerprint (shared helper).
791+ fn fold_sign_into_binary ( binary : & mut [ u8 ] , soaking : & [ i8 ] ) {
792+ let bit_count = ( binary. len ( ) * 8 ) . min ( soaking. len ( ) ) ;
793+ for i in 0 ..bit_count {
794+ let byte_idx = i / 8 ;
795+ let bit_idx = i % 8 ;
796+ if soaking[ i] > 0 {
797+ binary[ byte_idx] |= 1 << bit_idx;
798+ } else {
799+ binary[ byte_idx] &= !( 1 << bit_idx) ;
800+ }
801+ }
802+ }
803+
601804#[ cfg( test) ]
602805mod tests {
603806 use super :: * ;
@@ -982,4 +1185,164 @@ mod tests {
9821185 assert_eq ! ( slice. len( ) , 3 * BINARY_BYTES ) ;
9831186 assert_eq ! ( slice[ BINARY_BYTES ] , 0xAB ) ;
9841187 }
1188+
1189+ // ================================================================
1190+ // ThreePlaneRowBuffer tests
1191+ // ================================================================
1192+
1193+ #[ test]
1194+ fn three_plane_row_buffer_new ( ) {
1195+ let buf = ThreePlaneRowBuffer :: new ( ) ;
1196+ assert_eq ! ( buf. s_binary. len( ) , PLANE_BINARY_BYTES ) ;
1197+ assert_eq ! ( buf. p_binary. len( ) , PLANE_BINARY_BYTES ) ;
1198+ assert_eq ! ( buf. o_binary. len( ) , PLANE_BINARY_BYTES ) ;
1199+ assert_eq ! ( buf. total_bytes( ) , 3 * PLANE_BINARY_BYTES ) ;
1200+ }
1201+
1202+ #[ test]
1203+ fn three_plane_row_buffer_default ( ) {
1204+ let buf = ThreePlaneRowBuffer :: default ( ) ;
1205+ assert_eq ! ( buf. total_bytes( ) , 6144 ) ;
1206+ }
1207+
1208+ #[ test]
1209+ fn three_plane_row_buffer_from_planes ( ) {
1210+ let ( mut s, mut p, mut o) = make_test_planes ( ) ;
1211+ let buf = ThreePlaneRowBuffer :: from_planes ( & mut s, & mut p, & mut o) ;
1212+ assert_eq ! ( buf. s_binary. len( ) , PLANE_BINARY_BYTES ) ;
1213+ // Non-trivial planes should produce non-zero binaries
1214+ assert ! ( buf. s_binary. iter( ) . any( |& b| b != 0 ) ) ;
1215+ }
1216+
1217+ #[ test]
1218+ fn three_plane_row_buffer_xor_spo ( ) {
1219+ let mut buf = ThreePlaneRowBuffer :: new ( ) ;
1220+ buf. s_binary [ 0 ] = 0xFF ;
1221+ buf. p_binary [ 0 ] = 0x0F ;
1222+ buf. o_binary [ 0 ] = 0xAA ;
1223+ let xor = buf. xor_spo ( ) ;
1224+ assert_eq ! ( xor[ 0 ] , 0xFF ^ 0x0F ^ 0xAA ) ;
1225+ assert_eq ! ( xor[ 1 ] , 0 ) ; // rest is zero
1226+ }
1227+
1228+ #[ test]
1229+ fn three_plane_row_buffer_hamming_self_zero ( ) {
1230+ let ( mut s, mut p, mut o) = make_test_planes ( ) ;
1231+ let buf = ThreePlaneRowBuffer :: from_planes ( & mut s, & mut p, & mut o) ;
1232+ let ( ds, dp, do_) = buf. hamming_distance ( & buf) ;
1233+ assert_eq ! ( ds, 0 ) ;
1234+ assert_eq ! ( dp, 0 ) ;
1235+ assert_eq ! ( do_, 0 ) ;
1236+ }
1237+
1238+ #[ test]
1239+ fn three_plane_row_buffer_hamming_different ( ) {
1240+ let mut buf1 = ThreePlaneRowBuffer :: new ( ) ;
1241+ let mut buf2 = ThreePlaneRowBuffer :: new ( ) ;
1242+ buf1. s_binary . fill ( 0xFF ) ;
1243+ buf2. s_binary . fill ( 0x00 ) ;
1244+ let ( ds, dp, _) = buf1. hamming_distance ( & buf2) ;
1245+ assert_eq ! ( ds, PLANE_BINARY_BYTES as u64 * 8 ) ;
1246+ assert_eq ! ( dp, 0 ) ; // both zero
1247+ }
1248+
1249+ // ================================================================
1250+ // SoakingRowBuffer tests
1251+ // ================================================================
1252+
1253+ #[ test]
1254+ fn soaking_row_buffer_new ( ) {
1255+ let buf = SoakingRowBuffer :: new ( 100 ) ;
1256+ assert ! ( buf. is_active( ) ) ;
1257+ assert_eq ! ( buf. dims, 100 ) ;
1258+ assert_eq ! ( buf. data. as_ref( ) . unwrap( ) . len( ) , 100 ) ;
1259+ }
1260+
1261+ #[ test]
1262+ fn soaking_row_buffer_crystallize ( ) {
1263+ let mut buf = SoakingRowBuffer :: new ( 8 ) ;
1264+ buf. data . as_mut ( ) . unwrap ( ) . copy_from_slice ( & [ 1 , -1 , 1 , -1 , 1 , -1 , 1 , -1 ] ) ;
1265+ let bits = buf. crystallize ( ) ;
1266+ assert_eq ! ( bits[ 0 ] , 0b01010101 ) ;
1267+ assert ! ( !buf. is_active( ) ) ; // should be nulled after crystallize
1268+ }
1269+
1270+ #[ test]
1271+ fn soaking_row_buffer_crystallize_inactive ( ) {
1272+ let mut buf = SoakingRowBuffer :: new ( 16 ) ;
1273+ buf. null_out ( ) ;
1274+ assert ! ( !buf. is_active( ) ) ;
1275+ let bits = buf. crystallize ( ) ;
1276+ // Should return zeroed bits when inactive
1277+ assert ! ( bits. iter( ) . all( |& b| b == 0 ) ) ;
1278+ }
1279+
1280+ #[ test]
1281+ fn soaking_row_buffer_null_out ( ) {
1282+ let mut buf = SoakingRowBuffer :: new ( 10 ) ;
1283+ assert ! ( buf. is_active( ) ) ;
1284+ buf. null_out ( ) ;
1285+ assert ! ( !buf. is_active( ) ) ;
1286+ }
1287+
1288+ // ================================================================
1289+ // BindNodeV2Row tests
1290+ // ================================================================
1291+
1292+ #[ test]
1293+ fn bind_node_v2_row_new ( ) {
1294+ let row = BindNodeV2Row :: new ( 100 ) ;
1295+ assert_eq ! ( row. gate, GateState :: Form ) ;
1296+ assert ! ( row. s_soaking. is_active( ) ) ;
1297+ assert ! ( row. p_soaking. is_active( ) ) ;
1298+ assert ! ( row. o_soaking. is_active( ) ) ;
1299+ assert_eq ! ( row. nars_frequency, 32768 ) ;
1300+ assert_eq ! ( row. nars_confidence, 0 ) ;
1301+ assert_eq ! ( row. fingerprints. total_bytes( ) , 6144 ) ;
1302+ }
1303+
1304+ #[ test]
1305+ fn bind_node_v2_row_crystallize ( ) {
1306+ let mut row = BindNodeV2Row :: new ( 16 ) ;
1307+ // Put some data in soaking
1308+ row. s_soaking . data . as_mut ( ) . unwrap ( ) . fill ( 1 ) ;
1309+ row. crystallize ( ) ;
1310+ assert_eq ! ( row. gate, GateState :: Flow ) ;
1311+ assert ! ( !row. s_soaking. is_active( ) ) ;
1312+ assert ! ( !row. p_soaking. is_active( ) ) ;
1313+ assert ! ( !row. o_soaking. is_active( ) ) ;
1314+ }
1315+
1316+ #[ test]
1317+ fn bind_node_v2_row_lifecycle ( ) {
1318+ let mut row = BindNodeV2Row :: new ( 16 ) ;
1319+ assert_eq ! ( row. gate, GateState :: Form ) ;
1320+
1321+ // Cannot freeze from Form
1322+ row. freeze ( ) ;
1323+ assert_eq ! ( row. gate, GateState :: Form ) ;
1324+
1325+ // Crystallize: Form -> Flow
1326+ row. crystallize ( ) ;
1327+ assert_eq ! ( row. gate, GateState :: Flow ) ;
1328+
1329+ // Double crystallize is no-op
1330+ row. crystallize ( ) ;
1331+ assert_eq ! ( row. gate, GateState :: Flow ) ;
1332+
1333+ // Freeze: Flow -> Freeze
1334+ row. freeze ( ) ;
1335+ assert_eq ! ( row. gate, GateState :: Freeze ) ;
1336+ }
1337+
1338+ #[ test]
1339+ fn bind_node_v2_row_crystallize_folds_sign ( ) {
1340+ let mut row = BindNodeV2Row :: new ( 16 ) ;
1341+ // Set subject soaking to all positive
1342+ row. s_soaking . data . as_mut ( ) . unwrap ( ) . fill ( 5 ) ;
1343+ row. crystallize ( ) ;
1344+ // First 2 bytes of s_binary should have bits set (16 bits = 2 bytes)
1345+ assert_eq ! ( row. fingerprints. s_binary[ 0 ] , 0xFF ) ;
1346+ assert_eq ! ( row. fingerprints. s_binary[ 1 ] , 0xFF ) ;
1347+ }
9851348}
0 commit comments