bem/core/solver/
ilu_preconditioner.rs

1//! Incomplete LU (ILU) Preconditioner
2//!
3//! Ported from NumCalc's NC_IncompleteLUDecomposition.
4//!
5//! This preconditioner is critical for BEM systems which are typically
6//! too ill-conditioned for simple Jacobi (diagonal) preconditioning.
7//!
8//! ## Algorithm
9//!
10//! 1. **Row scaling**: Scale matrix rows so that `sqrt(nnz_in_row / sum_of_squared_norms) = 1`
11//! 2. **Threshold dropping**: Keep entries with `|a_ij| > threshold` or diagonal entries
12//! 3. **Build sparse L and U**: L stored by rows (lower triangular), U by columns (upper triangular)
13//! 4. **Incomplete factorization**: Compute L and U with fill-in restricted to original sparsity
14//! 5. **Forward/backward substitution**: Apply (LU)⁻¹ = U⁻¹ L⁻¹
15//!
16//! ## Threshold Selection (from NumCalc)
17//!
18//! Different thresholds for different BEM methods:
19//! - TBEM: 0.6 - 1.2 (dense, higher threshold)
20//! - SLFMM: 0.01 - 0.9 (sparser, lower threshold)
21//! - MLFMM: 0.005 - 0.65 (sparsest, lowest threshold)
22
23use ndarray::{Array1, Array2};
24use num_complex::Complex64;
25
26use super::preconditioner::Preconditioner;
27
28/// ILU Preconditioner method type
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum IluMethod {
31    /// Traditional BEM (dense matrix)
32    Tbem,
33    /// Single-Level FMM (sparse near-field)
34    Slfmm,
35    /// Multi-Level FMM (very sparse near-field)
36    Mlfmm,
37}
38
39/// Scanning degree for ILU threshold selection
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum IluScanningDegree {
42    /// Coarsest (highest threshold, fastest)
43    Coarse = 0,
44    /// Medium
45    Medium = 1,
46    /// Fine
47    Fine = 2,
48    /// Finest (lowest threshold, most accurate)
49    Finest = 3,
50}
51
52/// Get threshold factor based on method and scanning degree
53fn get_threshold_factor(method: IluMethod, degree: IluScanningDegree) -> f64 {
54    match method {
55        IluMethod::Tbem => match degree {
56            IluScanningDegree::Coarse => 1.2,
57            IluScanningDegree::Medium => 1.0,
58            IluScanningDegree::Fine => 0.8,
59            IluScanningDegree::Finest => 0.6,
60        },
61        IluMethod::Slfmm => match degree {
62            IluScanningDegree::Coarse => 0.9,
63            IluScanningDegree::Medium => 0.35,
64            IluScanningDegree::Fine => 0.07,
65            IluScanningDegree::Finest => 0.01,
66        },
67        IluMethod::Mlfmm => match degree {
68            IluScanningDegree::Coarse => 0.65,
69            IluScanningDegree::Medium => 0.15,
70            IluScanningDegree::Fine => 0.05,
71            IluScanningDegree::Finest => 0.005,
72        },
73    }
74}
75
76/// Result of ILU setup - includes scaled system for use with CGS
77#[derive(Debug, Clone)]
78pub struct IluSetup {
79    /// The ILU preconditioner
80    pub preconditioner: IluPreconditioner,
81    /// Row-scaled matrix (A_scaled = D * A where D is row scaling)
82    pub scaled_matrix: Array2<Complex64>,
83    /// Row scaling factors (to scale RHS: b_scaled = D * b)
84    pub row_scale: Array1<Complex64>,
85}
86
87/// Incomplete LU (ILU) Preconditioner
88///
89/// Based on NumCalc's NC_IncompleteLUDecomposition.
90/// Uses threshold-based dropping with row scaling.
91#[derive(Debug, Clone)]
92pub struct IluPreconditioner {
93    /// L matrix values (lower triangular, stored by rows)
94    l_values: Vec<Complex64>,
95    /// Column indices for L entries
96    l_col_indices: Vec<usize>,
97    /// Row start indices for L (length n+1)
98    l_row_ptr: Vec<usize>,
99
100    /// U matrix values (upper triangular, stored by columns)
101    u_values: Vec<Complex64>,
102    /// Row indices for U entries
103    u_row_indices: Vec<usize>,
104    /// Column start indices for U (length n+1)
105    u_col_ptr: Vec<usize>,
106
107    /// Matrix dimension
108    n: usize,
109}
110
111impl IluPreconditioner {
112    /// Create ILU preconditioner from a dense matrix
113    ///
114    /// # Arguments
115    /// * `a` - The coefficient matrix (will be scaled internally)
116    /// * `method` - BEM method type (affects threshold selection)
117    /// * `degree` - Scanning degree (affects accuracy vs speed tradeoff)
118    pub fn from_matrix(
119        a: &Array2<Complex64>,
120        method: IluMethod,
121        degree: IluScanningDegree,
122    ) -> Self {
123        let n = a.nrows();
124        assert_eq!(n, a.ncols(), "Matrix must be square");
125
126        let threshold = get_threshold_factor(method, degree);
127
128        // Clone matrix for scaling
129        let mut scaled = a.clone();
130        let mut rhs_scale = vec![Complex64::new(1.0, 0.0); n];
131
132        // Step 1: Row scaling
133        // Scale each row so that sqrt(n_cols / sum_of_squared_norms) approaches 1
134        for i in 0..n {
135            let row_sum_sq: f64 = scaled.row(i).iter().map(|x| x.norm_sqr()).sum();
136            if row_sum_sq > 1e-30 {
137                let scale = (n as f64 / row_sum_sq).sqrt();
138                for j in 0..n {
139                    scaled[[i, j]] *= scale;
140                }
141                rhs_scale[i] = Complex64::new(scale, 0.0);
142            }
143        }
144
145        // Step 2: Identify entries to keep (above threshold or diagonal)
146        // Count "trues" (kept entries)
147        let mut keep = vec![vec![false; n]; n];
148        let mut n_trues = 0;
149
150        for i in 0..n {
151            for j in 0..n {
152                if scaled[[i, j]].norm() > threshold || i == j {
153                    keep[i][j] = true;
154                    n_trues += 1;
155                }
156            }
157        }
158
159        // Step 3: Build column index array (for each row, which columns are kept)
160        // jcol_tru[k] = column index of k-th kept entry
161        // nrow_tru[i] = start index in jcol_tru for row i
162        let mut jcol_tru = vec![0usize; n_trues];
163        let mut nrow_tru = vec![0usize; n + 1];
164
165        let mut kl = 0;
166        for i in 0..n {
167            nrow_tru[i] = kl;
168            for j in 0..n {
169                if keep[i][j] {
170                    jcol_tru[kl] = j;
171                    kl += 1;
172                }
173            }
174        }
175        nrow_tru[n] = kl;
176
177        // Step 4: Build row index array (for each column, which rows are kept)
178        // irow_tru[k] = row index of k-th kept entry (by column)
179        // ncol_tru[j] = start index in irow_tru for column j
180        let mut irow_tru = vec![0usize; n_trues];
181        let mut ncol_tru = vec![0usize; n + 1];
182
183        kl = 0;
184        for j in 0..n {
185            ncol_tru[j] = kl;
186            for i in 0..n {
187                // Check if (i, j) is in the kept set
188                for k in nrow_tru[i]..nrow_tru[i + 1] {
189                    if jcol_tru[k] == j {
190                        irow_tru[kl] = i;
191                        kl += 1;
192                        break;
193                    }
194                }
195            }
196        }
197        ncol_tru[n] = kl;
198
199        // Step 5: Count L and U entries
200        // L: lower triangular (including diagonal) - stored by rows
201        // U: upper triangular (excluding diagonal) - stored by columns
202        let mut nunz_l = 0;
203        let mut nunz_u = 0;
204
205        for i in 0..n {
206            // Entries in row i with column <= i go to L
207            for k in nrow_tru[i]..nrow_tru[i + 1] {
208                if jcol_tru[k] <= i {
209                    nunz_l += 1;
210                }
211            }
212            // Entries in column i with row < i go to U
213            for k in ncol_tru[i]..ncol_tru[i + 1] {
214                if irow_tru[k] < i {
215                    nunz_u += 1;
216                }
217            }
218        }
219
220        // Step 6: Allocate L and U structures
221        let mut l_col_indices = vec![0usize; nunz_l];
222        let mut l_row_ptr = vec![0usize; n + 1];
223        let mut u_row_indices = vec![0usize; nunz_u];
224        let mut u_col_ptr = vec![0usize; n + 1];
225        let mut l_values = vec![Complex64::new(0.0, 0.0); nunz_l];
226        let mut u_values = vec![Complex64::new(0.0, 0.0); nunz_u];
227
228        // Fill L and U index structures
229        kl = 0;
230        let mut ku = 0;
231        for i in 0..n {
232            l_row_ptr[i] = kl;
233            for k in nrow_tru[i]..nrow_tru[i + 1] {
234                let j = jcol_tru[k];
235                if j <= i {
236                    l_col_indices[kl] = j;
237                    l_values[kl] = scaled[[i, j]];
238                    kl += 1;
239                }
240            }
241
242            u_col_ptr[i] = ku;
243            for k in ncol_tru[i]..ncol_tru[i + 1] {
244                let row = irow_tru[k];
245                if row < i {
246                    u_row_indices[ku] = row;
247                    u_values[ku] = scaled[[row, i]];
248                    ku += 1;
249                }
250            }
251        }
252        l_row_ptr[n] = kl;
253        u_col_ptr[n] = ku;
254
255        // Step 7: Incomplete LU decomposition
256        // This is the core algorithm that computes L and U factors
257        Self::compute_ilu_factorization(
258            n,
259            &mut l_values,
260            &l_col_indices,
261            &l_row_ptr,
262            &mut u_values,
263            &u_row_indices,
264            &u_col_ptr,
265            &jcol_tru,
266            &nrow_tru,
267            &irow_tru,
268            &ncol_tru,
269        );
270
271        IluPreconditioner {
272            l_values,
273            l_col_indices,
274            l_row_ptr,
275            u_values,
276            u_row_indices,
277            u_col_ptr,
278            n,
279        }
280    }
281
282    /// Setup ILU preconditioner with scaled system
283    ///
284    /// This is the recommended method for BEM systems. It returns:
285    /// - The ILU preconditioner
286    /// - The row-scaled matrix (for use in matvec)
287    /// - The row scaling factors (to scale the RHS)
288    ///
289    /// # Usage
290    /// ```ignore
291    /// let setup = IluPreconditioner::setup_system(&matrix, IluMethod::Tbem, IluScanningDegree::Fine);
292    /// let scaled_rhs = &setup.row_scale * &rhs;  // Scale RHS
293    /// // Use setup.scaled_matrix for matvec in CGS
294    /// // Use setup.preconditioner for preconditioning
295    /// ```
296    pub fn setup_system(
297        a: &Array2<Complex64>,
298        method: IluMethod,
299        degree: IluScanningDegree,
300    ) -> IluSetup {
301        let threshold = get_threshold_factor(method, degree);
302        Self::setup_system_with_threshold(a, threshold)
303    }
304
305    /// Setup ILU preconditioner with custom threshold
306    ///
307    /// For dense TBEM matrices, use low threshold (e.g., 0.05-0.2).
308    /// For sparse FMM near-field, use higher threshold (e.g., 0.3-1.0).
309    pub fn setup_system_with_threshold(a: &Array2<Complex64>, threshold: f64) -> IluSetup {
310        let n = a.nrows();
311        assert_eq!(n, a.ncols(), "Matrix must be square");
312
313        // Clone matrix for scaling
314        let mut scaled = a.clone();
315        let mut row_scale = Array1::from_elem(n, Complex64::new(1.0, 0.0));
316
317        // Step 1: Row scaling
318        for i in 0..n {
319            let row_sum_sq: f64 = scaled.row(i).iter().map(|x| x.norm_sqr()).sum();
320            if row_sum_sq > 1e-30 {
321                let scale = (n as f64 / row_sum_sq).sqrt();
322                for j in 0..n {
323                    scaled[[i, j]] *= scale;
324                }
325                row_scale[i] = Complex64::new(scale, 0.0);
326            }
327        }
328
329        // Step 2-7: Build ILU (same as from_matrix but using pre-scaled matrix)
330        let mut keep = vec![vec![false; n]; n];
331        let mut n_trues = 0;
332
333        for i in 0..n {
334            for j in 0..n {
335                if scaled[[i, j]].norm() > threshold || i == j {
336                    keep[i][j] = true;
337                    n_trues += 1;
338                }
339            }
340        }
341
342        let mut jcol_tru = vec![0usize; n_trues];
343        let mut nrow_tru = vec![0usize; n + 1];
344
345        let mut kl = 0;
346        for i in 0..n {
347            nrow_tru[i] = kl;
348            for j in 0..n {
349                if keep[i][j] {
350                    jcol_tru[kl] = j;
351                    kl += 1;
352                }
353            }
354        }
355        nrow_tru[n] = kl;
356
357        let mut irow_tru = vec![0usize; n_trues];
358        let mut ncol_tru = vec![0usize; n + 1];
359
360        kl = 0;
361        for j in 0..n {
362            ncol_tru[j] = kl;
363            for i in 0..n {
364                for k in nrow_tru[i]..nrow_tru[i + 1] {
365                    if jcol_tru[k] == j {
366                        irow_tru[kl] = i;
367                        kl += 1;
368                        break;
369                    }
370                }
371            }
372        }
373        ncol_tru[n] = kl;
374
375        let mut nunz_l = 0;
376        let mut nunz_u = 0;
377
378        for i in 0..n {
379            for k in nrow_tru[i]..nrow_tru[i + 1] {
380                if jcol_tru[k] <= i {
381                    nunz_l += 1;
382                }
383            }
384            for k in ncol_tru[i]..ncol_tru[i + 1] {
385                if irow_tru[k] < i {
386                    nunz_u += 1;
387                }
388            }
389        }
390
391        let mut l_col_indices = vec![0usize; nunz_l];
392        let mut l_row_ptr = vec![0usize; n + 1];
393        let mut u_row_indices = vec![0usize; nunz_u];
394        let mut u_col_ptr = vec![0usize; n + 1];
395        let mut l_values = vec![Complex64::new(0.0, 0.0); nunz_l];
396        let mut u_values = vec![Complex64::new(0.0, 0.0); nunz_u];
397
398        kl = 0;
399        let mut ku = 0;
400        for i in 0..n {
401            l_row_ptr[i] = kl;
402            for k in nrow_tru[i]..nrow_tru[i + 1] {
403                let j = jcol_tru[k];
404                if j <= i {
405                    l_col_indices[kl] = j;
406                    l_values[kl] = scaled[[i, j]];
407                    kl += 1;
408                }
409            }
410
411            u_col_ptr[i] = ku;
412            for k in ncol_tru[i]..ncol_tru[i + 1] {
413                let row = irow_tru[k];
414                if row < i {
415                    u_row_indices[ku] = row;
416                    u_values[ku] = scaled[[row, i]];
417                    ku += 1;
418                }
419            }
420        }
421        l_row_ptr[n] = kl;
422        u_col_ptr[n] = ku;
423
424        Self::compute_ilu_factorization(
425            n,
426            &mut l_values,
427            &l_col_indices,
428            &l_row_ptr,
429            &mut u_values,
430            &u_row_indices,
431            &u_col_ptr,
432            &jcol_tru,
433            &nrow_tru,
434            &irow_tru,
435            &ncol_tru,
436        );
437
438        let preconditioner = IluPreconditioner {
439            l_values,
440            l_col_indices,
441            l_row_ptr,
442            u_values,
443            u_row_indices,
444            u_col_ptr,
445            n,
446        };
447
448        IluSetup {
449            preconditioner,
450            scaled_matrix: scaled,
451            row_scale,
452        }
453    }
454
455    /// Create ILU preconditioner with custom threshold
456    pub fn from_matrix_with_threshold(a: &Array2<Complex64>, threshold: f64) -> Self {
457        let n = a.nrows();
458        assert_eq!(n, a.ncols(), "Matrix must be square");
459
460        // Clone matrix for scaling
461        let mut scaled = a.clone();
462
463        // Step 1: Row scaling
464        for i in 0..n {
465            let row_sum_sq: f64 = scaled.row(i).iter().map(|x| x.norm_sqr()).sum();
466            if row_sum_sq > 1e-30 {
467                let scale = (n as f64 / row_sum_sq).sqrt();
468                for j in 0..n {
469                    scaled[[i, j]] *= scale;
470                }
471            }
472        }
473
474        // Step 2: Identify entries to keep
475        let mut keep = vec![vec![false; n]; n];
476        let mut n_trues = 0;
477
478        for i in 0..n {
479            for j in 0..n {
480                if scaled[[i, j]].norm() > threshold || i == j {
481                    keep[i][j] = true;
482                    n_trues += 1;
483                }
484            }
485        }
486
487        // Step 3-7: Same as from_matrix
488        let mut jcol_tru = vec![0usize; n_trues];
489        let mut nrow_tru = vec![0usize; n + 1];
490
491        let mut kl = 0;
492        for i in 0..n {
493            nrow_tru[i] = kl;
494            for j in 0..n {
495                if keep[i][j] {
496                    jcol_tru[kl] = j;
497                    kl += 1;
498                }
499            }
500        }
501        nrow_tru[n] = kl;
502
503        let mut irow_tru = vec![0usize; n_trues];
504        let mut ncol_tru = vec![0usize; n + 1];
505
506        kl = 0;
507        for j in 0..n {
508            ncol_tru[j] = kl;
509            for i in 0..n {
510                for k in nrow_tru[i]..nrow_tru[i + 1] {
511                    if jcol_tru[k] == j {
512                        irow_tru[kl] = i;
513                        kl += 1;
514                        break;
515                    }
516                }
517            }
518        }
519        ncol_tru[n] = kl;
520
521        let mut nunz_l = 0;
522        let mut nunz_u = 0;
523
524        for i in 0..n {
525            for k in nrow_tru[i]..nrow_tru[i + 1] {
526                if jcol_tru[k] <= i {
527                    nunz_l += 1;
528                }
529            }
530            for k in ncol_tru[i]..ncol_tru[i + 1] {
531                if irow_tru[k] < i {
532                    nunz_u += 1;
533                }
534            }
535        }
536
537        let mut l_col_indices = vec![0usize; nunz_l];
538        let mut l_row_ptr = vec![0usize; n + 1];
539        let mut u_row_indices = vec![0usize; nunz_u];
540        let mut u_col_ptr = vec![0usize; n + 1];
541        let mut l_values = vec![Complex64::new(0.0, 0.0); nunz_l];
542        let mut u_values = vec![Complex64::new(0.0, 0.0); nunz_u];
543
544        kl = 0;
545        let mut ku = 0;
546        for i in 0..n {
547            l_row_ptr[i] = kl;
548            for k in nrow_tru[i]..nrow_tru[i + 1] {
549                let j = jcol_tru[k];
550                if j <= i {
551                    l_col_indices[kl] = j;
552                    l_values[kl] = scaled[[i, j]];
553                    kl += 1;
554                }
555            }
556
557            u_col_ptr[i] = ku;
558            for k in ncol_tru[i]..ncol_tru[i + 1] {
559                let row = irow_tru[k];
560                if row < i {
561                    u_row_indices[ku] = row;
562                    u_values[ku] = scaled[[row, i]];
563                    ku += 1;
564                }
565            }
566        }
567        l_row_ptr[n] = kl;
568        u_col_ptr[n] = ku;
569
570        Self::compute_ilu_factorization(
571            n,
572            &mut l_values,
573            &l_col_indices,
574            &l_row_ptr,
575            &mut u_values,
576            &u_row_indices,
577            &u_col_ptr,
578            &jcol_tru,
579            &nrow_tru,
580            &irow_tru,
581            &ncol_tru,
582        );
583
584        IluPreconditioner {
585            l_values,
586            l_col_indices,
587            l_row_ptr,
588            u_values,
589            u_row_indices,
590            u_col_ptr,
591            n,
592        }
593    }
594
595    /// Compute the incomplete LU factorization
596    ///
597    /// This is the core algorithm from NumCalc.
598    /// Modifies l_values and u_values in place.
599    #[allow(clippy::too_many_arguments)]
600    fn compute_ilu_factorization(
601        n: usize,
602        l_values: &mut [Complex64],
603        l_col_indices: &[usize],
604        l_row_ptr: &[usize],
605        u_values: &mut [Complex64],
606        u_row_indices: &[usize],
607        u_col_ptr: &[usize],
608        jcol_tru: &[usize],
609        nrow_tru: &[usize],
610        irow_tru: &[usize],
611        ncol_tru: &[usize],
612    ) {
613        // Boolean vectors for marking row/column membership
614        let mut mi_row = vec![false; n];
615        let mut mi_col = vec![false; n];
616        let mut mk_vct = vec![false; n];
617
618        for i in 0..n {
619            // Reset all markers (critical: must reset ALL, not just 0..=i)
620            for j in 0..n {
621                mi_row[j] = false;
622                mi_col[j] = false;
623            }
624            // Mark kept entries in row i (from original sparsity)
625            for k in nrow_tru[i]..nrow_tru[i + 1] {
626                mi_row[jcol_tru[k]] = true;
627            }
628            // Mark kept entries in column i
629            for k in ncol_tru[i]..ncol_tru[i + 1] {
630                mi_col[irow_tru[k]] = true;
631            }
632
633            // Update L column i (rows >= i)
634            for k in i..n {
635                if !mi_col[k] {
636                    continue;
637                }
638
639                // Find position of L[k, i]
640                let mut j1 = 0;
641                for j in l_row_ptr[k]..l_row_ptr[k + 1] {
642                    if l_col_indices[j] == i {
643                        j1 = j;
644                        break;
645                    }
646                }
647
648                // Mark kept entries in row k
649                for j in 0..n {
650                    mk_vct[j] = false;
651                }
652                for j in nrow_tru[k]..nrow_tru[k + 1] {
653                    mk_vct[jcol_tru[j]] = true;
654                }
655
656                // L[k,i] -= sum_{m<i} L[k,m] * U[m,i]
657                let mut ml = 0;
658                let mut mu = 0;
659                for m in 0..i {
660                    if mk_vct[m] && mi_col[m] {
661                        l_values[j1] -=
662                            l_values[l_row_ptr[k] + ml] * u_values[u_col_ptr[i] + mu];
663                    }
664                    if mk_vct[m] {
665                        ml += 1;
666                    }
667                    if mi_col[m] {
668                        mu += 1;
669                    }
670                }
671            }
672
673            // Update U row i (columns > i)
674            for k in (i + 1)..n {
675                if !mi_row[k] {
676                    continue;
677                }
678
679                // Find position of U[i, k]
680                let mut j1 = 0;
681                for j in u_col_ptr[k]..u_col_ptr[k + 1] {
682                    if u_row_indices[j] == i {
683                        j1 = j;
684                        break;
685                    }
686                }
687
688                // Mark kept entries in column k
689                for j in 0..n {
690                    mk_vct[j] = false;
691                }
692                for j in ncol_tru[k]..ncol_tru[k + 1] {
693                    mk_vct[irow_tru[j]] = true;
694                }
695
696                // U[i,k] -= sum_{m<i} L[i,m] * U[m,k]
697                let mut ml = 0;
698                let mut mu = 0;
699                for m in 0..i {
700                    if mi_row[m] && mk_vct[m] {
701                        u_values[j1] -=
702                            l_values[l_row_ptr[i] + ml] * u_values[u_col_ptr[k] + mu];
703                    }
704                    if mi_row[m] {
705                        ml += 1;
706                    }
707                    if mk_vct[m] {
708                        mu += 1;
709                    }
710                }
711
712                // U[i,k] /= L[i,i]
713                let l_diag_idx = l_row_ptr[i + 1] - 1;
714                if l_values[l_diag_idx].norm() > 1e-30 {
715                    u_values[j1] /= l_values[l_diag_idx];
716                }
717            }
718        }
719    }
720
721    /// Forward-backward substitution: solve (LU)z = r
722    ///
723    /// 1. Forward: L * y = r
724    /// 2. Backward: U * z = y
725    fn forward_backward(&self, r: &Array1<Complex64>) -> Array1<Complex64> {
726        let mut z = r.clone();
727
728        // Forward elimination: L * y = r
729        // L is lower triangular stored by rows
730        if self.l_values.is_empty() {
731            return z;
732        }
733
734        // z[0] /= L[0,0]
735        let l_diag_0 = self.l_values[0];
736        if l_diag_0.norm() > 1e-30 {
737            z[0] /= l_diag_0;
738        }
739
740        for i in 1..self.n {
741            // z[i] -= sum_{j<i} L[i,j] * z[j]
742            // The last entry in row i is the diagonal
743            let row_end = self.l_row_ptr[i + 1];
744            let diag_idx = row_end - 1;
745
746            for k in self.l_row_ptr[i]..diag_idx {
747                let j = self.l_col_indices[k];
748                let z_j = z[j];
749                z[i] -= self.l_values[k] * z_j;
750            }
751
752            // z[i] /= L[i,i]
753            if self.l_values[diag_idx].norm() > 1e-30 {
754                z[i] /= self.l_values[diag_idx];
755            }
756        }
757
758        // Backward substitution: U * result = z
759        // U is upper triangular stored by columns
760        // Process columns from right to left
761        for i in (1..self.n).rev() {
762            // For each entry U[row, i] where row < i:
763            // z[row] -= U[row, i] * z[i]
764            let z_i = z[i];
765            for k in self.u_col_ptr[i]..self.u_col_ptr[i + 1] {
766                let row = self.u_row_indices[k];
767                z[row] -= self.u_values[k] * z_i;
768            }
769        }
770
771        z
772    }
773
774    /// Get number of nonzeros in L
775    pub fn nnz_l(&self) -> usize {
776        self.l_values.len()
777    }
778
779    /// Get number of nonzeros in U
780    pub fn nnz_u(&self) -> usize {
781        self.u_values.len()
782    }
783
784    /// Get fill ratio (nnz(L+U) / n^2)
785    pub fn fill_ratio(&self) -> f64 {
786        (self.l_values.len() + self.u_values.len()) as f64 / (self.n * self.n) as f64
787    }
788}
789
790impl Preconditioner for IluPreconditioner {
791    fn apply(&self, r: &Array1<Complex64>) -> Array1<Complex64> {
792        self.forward_backward(r)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[test]
801    fn test_ilu_simple() {
802        // Simple 3x3 diagonally dominant matrix
803        let a = Array2::from_shape_vec(
804            (3, 3),
805            vec![
806                Complex64::new(4.0, 0.0),
807                Complex64::new(1.0, 0.0),
808                Complex64::new(0.0, 0.0),
809                Complex64::new(1.0, 0.0),
810                Complex64::new(5.0, 0.0),
811                Complex64::new(2.0, 0.0),
812                Complex64::new(0.0, 0.0),
813                Complex64::new(2.0, 0.0),
814                Complex64::new(6.0, 0.0),
815            ],
816        )
817        .unwrap();
818
819        let precond = IluPreconditioner::from_matrix(&a, IluMethod::Tbem, IluScanningDegree::Coarse);
820
821        // Check that ILU was created
822        assert!(precond.nnz_l() > 0);
823
824        // Apply to a test vector
825        let r = Array1::from_vec(vec![
826            Complex64::new(1.0, 0.0),
827            Complex64::new(1.0, 0.0),
828            Complex64::new(1.0, 0.0),
829        ]);
830        let z = precond.apply(&r);
831
832        // Just check that it produces a result
833        assert_eq!(z.len(), 3);
834        assert!(z.iter().all(|x| x.is_finite()));
835    }
836
837    #[test]
838    fn test_ilu_complex() {
839        // Test with complex entries (like BEM matrices)
840        let a = Array2::from_shape_vec(
841            (3, 3),
842            vec![
843                Complex64::new(4.0, 1.0),
844                Complex64::new(1.0, -0.5),
845                Complex64::new(0.5, 0.0),
846                Complex64::new(1.0, 0.5),
847                Complex64::new(5.0, -1.0),
848                Complex64::new(2.0, 0.3),
849                Complex64::new(0.5, 0.0),
850                Complex64::new(2.0, -0.3),
851                Complex64::new(6.0, 2.0),
852            ],
853        )
854        .unwrap();
855
856        let precond = IluPreconditioner::from_matrix(&a, IluMethod::Tbem, IluScanningDegree::Fine);
857
858        let r = Array1::from_vec(vec![
859            Complex64::new(1.0, 0.5),
860            Complex64::new(2.0, -0.5),
861            Complex64::new(0.5, 1.0),
862        ]);
863        let z = precond.apply(&r);
864
865        assert_eq!(z.len(), 3);
866        assert!(z.iter().all(|x| x.is_finite()));
867    }
868
869    #[test]
870    fn test_ilu_fill_ratio() {
871        // For a dense matrix with low threshold, fill ratio should be high
872        let n = 10;
873        let mut a = Array2::zeros((n, n));
874        for i in 0..n {
875            for j in 0..n {
876                a[[i, j]] = Complex64::new((i + j) as f64 + 1.0, 0.0);
877            }
878            // Strong diagonal
879            a[[i, i]] = Complex64::new((n * 2) as f64, 0.0);
880        }
881
882        let precond_coarse =
883            IluPreconditioner::from_matrix(&a, IluMethod::Tbem, IluScanningDegree::Coarse);
884        let precond_finest =
885            IluPreconditioner::from_matrix(&a, IluMethod::Tbem, IluScanningDegree::Finest);
886
887        // Finest should have higher fill ratio (lower threshold)
888        assert!(precond_finest.fill_ratio() >= precond_coarse.fill_ratio());
889    }
890}