diff --git a/src/controllers/apotheosis.rs b/src/controllers/apotheosis.rs index c8786c7..70f7ca6 100644 --- a/src/controllers/apotheosis.rs +++ b/src/controllers/apotheosis.rs @@ -104,6 +104,12 @@ where self.records.is_empty() } + /// HNSW nodes and edges per layer, layer 0 first. Same data `draw()` writes as GEXF. + #[allow(clippy::type_complexity)] + pub fn draw_model(&self) -> Vec<(Vec, Vec<(String, String, f32)>)> { + self.hnsw.draw_model() + } + /// Performs an approximate k-NN search. If the `MetricId` natively maps to a Radix Tree key /// (e.g. TLSH string), it jumps directly to the HNSW node to retrieve neighbors, bypassing full traversal. /// Otherwise, it performs a standard HNSW k-NN search using the `query`. diff --git a/src/controllers/hnsw.rs b/src/controllers/hnsw.rs index 85cac1d..f3b31c0 100644 --- a/src/controllers/hnsw.rs +++ b/src/controllers/hnsw.rs @@ -52,6 +52,13 @@ where D: DistanceAlgorithm + Default, { pub fn new() -> Self { + const { + assert!( + M >= 2, + "M must be >= 2: with M=1, ln(1)=0 in random_level() diverges to usize::MAX" + ); + }; + Self { features: vec![], upper_layers: vec![], diff --git a/tests/ann_correctness.rs b/tests/ann_correctness.rs index bcb3a89..91a8394 100644 --- a/tests/ann_correctness.rs +++ b/tests/ann_correctness.rs @@ -98,7 +98,7 @@ fn multi_layer_search_correct_across_layers() { inserted.push(value); } - let layer_count = idx.hnsw.draw_model().len(); + let layer_count = idx.draw_model().len(); assert!( layer_count > 1, "dataset must produce upper layers to exercise the zero/upper index asymmetry, got {layer_count} layer(s)" @@ -172,8 +172,8 @@ fn heuristic_produces_different_graph_than_greedy() { .collect() }; - let greedy_edges = edges_for(greedy.hnsw.draw_model()[0].1.clone()); - let heuristic_edges = edges_for(heuristic.hnsw.draw_model()[0].1.clone()); + let greedy_edges = edges_for(greedy.draw_model()[0].1.clone()); + let heuristic_edges = edges_for(heuristic.draw_model()[0].1.clone()); assert_ne!( greedy_edges, heuristic_edges, diff --git a/tests/api_contract.rs b/tests/api_contract.rs index a7bca7e..3b4a0a3 100644 --- a/tests/api_contract.rs +++ b/tests/api_contract.rs @@ -75,7 +75,7 @@ fn draw_produces_one_gexf_file_per_layer() { idx.insert(SimpleNumberRecord::create(i.to_string())); } - let layer_count = idx.hnsw.draw_model().len(); + let layer_count = idx.draw_model().len(); idx.draw(&base); for n in 0..layer_count { diff --git a/tests/config_boundary.rs b/tests/config_boundary.rs index ea43cc0..24dbd5a 100644 --- a/tests/config_boundary.rs +++ b/tests/config_boundary.rs @@ -48,10 +48,10 @@ fn sync_invariant_min_viable() { for i in 0..10u32 { idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique } - let zero_len = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10); + let zero_len = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10); assert_eq!(zero_len, 10); - assert_eq!(idx.records.len(), zero_len); + assert_eq!(idx.len(), zero_len); } #[test] @@ -60,10 +60,10 @@ fn sync_invariant_inverted_ratio() { for i in 0..10u32 { idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique } - let zero_len = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10); + let zero_len = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10); assert_eq!(zero_len, 10); - assert_eq!(idx.records.len(), zero_len); + assert_eq!(idx.len(), zero_len); } #[test] @@ -72,10 +72,10 @@ fn sync_invariant_equal_m_m0() { for i in 0..10u32 { idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique } - let zero_len = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10); + let zero_len = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10); assert_eq!(zero_len, 10); - assert_eq!(idx.records.len(), zero_len); + assert_eq!(idx.len(), zero_len); } #[test] @@ -84,10 +84,10 @@ fn sync_invariant_dense() { for i in 0..10u32 { idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique } - let zero_len = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10); + let zero_len = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10); assert_eq!(zero_len, 10); - assert_eq!(idx.records.len(), zero_len); + assert_eq!(idx.len(), zero_len); } // --------------------------------------------------------------------------- diff --git a/tests/config_matrix.rs b/tests/config_matrix.rs index 1943a3c..edb1950 100644 --- a/tests/config_matrix.rs +++ b/tests/config_matrix.rs @@ -66,11 +66,11 @@ fn build_dataset_heuristic() -> ApoNumHeuristic { #[test] fn ff1_sync_invariant_default() { let idx = build_dataset_default(); - let zero_layer_count = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10, "records must contain 10 entries"); + let zero_layer_count = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10, "records must contain 10 entries"); assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes"); assert_eq!( - idx.records.len(), + idx.len(), zero_layer_count, "records and zero_layer diverged" ); @@ -79,11 +79,11 @@ fn ff1_sync_invariant_default() { #[test] fn ff1_sync_invariant_small() { let idx = build_dataset_small(); - let zero_layer_count = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10, "records must contain 10 entries"); + let zero_layer_count = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10, "records must contain 10 entries"); assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes"); assert_eq!( - idx.records.len(), + idx.len(), zero_layer_count, "records and zero_layer diverged" ); @@ -92,11 +92,11 @@ fn ff1_sync_invariant_small() { #[test] fn ff1_sync_invariant_heuristic() { let idx = build_dataset_heuristic(); - let zero_layer_count = idx.hnsw.draw_model()[0].0.len(); - assert_eq!(idx.records.len(), 10, "records must contain 10 entries"); + let zero_layer_count = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 10, "records must contain 10 entries"); assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes"); assert_eq!( - idx.records.len(), + idx.len(), zero_layer_count, "records and zero_layer diverged" ); diff --git a/tests/fitness_functions.rs b/tests/fitness_functions.rs index 612cf1a..de2612e 100644 --- a/tests/fitness_functions.rs +++ b/tests/fitness_functions.rs @@ -29,10 +29,10 @@ fn sync_invariant_holds_after_bulk_insert() { idx.insert(SimpleNumberRecord::create(i.to_string())); } - let hnsw_len = idx.hnsw.draw_model()[0].0.len(); + let hnsw_len = idx.draw_model()[0].0.len(); assert_eq!( - idx.records.len(), + idx.len(), 50, "records[] should contain exactly 50 entries after 50 inserts" ); @@ -41,7 +41,7 @@ fn sync_invariant_holds_after_bulk_insert() { "HNSW zero_layer should contain exactly 50 nodes after 50 inserts" ); assert_eq!( - idx.records.len(), + idx.len(), hnsw_len, "records[] and HNSW must be in sync - parallel structures diverged" ); @@ -211,18 +211,14 @@ fn sync_invariant_holds_after_duplicate_insert() { let second = idx.insert(SimpleNumberRecord::create("7".to_string())); assert!(!second, "duplicate insert must return false"); - let zero_layer_count = idx.hnsw.draw_model()[0].0.len(); - assert_eq!( - idx.records.len(), - 1, - "records must not grow on duplicate insert" - ); + let zero_layer_count = idx.draw_model()[0].0.len(); + assert_eq!(idx.len(), 1, "records must not grow on duplicate insert"); assert_eq!( zero_layer_count, 1, "zero_layer must not grow on duplicate insert" ); assert_eq!( - idx.records.len(), + idx.len(), zero_layer_count, "records and zero_layer diverged after duplicate" );