@@ -109,6 +109,35 @@ impl<'a, T: RealNumber + FloatNumber + FloatCore, M: Array2<T>> CosinePair<'a, T
109109 )
110110 }
111111
112+ /// Lazy constructor that skips the Theta(n^2) [`init`] scan.
113+ ///
114+ /// [`query_row_top_k`] and [`query`] work unchanged because they
115+ /// recompute distances on the fly from `samples`, `row_norms`, and
116+ /// `parameters`. Methods that depend on the precomputed `distances` /
117+ /// `neighbours` fields (`closest_pair`, `ordered_pairs`, `query_row`,
118+ /// `distances_from`) will panic on a lazy instance.
119+ pub fn lazy ( m : & ' a M , top_k : usize ) -> Result < Self , Failed > {
120+ if m. shape ( ) . 0 < 2 {
121+ return Err ( Failed :: because (
122+ FailedError :: FindFailed ,
123+ "min number of rows should be 2" ,
124+ ) ) ;
125+ }
126+
127+ let row_norms = ( 0 ..m. shape ( ) . 0 ) . map ( |i| m. get_row ( i) . norm2 ( ) ) . collect ( ) ;
128+
129+ Ok ( Self {
130+ samples : m,
131+ distances : HashMap :: new ( ) ,
132+ neighbours : Vec :: new ( ) ,
133+ row_norms,
134+ parameters : CosinePairParameters {
135+ top_k : Some ( top_k) ,
136+ approximate : false ,
137+ } ,
138+ } )
139+ }
140+
112141 /// Constructor with full parameter control
113142 pub fn with_parameters ( m : & ' a M , parameters : CosinePairParameters ) -> Result < Self , Failed > {
114143 if m. shape ( ) . 0 < 2 {
@@ -1260,6 +1289,46 @@ mod tests {
12601289 }
12611290 }
12621291
1292+ #[ test]
1293+ fn lazy_skips_init_and_query_row_top_k_matches_non_lazy ( ) {
1294+ let x = mixed_direction_rows ( ) ;
1295+ let top_k = 4 ;
1296+
1297+ let lazy = CosinePair :: lazy ( & x, top_k) . unwrap ( ) ;
1298+ assert ! (
1299+ lazy. distances. is_empty( ) ,
1300+ "lazy instance must have empty distances"
1301+ ) ;
1302+ assert ! (
1303+ lazy. neighbours. is_empty( ) ,
1304+ "lazy instance must have empty neighbours"
1305+ ) ;
1306+
1307+ let eager = CosinePair :: with_top_k ( & x, top_k) . unwrap ( ) ;
1308+
1309+ for row in 0 ..x. shape ( ) . 0 {
1310+ let lazy_result = lazy. query_row_top_k ( row, 3 ) . unwrap ( ) ;
1311+ let eager_result = eager. query_row_top_k ( row, 3 ) . unwrap ( ) ;
1312+ assert_eq ! ( lazy_result. len( ) , eager_result. len( ) , "row {}" , row) ;
1313+ for ( got, want) in lazy_result. iter ( ) . zip ( eager_result. iter ( ) ) {
1314+ assert_eq ! ( got. 1 , want. 1 , "row {}" , row) ;
1315+ assert ! (
1316+ ( got. 0 - want. 0 ) . abs( ) < 1e-12 ,
1317+ "row {}: distance mismatch {} vs {}" ,
1318+ row,
1319+ got. 0 ,
1320+ want. 0
1321+ ) ;
1322+ }
1323+ }
1324+ }
1325+
1326+ #[ test]
1327+ fn lazy_rejects_single_row ( ) {
1328+ let x = DenseMatrix :: < f64 > :: from_2d_array ( & [ & [ 1.0 , 2.0 ] ] ) . unwrap ( ) ;
1329+ assert ! ( CosinePair :: lazy( & x, 1 ) . is_err( ) ) ;
1330+ }
1331+
12631332 #[ test]
12641333 fn query_row_top_k_samples_strided_candidates_when_approximate_is_true ( ) {
12651334 let x = mixed_direction_rows ( ) ;
0 commit comments