Skip to main content

oxicuda_sparse/ops/
krylov.rs

1//! Krylov subspace methods for sparse eigenvalue computation.
2//!
3//! This module provides GPU-accelerated Lanczos and Arnoldi iteration
4//! for computing extreme eigenvalues and eigenvectors of large sparse matrices.
5//!
6//! - [`LanczosPlan`] -- Lanczos iteration for symmetric matrices, producing a
7//!   tridiagonal matrix whose eigenvalues approximate those of the original matrix.
8//! - [`ArnoldiPlan`] -- Arnoldi iteration for general (non-symmetric) matrices,
9//!   producing an upper Hessenberg matrix.
10//!
11//! Both methods rely on repeated SpMV (sparse matrix-vector multiplication)
12//! as the core computational primitive, combined with vector orthogonalization
13//! kernels generated as PTX at runtime.
14
15use oxicuda_ptx::prelude::*;
16
17use crate::error::{SparseError, SparseResult};
18use crate::ptx_helpers::{
19    emit_warp_reduce_sum, load_float_imm, load_global_float, mul_float, store_global_float,
20};
21
22// ---------------------------------------------------------------------------
23// Common types
24// ---------------------------------------------------------------------------
25
26/// Specifies which eigenvalues to target in Krylov iteration.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum EigenTarget {
29    /// Eigenvalues with the largest absolute value.
30    LargestMagnitude,
31    /// Eigenvalues with the smallest absolute value.
32    SmallestMagnitude,
33    /// Eigenvalues with the largest real part (algebraic maximum for symmetric).
34    LargestAlgebraic,
35    /// Eigenvalues with the smallest real part (algebraic minimum for symmetric).
36    SmallestAlgebraic,
37}
38
39/// Default block size for Krylov vector operations.
40pub const KRYLOV_BLOCK_SIZE: u32 = 256;
41
42// ---------------------------------------------------------------------------
43// Lanczos iteration (symmetric matrices)
44// ---------------------------------------------------------------------------
45
46/// Configuration for Lanczos iteration on symmetric sparse matrices.
47#[derive(Debug, Clone)]
48pub struct LanczosConfig {
49    /// Maximum Krylov subspace dimension (number of Lanczos steps).
50    pub max_iterations: usize,
51    /// Convergence tolerance for eigenvalue residuals.
52    pub tolerance: f64,
53    /// Number of eigenvalues to compute.
54    pub num_eigenvalues: usize,
55    /// Which eigenvalues to target.
56    pub which: EigenTarget,
57}
58
59/// Result of a Lanczos iteration.
60#[derive(Debug, Clone)]
61pub struct LanczosResult {
62    /// Converged eigenvalues (sorted according to the target).
63    pub eigenvalues: Vec<f64>,
64    /// Diagonal of the tridiagonal matrix T (alpha coefficients).
65    pub alpha: Vec<f64>,
66    /// Sub-diagonal of the tridiagonal matrix T (beta coefficients).
67    pub beta: Vec<f64>,
68    /// Number of iterations performed.
69    pub iterations: usize,
70    /// Whether the iteration converged within tolerance.
71    pub converged: bool,
72}
73
74/// Lanczos iteration plan for symmetric sparse eigenvalue problems.
75///
76/// Generates PTX kernels for each phase of the Lanczos recurrence:
77/// 1. SpMV: `w = A * v_j` (delegated to the SpMV module)
78/// 2. Dot product: `alpha_j = w . v_j`
79/// 3. Orthogonalization: `w = w - alpha_j * v_j - beta_{j-1} * v_{j-1}`
80/// 4. Norm computation: `beta_j = ||w||`
81/// 5. Normalization: `v_{j+1} = w / beta_j`
82///
83/// Additionally provides full reorthogonalization to maintain numerical stability.
84#[derive(Debug)]
85pub struct LanczosPlan {
86    config: LanczosConfig,
87    /// Dimension of the matrix (n x n).
88    n: usize,
89}
90
91impl LanczosPlan {
92    /// Creates a new Lanczos plan for an n x n symmetric matrix.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`SparseError::InvalidArgument`] if configuration is invalid:
97    /// - `n == 0`
98    /// - `num_eigenvalues == 0`
99    /// - `max_iterations < num_eigenvalues`
100    /// - `max_iterations > n`
101    /// - `tolerance <= 0`
102    pub fn new(config: LanczosConfig, n: usize) -> SparseResult<Self> {
103        if n == 0 {
104            return Err(SparseError::InvalidArgument(
105                "matrix dimension n must be positive".to_string(),
106            ));
107        }
108        if config.num_eigenvalues == 0 {
109            return Err(SparseError::InvalidArgument(
110                "num_eigenvalues must be positive".to_string(),
111            ));
112        }
113        if config.max_iterations < config.num_eigenvalues {
114            return Err(SparseError::InvalidArgument(format!(
115                "max_iterations ({}) must be >= num_eigenvalues ({})",
116                config.max_iterations, config.num_eigenvalues
117            )));
118        }
119        if config.max_iterations > n {
120            return Err(SparseError::InvalidArgument(format!(
121                "max_iterations ({}) must be <= matrix dimension n ({})",
122                config.max_iterations, n
123            )));
124        }
125        if config.tolerance <= 0.0 {
126            return Err(SparseError::InvalidArgument(
127                "tolerance must be positive".to_string(),
128            ));
129        }
130
131        Ok(Self { config, n })
132    }
133
134    /// Returns the configuration for this plan.
135    #[inline]
136    pub fn config(&self) -> &LanczosConfig {
137        &self.config
138    }
139
140    /// Returns the matrix dimension.
141    #[inline]
142    pub fn dimension(&self) -> usize {
143        self.n
144    }
145
146    /// Returns the workspace size in bytes needed for f64 Lanczos vectors.
147    ///
148    /// The workspace must hold:
149    /// - `max_iterations + 1` Lanczos vectors of dimension `n` (for reorthogonalization)
150    /// - 1 work vector `w` of dimension `n`
151    /// - `max_iterations` alpha values
152    /// - `max_iterations` beta values
153    pub fn workspace_bytes_f64(&self) -> usize {
154        let k = self.config.max_iterations;
155        let n = self.n;
156        let vectors = (k + 2) * n * 8; // (k+1) Lanczos vecs + 1 work vec, f64
157        let scalars = (k + k) * 8; // alpha + beta arrays
158        vectors + scalars
159    }
160
161    /// Returns the workspace size in bytes needed for f32 Lanczos vectors.
162    pub fn workspace_bytes_f32(&self) -> usize {
163        let k = self.config.max_iterations;
164        let n = self.n;
165        let vectors = (k + 2) * n * 4;
166        let scalars = (k + k) * 4;
167        vectors + scalars
168    }
169
170    /// Generates PTX for a single Lanczos step kernel (f64).
171    ///
172    /// The kernel performs the orthogonalization and normalization phases
173    /// of one Lanczos iteration:
174    ///
175    /// ```text
176    /// // Input: w = A * v_j (SpMV already done)
177    /// alpha_j = dot(w, v_j)                                  // dot product
178    /// w = w - alpha_j * v_j - beta_{j-1} * v_{j-1}           // orthogonalize
179    /// beta_j = ||w||                                          // norm
180    /// v_{j+1} = w / beta_j                                   // normalize
181    /// ```
182    ///
183    /// # Kernel Parameters
184    ///
185    /// - `w_ptr`: device pointer to work vector w (length n), modified in-place
186    /// - `v_j_ptr`: device pointer to current Lanczos vector v_j (length n)
187    /// - `v_jm1_ptr`: device pointer to previous Lanczos vector v_{j-1} (length n)
188    /// - `v_jp1_ptr`: device pointer to output vector v_{j+1} (length n)
189    /// - `alpha_ptr`: device pointer to output scalar alpha_j
190    /// - `beta_prev`: the previous beta_{j-1} value (as bits)
191    /// - `beta_out_ptr`: device pointer to output scalar beta_j
192    /// - `n`: vector length
193    ///
194    /// # Errors
195    ///
196    /// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
197    pub fn generate_lanczos_step_ptx(&self) -> SparseResult<String> {
198        emit_lanczos_step_f64(self.n)
199    }
200
201    /// Generates PTX for a single Lanczos step kernel (f32).
202    ///
203    /// Same semantics as [`generate_lanczos_step_ptx`](Self::generate_lanczos_step_ptx)
204    /// but for single-precision floating point.
205    pub fn generate_lanczos_step_ptx_f32(&self) -> SparseResult<String> {
206        emit_lanczos_step_f32(self.n)
207    }
208
209    /// Generates PTX for full reorthogonalization against all previous Lanczos vectors.
210    ///
211    /// This kernel applies modified Gram-Schmidt orthogonalization of `w`
212    /// against the first `j` Lanczos vectors stored column-major in `V`.
213    ///
214    /// ```text
215    /// for i in 0..j:
216    ///     h = dot(w, V[:, i])
217    ///     w = w - h * V[:, i]
218    /// ```
219    ///
220    /// Each thread handles one element of `w` and iterates over all `j` vectors,
221    /// using warp reductions for the dot products.
222    ///
223    /// # Kernel Parameters
224    ///
225    /// - `w_ptr`: device pointer to vector to orthogonalize (length n), modified in-place
226    /// - `v_basis_ptr`: device pointer to basis matrix V stored column-major (n x j)
227    /// - `coeffs_ptr`: device pointer to output coefficients h_i (length j)
228    /// - `num_vecs`: number of basis vectors j to orthogonalize against
229    /// - `n`: vector length
230    ///
231    /// # Errors
232    ///
233    /// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
234    pub fn generate_reorthogonalize_ptx(&self) -> SparseResult<String> {
235        emit_reorthogonalize_f64(self.n)
236    }
237
238    /// Generates PTX for full reorthogonalization (f32 variant).
239    pub fn generate_reorthogonalize_ptx_f32(&self) -> SparseResult<String> {
240        emit_reorthogonalize_f32(self.n)
241    }
242
243    /// Generates PTX for the dot product reduction kernel (f64).
244    ///
245    /// Used to compute `alpha_j = dot(w, v_j)` in the Lanczos recurrence.
246    pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
247        emit_dot_product_reduce_f64(self.n)
248    }
249
250    /// Generates PTX for the dot product reduction kernel (f32).
251    pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
252        emit_dot_product_reduce_f32(self.n)
253    }
254
255    /// Generates PTX for the vector norm-squared reduction kernel (f64).
256    ///
257    /// Used to compute `beta_j = ||w||` (caller takes sqrt of the result).
258    pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
259        emit_norm_sq_reduce_f64(self.n)
260    }
261
262    /// Generates PTX for the vector norm-squared reduction kernel (f32).
263    pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
264        emit_norm_sq_reduce_f32(self.n)
265    }
266}
267
268// ---------------------------------------------------------------------------
269// Arnoldi iteration (general matrices)
270// ---------------------------------------------------------------------------
271
272/// Configuration for Arnoldi iteration on general sparse matrices.
273#[derive(Debug, Clone)]
274pub struct ArnoldiConfig {
275    /// Maximum Krylov subspace dimension (number of Arnoldi steps).
276    pub max_iterations: usize,
277    /// Convergence tolerance for eigenvalue residuals.
278    pub tolerance: f64,
279    /// Number of eigenvalues to compute.
280    pub num_eigenvalues: usize,
281    /// Which eigenvalues to target.
282    pub which: EigenTarget,
283}
284
285/// Result of an Arnoldi iteration.
286#[derive(Debug, Clone)]
287pub struct ArnoldiResult {
288    /// Converged eigenvalues as (real, imaginary) pairs.
289    pub eigenvalues: Vec<(f64, f64)>,
290    /// Upper Hessenberg matrix H (stored as row-major dense Vec\<Vec\<`f64`\>\>).
291    pub hessenberg: Vec<Vec<f64>>,
292    /// Number of iterations performed.
293    pub iterations: usize,
294    /// Whether the iteration converged within tolerance.
295    pub converged: bool,
296}
297
298/// Arnoldi iteration plan for general sparse eigenvalue problems.
299///
300/// Generates PTX kernels for each phase of the Arnoldi recurrence:
301/// 1. SpMV: `w = A * v_j` (delegated to the SpMV module)
302/// 2. Modified Gram-Schmidt: for i = 0..j: `h_{i,j} = w . v_i`, `w = w - h_{i,j} * v_i`
303/// 3. Norm and normalize: `h_{j+1,j} = ||w||`, `v_{j+1} = w / h_{j+1,j}`
304///
305/// Unlike Lanczos (which only orthogonalizes against 2 vectors), Arnoldi
306/// orthogonalizes against all previous basis vectors at every step.
307#[derive(Debug)]
308pub struct ArnoldiPlan {
309    config: ArnoldiConfig,
310    /// Dimension of the matrix (n x n).
311    n: usize,
312}
313
314impl ArnoldiPlan {
315    /// Creates a new Arnoldi plan for an n x n general matrix.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`SparseError::InvalidArgument`] if configuration is invalid:
320    /// - `n == 0`
321    /// - `num_eigenvalues == 0`
322    /// - `max_iterations < num_eigenvalues`
323    /// - `max_iterations > n`
324    /// - `tolerance <= 0`
325    pub fn new(config: ArnoldiConfig, n: usize) -> SparseResult<Self> {
326        if n == 0 {
327            return Err(SparseError::InvalidArgument(
328                "matrix dimension n must be positive".to_string(),
329            ));
330        }
331        if config.num_eigenvalues == 0 {
332            return Err(SparseError::InvalidArgument(
333                "num_eigenvalues must be positive".to_string(),
334            ));
335        }
336        if config.max_iterations < config.num_eigenvalues {
337            return Err(SparseError::InvalidArgument(format!(
338                "max_iterations ({}) must be >= num_eigenvalues ({})",
339                config.max_iterations, config.num_eigenvalues
340            )));
341        }
342        if config.max_iterations > n {
343            return Err(SparseError::InvalidArgument(format!(
344                "max_iterations ({}) must be <= matrix dimension n ({})",
345                config.max_iterations, n
346            )));
347        }
348        if config.tolerance <= 0.0 {
349            return Err(SparseError::InvalidArgument(
350                "tolerance must be positive".to_string(),
351            ));
352        }
353
354        Ok(Self { config, n })
355    }
356
357    /// Returns the configuration for this plan.
358    #[inline]
359    pub fn config(&self) -> &ArnoldiConfig {
360        &self.config
361    }
362
363    /// Returns the matrix dimension.
364    #[inline]
365    pub fn dimension(&self) -> usize {
366        self.n
367    }
368
369    /// Returns the workspace size in bytes needed for f64 Arnoldi vectors.
370    ///
371    /// The workspace must hold:
372    /// - `max_iterations + 1` Arnoldi vectors of dimension `n`
373    /// - 1 work vector `w` of dimension `n`
374    /// - `(max_iterations + 1) x max_iterations` Hessenberg matrix H
375    pub fn workspace_bytes_f64(&self) -> usize {
376        let k = self.config.max_iterations;
377        let n = self.n;
378        let vectors = (k + 2) * n * 8; // (k+1) basis vecs + 1 work vec
379        let hessenberg = (k + 1) * k * 8; // H is (k+1) x k
380        vectors + hessenberg
381    }
382
383    /// Returns the workspace size in bytes needed for f32 Arnoldi vectors.
384    pub fn workspace_bytes_f32(&self) -> usize {
385        let k = self.config.max_iterations;
386        let n = self.n;
387        let vectors = (k + 2) * n * 4;
388        let hessenberg = (k + 1) * k * 4;
389        vectors + hessenberg
390    }
391
392    /// Generates PTX for a single Arnoldi step kernel (f64).
393    ///
394    /// The kernel performs the modified Gram-Schmidt orthogonalization
395    /// and normalization of one Arnoldi iteration step:
396    ///
397    /// ```text
398    /// // Input: w = A * v_j (SpMV already done)
399    /// for i in 0..j:
400    ///     h_{i,j} = dot(w, v_i)
401    ///     w = w - h_{i,j} * v_i
402    /// h_{j+1,j} = ||w||
403    /// v_{j+1} = w / h_{j+1,j}
404    /// ```
405    ///
406    /// # Kernel Parameters
407    ///
408    /// - `w_ptr`: device pointer to work vector w (length n), modified in-place
409    /// - `v_basis_ptr`: device pointer to basis matrix V column-major (n x (j+1))
410    /// - `h_col_ptr`: device pointer to Hessenberg column h_{:,j} (length j+1)
411    /// - `v_jp1_ptr`: device pointer to output vector v_{j+1} (length n)
412    /// - `j`: current iteration index (number of existing basis vectors)
413    /// - `n`: vector length
414    ///
415    /// # Errors
416    ///
417    /// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
418    pub fn generate_arnoldi_step_ptx(&self) -> SparseResult<String> {
419        emit_arnoldi_step_f64(self.n)
420    }
421
422    /// Generates PTX for a single Arnoldi step kernel (f32).
423    pub fn generate_arnoldi_step_ptx_f32(&self) -> SparseResult<String> {
424        emit_arnoldi_step_f32(self.n)
425    }
426
427    /// Generates PTX for modified Gram-Schmidt orthogonalization kernel (f64).
428    ///
429    /// This is the inner loop of Arnoldi: orthogonalize `w` against all
430    /// existing basis vectors using modified Gram-Schmidt. This kernel
431    /// uses warp-level reductions for the dot products.
432    ///
433    /// # Kernel Parameters
434    ///
435    /// - `w_ptr`: device pointer to vector to orthogonalize (length n)
436    /// - `v_basis_ptr`: device pointer to basis matrix V column-major (n x j)
437    /// - `coeffs_ptr`: device pointer to output coefficients h_{i,j} (length j)
438    /// - `num_vecs`: number of basis vectors j
439    /// - `n`: vector length
440    ///
441    /// # Errors
442    ///
443    /// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
444    pub fn generate_gram_schmidt_ptx(&self) -> SparseResult<String> {
445        emit_gram_schmidt_f64(self.n)
446    }
447
448    /// Generates PTX for modified Gram-Schmidt orthogonalization kernel (f32).
449    pub fn generate_gram_schmidt_ptx_f32(&self) -> SparseResult<String> {
450        emit_gram_schmidt_f32(self.n)
451    }
452
453    /// Generates PTX for the dot product reduction kernel (f64).
454    ///
455    /// Used to compute `h_{i,j} = dot(w, v_i)` in the Arnoldi recurrence.
456    pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
457        emit_dot_product_reduce_f64(self.n)
458    }
459
460    /// Generates PTX for the dot product reduction kernel (f32).
461    pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
462        emit_dot_product_reduce_f32(self.n)
463    }
464
465    /// Generates PTX for the vector norm-squared reduction kernel (f64).
466    ///
467    /// Used to compute `h_{j+1,j} = ||w||` (caller takes sqrt of the result).
468    pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
469        emit_norm_sq_reduce_f64(self.n)
470    }
471
472    /// Generates PTX for the vector norm-squared reduction kernel (f32).
473    pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
474        emit_norm_sq_reduce_f32(self.n)
475    }
476}
477
478// ---------------------------------------------------------------------------
479// PTX emission: Lanczos step
480// ---------------------------------------------------------------------------
481
482/// Emits PTX for the Lanczos orthogonalization + normalization step (f64).
483///
484/// Kernel: `lanczos_step_f64`
485///
486/// Each thread handles one element of the vectors. The kernel:
487/// 1. Computes partial dot product `w[tid] * v_j[tid]` (summed via warp reduce + atomics)
488/// 2. Orthogonalizes `w[tid] -= alpha * v_j[tid] + beta_prev * v_jm1[tid]`
489/// 3. Computes partial norm `w[tid]^2` (summed via warp reduce + atomics)
490/// 4. Normalizes `v_jp1[tid] = w[tid] / beta`
491///
492/// Because dot product and norm require global synchronization, this kernel
493/// is designed for a two-pass approach: pass 1 computes alpha (dot), pass 2
494/// computes orthogonalization + beta (norm) + normalization.
495fn emit_lanczos_step_f64(n: usize) -> SparseResult<String> {
496    emit_lanczos_step_typed::<f64>(n, "lanczos_step_f64")
497}
498
499/// Emits PTX for the Lanczos step (f32).
500fn emit_lanczos_step_f32(n: usize) -> SparseResult<String> {
501    emit_lanczos_step_typed::<f32>(n, "lanczos_step_f32")
502}
503
504/// Generic Lanczos step PTX emitter.
505///
506/// This kernel handles the orthogonalization pass:
507/// `w[i] = w[i] - alpha * v_j[i] - beta_prev * v_jm1[i]`
508/// and then normalizes: `v_jp1[i] = w[i] / beta_j`
509///
510/// The dot products (alpha computation) and norm (beta computation) are
511/// handled by separate reduction kernels launched from the host.
512fn emit_lanczos_step_typed<T: oxicuda_blas::GpuFloat>(
513    _n: usize,
514    kernel_name: &str,
515) -> SparseResult<String> {
516    let is_f64 = T::SIZE == 8;
517    let elem_bytes = T::size_u32();
518    let mov_suffix = if is_f64 { "f64" } else { "f32" };
519
520    KernelBuilder::new(kernel_name)
521        .target(SmVersion::Sm80)
522        .param("w_ptr", PtxType::U64)
523        .param("v_j_ptr", PtxType::U64)
524        .param("v_jm1_ptr", PtxType::U64)
525        .param("v_jp1_ptr", PtxType::U64)
526        .param("alpha_bits", PtxType::U64)
527        .param("beta_prev_bits", PtxType::U64)
528        .param("beta_j_bits", PtxType::U64)
529        .param("n", PtxType::U32)
530        .body(move |b| {
531            let gid = b.global_thread_id_x();
532            let n_param = b.load_param_u32("n");
533
534            let gid_inner = gid.clone();
535            b.if_lt_u32(gid, n_param, move |b| {
536                let tid = gid_inner;
537                let w_ptr = b.load_param_u64("w_ptr");
538                let v_j_ptr = b.load_param_u64("v_j_ptr");
539                let v_jm1_ptr = b.load_param_u64("v_jm1_ptr");
540                let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
541                let alpha_bits = b.load_param_u64("alpha_bits");
542                let beta_prev_bits = b.load_param_u64("beta_prev_bits");
543                let beta_j_bits = b.load_param_u64("beta_j_bits");
544
545                let alpha = reinterpret_bits::<T>(b, alpha_bits);
546                let beta_prev = reinterpret_bits::<T>(b, beta_prev_bits);
547                let beta_j = reinterpret_bits::<T>(b, beta_j_bits);
548
549                // Load w[tid], v_j[tid], v_jm1[tid]
550                let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
551                let w_val = load_global_float::<T>(b, w_addr.clone());
552
553                let vj_addr = b.byte_offset_addr(v_j_ptr, tid.clone(), elem_bytes);
554                let vj_val = load_global_float::<T>(b, vj_addr);
555
556                let vjm1_addr = b.byte_offset_addr(v_jm1_ptr, tid.clone(), elem_bytes);
557                let vjm1_val = load_global_float::<T>(b, vjm1_addr);
558
559                // Orthogonalize: w[i] = w[i] - alpha * v_j[i] - beta_prev * v_jm1[i]
560                let alpha_vj = mul_float::<T>(b, alpha, vj_val);
561                let beta_vjm1 = mul_float::<T>(b, beta_prev, vjm1_val);
562                let sub1 = sub_float::<T>(b, w_val, alpha_vj);
563                let w_orth = sub_float::<T>(b, sub1, beta_vjm1);
564
565                // Store orthogonalized w
566                store_global_float::<T>(b, w_addr, w_orth.clone());
567
568                // Normalize: v_jp1[i] = w_orth[i] / beta_j
569                let v_jp1_val = div_float::<T>(b, w_orth, beta_j);
570                let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
571                store_global_float::<T>(b, vjp1_addr, v_jp1_val);
572            });
573
574            // Suppress unused variable warning on mov_suffix
575            let _ = mov_suffix;
576
577            b.ret();
578        })
579        .build()
580        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
581}
582
583// ---------------------------------------------------------------------------
584// PTX emission: Reorthogonalization
585// ---------------------------------------------------------------------------
586
587/// Emits PTX for full reorthogonalization kernel (f64).
588fn emit_reorthogonalize_f64(n: usize) -> SparseResult<String> {
589    emit_reorthogonalize_typed::<f64>(n, "reorthogonalize_f64")
590}
591
592/// Emits PTX for full reorthogonalization kernel (f32).
593fn emit_reorthogonalize_f32(n: usize) -> SparseResult<String> {
594    emit_reorthogonalize_typed::<f32>(n, "reorthogonalize_f32")
595}
596
597/// Generic reorthogonalization PTX emitter.
598///
599/// For each basis vector `v_i` (i in 0..num_vecs):
600///   1. Compute partial dot: `w[tid] * v_i[tid]` -> warp reduce -> atomic add to coeffs[i]
601///   2. Subtract projection: `w[tid] -= coeff * v_i[tid]`
602///
603/// This requires a grid-wide sync between steps 1 and 2 for each vector,
604/// so the kernel processes one basis vector per launch. The host loops
605/// over basis vectors, launching this kernel `num_vecs` times.
606///
607/// Kernel: `reorthogonalize_{f32,f64}`
608/// Params: w_ptr, v_i_ptr, dot_result_ptr, n
609fn emit_reorthogonalize_typed<T: oxicuda_blas::GpuFloat>(
610    _n: usize,
611    kernel_name: &str,
612) -> SparseResult<String> {
613    let elem_bytes = T::size_u32();
614
615    KernelBuilder::new(kernel_name)
616        .target(SmVersion::Sm80)
617        .param("w_ptr", PtxType::U64)
618        .param("v_i_ptr", PtxType::U64)
619        .param("coeff_bits", PtxType::U64)
620        .param("n", PtxType::U32)
621        .body(move |b| {
622            let gid = b.global_thread_id_x();
623            let n_param = b.load_param_u32("n");
624
625            let gid_inner = gid.clone();
626            b.if_lt_u32(gid, n_param, move |b| {
627                let tid = gid_inner;
628                let w_ptr = b.load_param_u64("w_ptr");
629                let v_i_ptr = b.load_param_u64("v_i_ptr");
630                let coeff_bits = b.load_param_u64("coeff_bits");
631
632                let coeff = reinterpret_bits::<T>(b, coeff_bits);
633
634                // Load w[tid] and v_i[tid]
635                let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
636                let w_val = load_global_float::<T>(b, w_addr.clone());
637
638                let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
639                let vi_val = load_global_float::<T>(b, vi_addr);
640
641                // w[tid] -= coeff * v_i[tid]
642                let proj = mul_float::<T>(b, coeff, vi_val);
643                let w_new = sub_float::<T>(b, w_val, proj);
644
645                store_global_float::<T>(b, w_addr, w_new);
646            });
647
648            b.ret();
649        })
650        .build()
651        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
652}
653
654// ---------------------------------------------------------------------------
655// PTX emission: Arnoldi step
656// ---------------------------------------------------------------------------
657
658/// Emits PTX for the Arnoldi orthogonalization step (f64).
659fn emit_arnoldi_step_f64(n: usize) -> SparseResult<String> {
660    emit_arnoldi_step_typed::<f64>(n, "arnoldi_step_f64")
661}
662
663/// Emits PTX for the Arnoldi step (f32).
664fn emit_arnoldi_step_f32(n: usize) -> SparseResult<String> {
665    emit_arnoldi_step_typed::<f32>(n, "arnoldi_step_f32")
666}
667
668/// Generic Arnoldi step PTX emitter.
669///
670/// The Arnoldi step kernel handles the normalization phase:
671/// `v_jp1[i] = w[i] / h_{j+1,j}`
672///
673/// The modified Gram-Schmidt orthogonalization (computing h_{i,j} and
674/// subtracting projections) is handled by the separate Gram-Schmidt kernel,
675/// which is launched once per basis vector from the host.
676///
677/// Kernel: `arnoldi_step_{f32,f64}`
678/// Params: w_ptr, v_jp1_ptr, h_jp1_j_bits (norm), n
679fn emit_arnoldi_step_typed<T: oxicuda_blas::GpuFloat>(
680    _n: usize,
681    kernel_name: &str,
682) -> SparseResult<String> {
683    let elem_bytes = T::size_u32();
684
685    KernelBuilder::new(kernel_name)
686        .target(SmVersion::Sm80)
687        .param("w_ptr", PtxType::U64)
688        .param("v_jp1_ptr", PtxType::U64)
689        .param("h_jp1_j_bits", PtxType::U64)
690        .param("n", PtxType::U32)
691        .body(move |b| {
692            let gid = b.global_thread_id_x();
693            let n_param = b.load_param_u32("n");
694
695            let gid_inner = gid.clone();
696            b.if_lt_u32(gid, n_param, move |b| {
697                let tid = gid_inner;
698                let w_ptr = b.load_param_u64("w_ptr");
699                let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
700                let h_bits = b.load_param_u64("h_jp1_j_bits");
701
702                let h_jp1_j = reinterpret_bits::<T>(b, h_bits);
703
704                // Load w[tid]
705                let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
706                let w_val = load_global_float::<T>(b, w_addr);
707
708                // v_jp1[tid] = w[tid] / h_{j+1,j}
709                let v_new = div_float::<T>(b, w_val, h_jp1_j);
710                let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
711                store_global_float::<T>(b, vjp1_addr, v_new);
712            });
713
714            b.ret();
715        })
716        .build()
717        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
718}
719
720// ---------------------------------------------------------------------------
721// PTX emission: Modified Gram-Schmidt
722// ---------------------------------------------------------------------------
723
724/// Emits PTX for modified Gram-Schmidt projection kernel (f64).
725fn emit_gram_schmidt_f64(n: usize) -> SparseResult<String> {
726    emit_gram_schmidt_typed::<f64>(n, "gram_schmidt_f64")
727}
728
729/// Emits PTX for modified Gram-Schmidt projection kernel (f32).
730fn emit_gram_schmidt_f32(n: usize) -> SparseResult<String> {
731    emit_gram_schmidt_typed::<f32>(n, "gram_schmidt_f32")
732}
733
734/// Generic modified Gram-Schmidt PTX emitter.
735///
736/// This kernel subtracts the projection of `w` onto a single basis vector `v_i`:
737///   `w[tid] -= h_{i,j} * v_i[tid]`
738///
739/// The dot product `h_{i,j} = dot(w, v_i)` is computed separately via a
740/// reduction kernel. The host launches this kernel once per basis vector
741/// in the modified Gram-Schmidt loop: for i in 0..j.
742///
743/// Kernel: `gram_schmidt_{f32,f64}`
744/// Params: w_ptr, v_i_ptr, h_ij_bits, n
745fn emit_gram_schmidt_typed<T: oxicuda_blas::GpuFloat>(
746    _n: usize,
747    kernel_name: &str,
748) -> SparseResult<String> {
749    let elem_bytes = T::size_u32();
750
751    KernelBuilder::new(kernel_name)
752        .target(SmVersion::Sm80)
753        .param("w_ptr", PtxType::U64)
754        .param("v_i_ptr", PtxType::U64)
755        .param("h_ij_bits", PtxType::U64)
756        .param("n", PtxType::U32)
757        .body(move |b| {
758            let gid = b.global_thread_id_x();
759            let n_param = b.load_param_u32("n");
760
761            let gid_inner = gid.clone();
762            b.if_lt_u32(gid, n_param, move |b| {
763                let tid = gid_inner;
764                let w_ptr = b.load_param_u64("w_ptr");
765                let v_i_ptr = b.load_param_u64("v_i_ptr");
766                let h_bits = b.load_param_u64("h_ij_bits");
767
768                let h_ij = reinterpret_bits::<T>(b, h_bits);
769
770                // Load w[tid] and v_i[tid]
771                let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
772                let w_val = load_global_float::<T>(b, w_addr.clone());
773
774                let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
775                let vi_val = load_global_float::<T>(b, vi_addr);
776
777                // w[tid] -= h_{i,j} * v_i[tid]
778                let proj = mul_float::<T>(b, h_ij, vi_val);
779                let w_new = sub_float::<T>(b, w_val, proj);
780
781                store_global_float::<T>(b, w_addr, w_new);
782            });
783
784            b.ret();
785        })
786        .build()
787        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
788}
789
790// ---------------------------------------------------------------------------
791// PTX emission: Dot product reduction
792// ---------------------------------------------------------------------------
793
794/// Emits PTX for a warp-level partial dot product kernel.
795///
796/// Each warp computes a partial dot product of two vectors and the first
797/// lane atomically adds the result to a global accumulator. This is used
798/// by both Lanczos (alpha computation) and Arnoldi (h_{i,j} computation).
799///
800/// Kernel: `dot_product_reduce_{f32,f64}`
801/// Params: a_ptr, b_ptr, result_ptr, n
802fn emit_dot_product_reduce_f64(_n: usize) -> SparseResult<String> {
803    emit_dot_product_reduce_typed::<f64>("dot_product_reduce_f64")
804}
805
806fn emit_dot_product_reduce_f32(_n: usize) -> SparseResult<String> {
807    emit_dot_product_reduce_typed::<f32>("dot_product_reduce_f32")
808}
809
810fn emit_dot_product_reduce_typed<T: oxicuda_blas::GpuFloat>(
811    kernel_name: &str,
812) -> SparseResult<String> {
813    let elem_bytes = T::size_u32();
814
815    KernelBuilder::new(kernel_name)
816        .target(SmVersion::Sm80)
817        .param("a_ptr", PtxType::U64)
818        .param("b_ptr", PtxType::U64)
819        .param("result_ptr", PtxType::U64)
820        .param("n", PtxType::U32)
821        .body(move |b| {
822            let gid = b.global_thread_id_x();
823            let n_param = b.load_param_u32("n");
824
825            // Save gid for lane computation after the closure
826            let gid_for_lane = gid.clone();
827
828            // Each thread computes a[gid] * b[gid] if in bounds, else 0
829            let prod = load_float_imm::<T>(b, 0.0);
830
831            let gid_inner = gid.clone();
832            let prod_inner = prod.clone();
833            b.if_lt_u32(gid, n_param, move |b| {
834                let tid = gid_inner;
835                let a_ptr = b.load_param_u64("a_ptr");
836                let b_ptr_reg = b.load_param_u64("b_ptr");
837
838                let a_addr = b.byte_offset_addr(a_ptr, tid.clone(), elem_bytes);
839                let a_val = load_global_float::<T>(b, a_addr);
840
841                let b_addr = b.byte_offset_addr(b_ptr_reg, tid, elem_bytes);
842                let b_val = load_global_float::<T>(b, b_addr);
843
844                let p = mul_float::<T>(b, a_val, b_val);
845                let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
846                b.raw_ptx(&format!("mov.{suffix} {prod_inner}, {p};"));
847            });
848
849            // Warp reduce
850            let reduced = emit_warp_reduce_sum::<T>(b, prod);
851
852            // Lane 0 atomically adds to result
853            let lane = b.alloc_reg(PtxType::U32);
854            b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
855
856            // Only lane 0 writes; every other lane skips ahead. Inverted
857            // skip-branch (`setp.eq` -> `setp.ne`) via the structured
858            // `branch_if` so the target matches the `$`-prefixed label.
859            let not_lane_0 = b.alloc_reg(PtxType::Pred);
860            b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
861
862            let skip_label = b.fresh_label("dot_skip");
863            b.branch_if(not_lane_0, &skip_label);
864
865            let result_ptr = b.load_param_u64("result_ptr");
866            crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
867
868            b.label(&skip_label);
869
870            b.ret();
871        })
872        .build()
873        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
874}
875
876// ---------------------------------------------------------------------------
877// PTX emission: Vector norm (squared) reduction
878// ---------------------------------------------------------------------------
879
880/// Emits PTX for warp-level vector norm-squared reduction kernel.
881///
882/// Each warp computes partial ||v||^2 and lane 0 atomically adds to result.
883/// The host takes sqrt of the accumulated result to get the 2-norm.
884///
885/// Kernel: `norm_sq_reduce_{f32,f64}`
886/// Params: v_ptr, result_ptr, n
887fn emit_norm_sq_reduce_f64(_n: usize) -> SparseResult<String> {
888    emit_norm_sq_reduce_typed::<f64>("norm_sq_reduce_f64")
889}
890
891fn emit_norm_sq_reduce_f32(_n: usize) -> SparseResult<String> {
892    emit_norm_sq_reduce_typed::<f32>("norm_sq_reduce_f32")
893}
894
895fn emit_norm_sq_reduce_typed<T: oxicuda_blas::GpuFloat>(kernel_name: &str) -> SparseResult<String> {
896    let elem_bytes = T::size_u32();
897
898    KernelBuilder::new(kernel_name)
899        .target(SmVersion::Sm80)
900        .param("v_ptr", PtxType::U64)
901        .param("result_ptr", PtxType::U64)
902        .param("n", PtxType::U32)
903        .body(move |b| {
904            let gid = b.global_thread_id_x();
905            let n_param = b.load_param_u32("n");
906
907            // Save gid for lane computation after the closure
908            let gid_for_lane = gid.clone();
909
910            let sq = load_float_imm::<T>(b, 0.0);
911
912            let gid_inner = gid.clone();
913            let sq_inner = sq.clone();
914            b.if_lt_u32(gid, n_param, move |b| {
915                let tid = gid_inner;
916                let v_ptr = b.load_param_u64("v_ptr");
917
918                let v_addr = b.byte_offset_addr(v_ptr, tid, elem_bytes);
919                let v_val = load_global_float::<T>(b, v_addr);
920
921                let p = mul_float::<T>(b, v_val.clone(), v_val);
922                let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
923                b.raw_ptx(&format!("mov.{suffix} {sq_inner}, {p};"));
924            });
925
926            // Warp reduce
927            let reduced = emit_warp_reduce_sum::<T>(b, sq);
928
929            // Lane 0 atomically adds
930            let lane = b.alloc_reg(PtxType::U32);
931            b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
932
933            // Only lane 0 writes; every other lane skips ahead. Inverted
934            // skip-branch (`setp.eq` -> `setp.ne`) via the structured
935            // `branch_if` so the target matches the `$`-prefixed label.
936            let not_lane_0 = b.alloc_reg(PtxType::Pred);
937            b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
938
939            let skip_label = b.fresh_label("norm_skip");
940            b.branch_if(not_lane_0, &skip_label);
941
942            let result_ptr = b.load_param_u64("result_ptr");
943            crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
944
945            b.label(&skip_label);
946
947            b.ret();
948        })
949        .build()
950        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
951}
952
953// ---------------------------------------------------------------------------
954// Helper: float arithmetic
955// ---------------------------------------------------------------------------
956
957/// Reinterprets u64 bits as the float type T (like ptx_helpers::reinterpret_bits_to_float).
958fn reinterpret_bits<T: oxicuda_blas::GpuFloat>(
959    b: &mut BodyBuilder<'_>,
960    bits: Register,
961) -> Register {
962    crate::ptx_helpers::reinterpret_bits_to_float::<T>(b, bits)
963}
964
965/// Emits a subtraction: `dst = a - bv`.
966fn sub_float<T: oxicuda_blas::GpuFloat>(
967    b: &mut BodyBuilder<'_>,
968    a: Register,
969    bv: Register,
970) -> Register {
971    if T::PTX_TYPE == PtxType::F32 {
972        let dst = b.alloc_reg(PtxType::F32);
973        b.raw_ptx(&format!("sub.rn.f32 {dst}, {a}, {bv};"));
974        dst
975    } else {
976        let dst = b.alloc_reg(PtxType::F64);
977        b.raw_ptx(&format!("sub.rn.f64 {dst}, {a}, {bv};"));
978        dst
979    }
980}
981
982/// Emits a division: `dst = a / bv`.
983fn div_float<T: oxicuda_blas::GpuFloat>(
984    b: &mut BodyBuilder<'_>,
985    a: Register,
986    bv: Register,
987) -> Register {
988    if T::PTX_TYPE == PtxType::F32 {
989        let dst = b.alloc_reg(PtxType::F32);
990        b.raw_ptx(&format!("div.rn.f32 {dst}, {a}, {bv};"));
991        dst
992    } else {
993        let dst = b.alloc_reg(PtxType::F64);
994        b.raw_ptx(&format!("div.rn.f64 {dst}, {a}, {bv};"));
995        dst
996    }
997}
998
999// ---------------------------------------------------------------------------
1000// Tests
1001// ---------------------------------------------------------------------------
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use crate::ptx_helpers::test_support::assert_assembles_and_clean;
1007
1008    /// The Krylov dot-product and norm-squared reduction kernels must assemble
1009    /// for sm_86 in both precisions. The lane-0 write branch must use a
1010    /// `$`-prefixed target and the f64 warp reduction must avoid `.b64` shuffles.
1011    #[test]
1012    fn krylov_reductions_f32_f64_assemble_sm86() {
1013        let dot_f32 = emit_dot_product_reduce_f32(1024).expect("dot f32");
1014        assert_assembles_and_clean("krylov_dot_f32", &dot_f32);
1015        let dot_f64 = emit_dot_product_reduce_f64(1024).expect("dot f64");
1016        assert_assembles_and_clean("krylov_dot_f64", &dot_f64);
1017
1018        let norm_f32 = emit_norm_sq_reduce_f32(1024).expect("norm f32");
1019        assert_assembles_and_clean("krylov_norm_f32", &norm_f32);
1020        let norm_f64 = emit_norm_sq_reduce_f64(1024).expect("norm f64");
1021        assert_assembles_and_clean("krylov_norm_f64", &norm_f64);
1022        assert!(
1023            !dot_f64.contains("0F00000000") && !norm_f64.contains("0F00000000"),
1024            "f64 Krylov reduction kernels must not materialize an f32 0.0 immediate"
1025        );
1026    }
1027
1028    // -- Lanczos config validation tests --
1029
1030    #[test]
1031    fn lanczos_new_valid_config() {
1032        let config = LanczosConfig {
1033            max_iterations: 50,
1034            tolerance: 1e-10,
1035            num_eigenvalues: 5,
1036            which: EigenTarget::LargestMagnitude,
1037        };
1038        let plan = LanczosPlan::new(config, 100);
1039        assert!(plan.is_ok());
1040        let plan = plan.expect("test: valid config should succeed");
1041        assert_eq!(plan.dimension(), 100);
1042    }
1043
1044    #[test]
1045    fn lanczos_rejects_zero_dimension() {
1046        let config = LanczosConfig {
1047            max_iterations: 10,
1048            tolerance: 1e-6,
1049            num_eigenvalues: 3,
1050            which: EigenTarget::SmallestMagnitude,
1051        };
1052        let result = LanczosPlan::new(config, 0);
1053        assert!(result.is_err());
1054        match result {
1055            Err(SparseError::InvalidArgument(msg)) => {
1056                assert!(msg.contains("dimension"));
1057            }
1058            other => panic!("expected InvalidArgument, got: {other:?}"),
1059        }
1060    }
1061
1062    #[test]
1063    fn lanczos_rejects_zero_eigenvalues() {
1064        let config = LanczosConfig {
1065            max_iterations: 10,
1066            tolerance: 1e-6,
1067            num_eigenvalues: 0,
1068            which: EigenTarget::LargestAlgebraic,
1069        };
1070        let result = LanczosPlan::new(config, 100);
1071        assert!(result.is_err());
1072    }
1073
1074    #[test]
1075    fn lanczos_rejects_iterations_less_than_eigenvalues() {
1076        let config = LanczosConfig {
1077            max_iterations: 3,
1078            tolerance: 1e-6,
1079            num_eigenvalues: 10,
1080            which: EigenTarget::SmallestAlgebraic,
1081        };
1082        let result = LanczosPlan::new(config, 100);
1083        assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1084    }
1085
1086    #[test]
1087    fn lanczos_rejects_iterations_greater_than_n() {
1088        let config = LanczosConfig {
1089            max_iterations: 200,
1090            tolerance: 1e-6,
1091            num_eigenvalues: 5,
1092            which: EigenTarget::LargestMagnitude,
1093        };
1094        let result = LanczosPlan::new(config, 100);
1095        assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1096    }
1097
1098    #[test]
1099    fn lanczos_rejects_non_positive_tolerance() {
1100        let config = LanczosConfig {
1101            max_iterations: 50,
1102            tolerance: 0.0,
1103            num_eigenvalues: 5,
1104            which: EigenTarget::LargestMagnitude,
1105        };
1106        let result = LanczosPlan::new(config, 100);
1107        assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1108
1109        let config_neg = LanczosConfig {
1110            max_iterations: 50,
1111            tolerance: -1e-6,
1112            num_eigenvalues: 5,
1113            which: EigenTarget::LargestMagnitude,
1114        };
1115        let result_neg = LanczosPlan::new(config_neg, 100);
1116        assert!(matches!(result_neg, Err(SparseError::InvalidArgument(_))));
1117    }
1118
1119    // -- Lanczos PTX generation tests --
1120
1121    #[test]
1122    fn lanczos_step_ptx_f64_generates() {
1123        let config = LanczosConfig {
1124            max_iterations: 30,
1125            tolerance: 1e-10,
1126            num_eigenvalues: 5,
1127            which: EigenTarget::LargestMagnitude,
1128        };
1129        let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1130        let ptx = plan.generate_lanczos_step_ptx();
1131        assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1132        let ptx_str = ptx.expect("test: PTX gen should succeed");
1133        assert!(ptx_str.contains(".entry lanczos_step_f64"));
1134        assert!(ptx_str.contains(".target sm_80"));
1135        // Should reference the parameter names
1136        assert!(ptx_str.contains("w_ptr"));
1137        assert!(ptx_str.contains("v_j_ptr"));
1138    }
1139
1140    #[test]
1141    fn lanczos_step_ptx_f32_generates() {
1142        let config = LanczosConfig {
1143            max_iterations: 20,
1144            tolerance: 1e-6,
1145            num_eigenvalues: 3,
1146            which: EigenTarget::SmallestMagnitude,
1147        };
1148        let plan = LanczosPlan::new(config, 500).expect("test: valid config");
1149        let ptx = plan.generate_lanczos_step_ptx_f32();
1150        assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1151        let ptx_str = ptx.expect("test: PTX gen should succeed");
1152        assert!(ptx_str.contains(".entry lanczos_step_f32"));
1153    }
1154
1155    #[test]
1156    fn lanczos_reorthogonalize_ptx_generates() {
1157        let config = LanczosConfig {
1158            max_iterations: 30,
1159            tolerance: 1e-10,
1160            num_eigenvalues: 5,
1161            which: EigenTarget::LargestAlgebraic,
1162        };
1163        let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1164        let ptx = plan.generate_reorthogonalize_ptx();
1165        assert!(ptx.is_ok(), "Reorthogonalize PTX failed: {ptx:?}");
1166        let ptx_str = ptx.expect("test: PTX gen should succeed");
1167        assert!(ptx_str.contains(".entry reorthogonalize_f64"));
1168        assert!(ptx_str.contains("w_ptr"));
1169    }
1170
1171    // -- Arnoldi config validation tests --
1172
1173    #[test]
1174    fn arnoldi_new_valid_config() {
1175        let config = ArnoldiConfig {
1176            max_iterations: 50,
1177            tolerance: 1e-10,
1178            num_eigenvalues: 5,
1179            which: EigenTarget::LargestMagnitude,
1180        };
1181        let plan = ArnoldiPlan::new(config, 200);
1182        assert!(plan.is_ok());
1183        let plan = plan.expect("test: valid config should succeed");
1184        assert_eq!(plan.dimension(), 200);
1185    }
1186
1187    #[test]
1188    fn arnoldi_rejects_invalid_config() {
1189        // Zero dimension
1190        let config = ArnoldiConfig {
1191            max_iterations: 10,
1192            tolerance: 1e-6,
1193            num_eigenvalues: 3,
1194            which: EigenTarget::LargestMagnitude,
1195        };
1196        assert!(ArnoldiPlan::new(config, 0).is_err());
1197
1198        // max_iterations > n
1199        let config2 = ArnoldiConfig {
1200            max_iterations: 500,
1201            tolerance: 1e-6,
1202            num_eigenvalues: 3,
1203            which: EigenTarget::SmallestMagnitude,
1204        };
1205        assert!(ArnoldiPlan::new(config2, 100).is_err());
1206
1207        // num_eigenvalues > max_iterations
1208        let config3 = ArnoldiConfig {
1209            max_iterations: 5,
1210            tolerance: 1e-6,
1211            num_eigenvalues: 20,
1212            which: EigenTarget::LargestAlgebraic,
1213        };
1214        assert!(ArnoldiPlan::new(config3, 100).is_err());
1215    }
1216
1217    // -- Arnoldi PTX generation tests --
1218
1219    #[test]
1220    fn arnoldi_step_ptx_f64_generates() {
1221        let config = ArnoldiConfig {
1222            max_iterations: 30,
1223            tolerance: 1e-10,
1224            num_eigenvalues: 5,
1225            which: EigenTarget::LargestMagnitude,
1226        };
1227        let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1228        let ptx = plan.generate_arnoldi_step_ptx();
1229        assert!(ptx.is_ok(), "Arnoldi PTX failed: {ptx:?}");
1230        let ptx_str = ptx.expect("test: PTX gen should succeed");
1231        assert!(ptx_str.contains(".entry arnoldi_step_f64"));
1232        assert!(ptx_str.contains("w_ptr"));
1233    }
1234
1235    #[test]
1236    fn arnoldi_step_ptx_f32_generates() {
1237        let config = ArnoldiConfig {
1238            max_iterations: 20,
1239            tolerance: 1e-6,
1240            num_eigenvalues: 3,
1241            which: EigenTarget::SmallestAlgebraic,
1242        };
1243        let plan = ArnoldiPlan::new(config, 300).expect("test: valid config");
1244        let ptx = plan.generate_arnoldi_step_ptx_f32();
1245        assert!(ptx.is_ok(), "Arnoldi f32 PTX failed: {ptx:?}");
1246        let ptx_str = ptx.expect("test: PTX gen should succeed");
1247        assert!(ptx_str.contains(".entry arnoldi_step_f32"));
1248    }
1249
1250    #[test]
1251    fn arnoldi_gram_schmidt_ptx_generates() {
1252        let config = ArnoldiConfig {
1253            max_iterations: 30,
1254            tolerance: 1e-10,
1255            num_eigenvalues: 5,
1256            which: EigenTarget::LargestMagnitude,
1257        };
1258        let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1259        let ptx = plan.generate_gram_schmidt_ptx();
1260        assert!(ptx.is_ok(), "Gram-Schmidt PTX failed: {ptx:?}");
1261        let ptx_str = ptx.expect("test: PTX gen should succeed");
1262        assert!(ptx_str.contains(".entry gram_schmidt_f64"));
1263    }
1264
1265    // -- Workspace size tests --
1266
1267    #[test]
1268    fn lanczos_workspace_size_f64() {
1269        let config = LanczosConfig {
1270            max_iterations: 50,
1271            tolerance: 1e-10,
1272            num_eigenvalues: 5,
1273            which: EigenTarget::LargestMagnitude,
1274        };
1275        let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1276        let ws = plan.workspace_bytes_f64();
1277        // (50+2) * 1000 * 8 + (50+50) * 8 = 52*8000 + 800 = 416000 + 800 = 416800
1278        assert_eq!(ws, 416_800);
1279    }
1280
1281    #[test]
1282    fn lanczos_workspace_size_f32() {
1283        let config = LanczosConfig {
1284            max_iterations: 50,
1285            tolerance: 1e-10,
1286            num_eigenvalues: 5,
1287            which: EigenTarget::LargestMagnitude,
1288        };
1289        let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1290        let ws = plan.workspace_bytes_f32();
1291        // (50+2) * 1000 * 4 + (50+50) * 4 = 208000 + 400 = 208400
1292        assert_eq!(ws, 208_400);
1293    }
1294
1295    #[test]
1296    fn arnoldi_workspace_size_f64() {
1297        let config = ArnoldiConfig {
1298            max_iterations: 30,
1299            tolerance: 1e-10,
1300            num_eigenvalues: 5,
1301            which: EigenTarget::LargestMagnitude,
1302        };
1303        let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1304        let ws = plan.workspace_bytes_f64();
1305        // vectors: (30+2) * 500 * 8 = 128000
1306        // hessenberg: (30+1) * 30 * 8 = 7440
1307        // total = 135440
1308        assert_eq!(ws, 135_440);
1309    }
1310
1311    // -- Tridiagonal structure tests --
1312
1313    #[test]
1314    fn lanczos_result_tridiagonal_structure() {
1315        // Verify that a LanczosResult can represent a proper tridiagonal matrix
1316        let result = LanczosResult {
1317            eigenvalues: vec![5.0, 3.0, 1.0],
1318            alpha: vec![4.0, 3.5, 2.0, 1.5, 1.0], // diagonal
1319            beta: vec![1.2, 0.8, 0.5, 0.3],       // sub-diagonal (length = alpha.len() - 1)
1320            iterations: 5,
1321            converged: true,
1322        };
1323        // Tridiagonal matrix T is k x k where k = alpha.len()
1324        assert_eq!(result.alpha.len(), 5);
1325        assert_eq!(result.beta.len(), result.alpha.len() - 1);
1326        assert!(result.converged);
1327        assert_eq!(result.iterations, 5);
1328    }
1329
1330    // -- Hessenberg structure tests --
1331
1332    #[test]
1333    #[allow(clippy::needless_range_loop)]
1334    fn arnoldi_result_hessenberg_structure() {
1335        // Verify that an ArnoldiResult can represent an upper Hessenberg matrix
1336        let k = 4;
1337        let mut h = vec![vec![0.0; k]; k + 1]; // (k+1) x k
1338        // Fill upper Hessenberg structure: h[i][j] != 0 only if i <= j+1
1339        for j in 0..k {
1340            for i in 0..=j + 1 {
1341                h[i][j] = (i + j + 1) as f64;
1342            }
1343        }
1344        // Verify sub-sub-diagonal is zero (i > j + 1)
1345        for j in 0..k {
1346            for i in (j + 2)..(k + 1) {
1347                assert!(
1348                    (h[i][j]).abs() < 1e-15,
1349                    "h[{i}][{j}] should be zero in upper Hessenberg"
1350                );
1351            }
1352        }
1353
1354        let result = ArnoldiResult {
1355            eigenvalues: vec![(3.0, 0.5), (3.0, -0.5), (1.0, 0.0)],
1356            hessenberg: h,
1357            iterations: k,
1358            converged: true,
1359        };
1360        assert_eq!(result.hessenberg.len(), k + 1);
1361        assert_eq!(result.hessenberg[0].len(), k);
1362        assert!(result.converged);
1363        // Complex eigenvalues come in conjugate pairs
1364        let (r1, i1) = result.eigenvalues[0];
1365        let (r2, i2) = result.eigenvalues[1];
1366        assert!((r1 - r2).abs() < 1e-15, "conjugate pair: same real part");
1367        assert!(
1368            (i1 + i2).abs() < 1e-15,
1369            "conjugate pair: opposite imag part"
1370        );
1371    }
1372
1373    // -- EigenTarget coverage --
1374
1375    #[test]
1376    fn eigen_target_variants() {
1377        // Ensure all variants exist and are distinct
1378        let targets = [
1379            EigenTarget::LargestMagnitude,
1380            EigenTarget::SmallestMagnitude,
1381            EigenTarget::LargestAlgebraic,
1382            EigenTarget::SmallestAlgebraic,
1383        ];
1384        for i in 0..targets.len() {
1385            for j in (i + 1)..targets.len() {
1386                assert_ne!(targets[i], targets[j]);
1387            }
1388        }
1389    }
1390
1391    // -- Dot product and norm reduction PTX tests --
1392
1393    #[test]
1394    fn dot_product_reduce_ptx_f64_generates() {
1395        let ptx = emit_dot_product_reduce_f64(1000);
1396        assert!(ptx.is_ok(), "dot product PTX failed: {ptx:?}");
1397        let ptx_str = ptx.expect("test: PTX gen should succeed");
1398        assert!(ptx_str.contains(".entry dot_product_reduce_f64"));
1399    }
1400
1401    #[test]
1402    fn dot_product_reduce_ptx_f32_generates() {
1403        let ptx = emit_dot_product_reduce_f32(1000);
1404        assert!(ptx.is_ok());
1405        let ptx_str = ptx.expect("test: PTX gen should succeed");
1406        assert!(ptx_str.contains(".entry dot_product_reduce_f32"));
1407    }
1408
1409    #[test]
1410    fn norm_sq_reduce_ptx_generates() {
1411        let ptx_f64 = emit_norm_sq_reduce_f64(1000);
1412        assert!(ptx_f64.is_ok());
1413        let ptx_str = ptx_f64.expect("test: PTX gen should succeed");
1414        assert!(ptx_str.contains(".entry norm_sq_reduce_f64"));
1415
1416        let ptx_f32 = emit_norm_sq_reduce_f32(1000);
1417        assert!(ptx_f32.is_ok());
1418        let ptx_str_f32 = ptx_f32.expect("test: PTX gen should succeed");
1419        assert!(ptx_str_f32.contains(".entry norm_sq_reduce_f32"));
1420    }
1421
1422    // -- Config accessor tests --
1423
1424    #[test]
1425    fn plan_config_accessors() {
1426        let lanczos_config = LanczosConfig {
1427            max_iterations: 40,
1428            tolerance: 1e-8,
1429            num_eigenvalues: 10,
1430            which: EigenTarget::SmallestAlgebraic,
1431        };
1432        let plan = LanczosPlan::new(lanczos_config.clone(), 200).expect("test: valid config");
1433        assert_eq!(plan.config().max_iterations, 40);
1434        assert_eq!(plan.config().num_eigenvalues, 10);
1435        assert!((plan.config().tolerance - 1e-8).abs() < 1e-15);
1436        assert_eq!(plan.config().which, EigenTarget::SmallestAlgebraic);
1437
1438        let arnoldi_config = ArnoldiConfig {
1439            max_iterations: 25,
1440            tolerance: 1e-12,
1441            num_eigenvalues: 6,
1442            which: EigenTarget::LargestAlgebraic,
1443        };
1444        let aplan = ArnoldiPlan::new(arnoldi_config, 300).expect("test: valid config");
1445        assert_eq!(aplan.config().max_iterations, 25);
1446        assert_eq!(aplan.config().num_eigenvalues, 6);
1447        assert_eq!(aplan.dimension(), 300);
1448    }
1449}