4545
4646use lance_graph_contract:: canonical_node:: EdgeCodecFlavor ;
4747use lance_graph_contract:: hhtl:: NiblePath ;
48- use lance_graph_contract:: soa_view:: MailboxSoaView ;
48+ use lance_graph_contract:: soa_view:: { IdentityPlane , MailboxSoaView } ;
4949
5050use crate :: graph:: graph_router:: Backend ;
5151
@@ -226,13 +226,18 @@ pub enum DistanceMeans {
226226 /// triangle inequality). Key-only, zero value decode
227227 /// (`E-PANCAKES-IS-RADIX-IS-HHTL`).
228228 PrefixDepth ,
229- // ── value-decode means (the costed tier — named here as the dispatch
230- // surface; wired on their own branch with their own cost gate, never
231- // mixed into the zero-decode facets; `E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`,
232- // `E-HELIX-IS-EXACT-LOCATION`): ──
233- // Hamming(plane) — fingerprint popcount over a content/topic/angle plane;
234- // HelixAngular — Signed360 exact-orthogonal-location distance;
235- // PqAdc — CAM-PQ asymmetric distance (IVF probe + tables).
229+ /// **Hamming over an identity plane** (`IdentityPlane`: content / topic /
230+ /// angle) — the popcount of the XOR of the two nodes' fingerprint planes.
231+ /// This is the **costed tier**: it reads the value-side plane
232+ /// (`MailboxSoaView::identity_plane_at`), so — unlike `PrefixDepth` — it is
233+ /// **NOT zero value decode** (`E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`). The right
234+ /// use of popcount: homogeneous 16K-bit fingerprint bits, not the
235+ /// heterogeneous GUID key.
236+ Hamming ( IdentityPlane ) ,
237+ // ── further value means (named; wired as they land, same costed tier):
238+ // HelixAngular — Signed360 exact-orthogonal-location distance
239+ // (`E-HELIX-IS-EXACT-LOCATION`);
240+ // PqAdc — CAM-PQ asymmetric distance (IVF probe + tables).
236241}
237242
238243/// Distance between two nodes (rows, each a GUID) under the chosen `means`.
@@ -258,6 +263,12 @@ pub fn node_distance<V: MailboxSoaView>(
258263 let cpd = pa. common_prefix_depth ( pb) ;
259264 Some ( u32:: from ( ( pa. depth ( ) - cpd) + ( pb. depth ( ) - cpd) ) )
260265 }
266+ // COSTED tier: reads the value-side plane (NOT zero value decode).
267+ DistanceMeans :: Hamming ( plane) => {
268+ let fa = view. identity_plane_at ( a, plane) ?;
269+ let fb = view. identity_plane_at ( b, plane) ?;
270+ Some ( fa. iter ( ) . zip ( fb) . map ( |( x, y) | ( x ^ y) . count_ones ( ) ) . sum ( ) )
271+ }
261272 }
262273}
263274
@@ -277,6 +288,7 @@ mod tests {
277288 keyed_rows : Vec < ( u64 , usize ) > ,
278289 paths : Vec < Option < NiblePath > > ,
279290 blocks : Vec < Option < EdgeBlock > > ,
291+ content_planes : Vec < Option < Vec < u64 > > > ,
280292 /// Logical populated rows; `None` ⇒ the full `class_ids` length. Set
281293 /// smaller to model the real `MailboxSoA<N>` (zero-padded capacity with
282294 /// `n_rows() == populated < entity_type().len()`).
@@ -328,6 +340,13 @@ mod tests {
328340 fn edge_block_at ( & self , row : usize ) -> Option < EdgeBlock > {
329341 self . blocks . get ( row) . copied ( ) . flatten ( )
330342 }
343+ fn identity_plane_at ( & self , row : usize , plane : IdentityPlane ) -> Option < & [ u64 ] > {
344+ // Only the Content plane is materialized in this fake.
345+ match plane {
346+ IdentityPlane :: Content => self . content_planes . get ( row) . and_then ( |p| p. as_deref ( ) ) ,
347+ IdentityPlane :: Topic | IdentityPlane :: Angle => None ,
348+ }
349+ }
331350 }
332351
333352 fn sample ( ) -> GuardedSoa {
@@ -356,6 +375,14 @@ mod tests {
356375 None ,
357376 None ,
358377 ] ,
378+ // content planes (2 u64 each) for the Hamming means; row2 unmaterialized.
379+ content_planes : vec ! [
380+ Some ( vec![ 0b1011 , 0 ] ) ,
381+ Some ( vec![ 0b1101 , 0 ] ) ,
382+ None ,
383+ Some ( vec![ 0 , 0 ] ) ,
384+ Some ( vec![ u64 :: MAX , u64 :: MAX ] ) ,
385+ ] ,
359386 logical_n : None ,
360387 }
361388 }
@@ -511,6 +538,36 @@ mod tests {
511538 assert ! ( d( 0 , 1 ) . unwrap( ) < d( 0 , 4 ) . unwrap( ) ) ;
512539 }
513540
541+ #[ test]
542+ fn node_distance_hamming_plane_is_popcount_xor_over_the_value_plane ( ) {
543+ let soa = sample ( ) ;
544+ // row0 0b1011, row1 0b1101 → XOR 0b0110 → popcount 2.
545+ assert_eq ! (
546+ node_distance( & soa, 0 , 1 , DistanceMeans :: Hamming ( IdentityPlane :: Content ) ) ,
547+ Some ( 2 )
548+ ) ;
549+ // self-distance = 0 (metric).
550+ assert_eq ! (
551+ node_distance( & soa, 0 , 0 , DistanceMeans :: Hamming ( IdentityPlane :: Content ) ) ,
552+ Some ( 0 )
553+ ) ;
554+ // all-zero vs all-ones over 2×u64 = 128 bits.
555+ assert_eq ! (
556+ node_distance( & soa, 3 , 4 , DistanceMeans :: Hamming ( IdentityPlane :: Content ) ) ,
557+ Some ( 128 )
558+ ) ;
559+ // unmaterialized plane (row2) ⇒ None (costed-tier fallback).
560+ assert_eq ! (
561+ node_distance( & soa, 0 , 2 , DistanceMeans :: Hamming ( IdentityPlane :: Content ) ) ,
562+ None
563+ ) ;
564+ // a plane this fake doesn't carry ⇒ None (Topic/Angle not materialized).
565+ assert_eq ! (
566+ node_distance( & soa, 0 , 1 , DistanceMeans :: Hamming ( IdentityPlane :: Angle ) ) ,
567+ None
568+ ) ;
569+ }
570+
514571 #[ test]
515572 fn node_distance_none_without_materialized_path ( ) {
516573 let mut soa = sample ( ) ;
0 commit comments