scirs2_integrate/pde/spectral/
mod.rs

1//! Spectral Methods for solving PDEs
2//!
3//! This module provides implementations of spectral methods for solving PDEs,
4//! which approximate solutions using global basis functions like Fourier series,
5//! Chebyshev polynomials, or Legendre polynomials.
6//!
7//! Key features:
8//! - Fourier spectral methods for periodic problems
9//! - Chebyshev spectral methods for non-periodic problems
10//! - Legendre spectral methods for non-periodic problems
11//! - Spectral element methods for complex geometries
12//! - Fast transforms using FFT
13//! - High accuracy for smooth solutions
14
15use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
16use std::f64::consts::PI;
17use std::time::Instant;
18
19// FFT functions would be implemented here or imported from another crate
20// For now, let's create stubs for these functions
21// use crate::fft::{fft, ifft, irfft, rfft};
22use crate::gaussian::GaussLegendreQuadrature;
23use crate::pde::{
24    BoundaryCondition, BoundaryConditionType, BoundaryLocation, Domain, PDEError, PDEResult,
25    PDESolution, PDESolverInfo,
26};
27
28/// Type of spectral basis functions
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub enum SpectralBasis {
31    /// Fourier basis (exp(ikx)) for periodic problems
32    Fourier,
33
34    /// Chebyshev polynomials (T_n(x)) for non-periodic problems
35    Chebyshev,
36
37    /// Legendre polynomials (P_n(x)) for non-periodic problems
38    Legendre,
39}
40
41/// Options for spectral solvers
42#[derive(Debug, Clone)]
43pub struct SpectralOptions {
44    /// Type of spectral basis to use
45    pub basis: SpectralBasis,
46
47    /// Number of modes/coefficients to use
48    pub num_modes: usize,
49
50    /// Maximum iterations for iterative solvers
51    pub max_iterations: usize,
52
53    /// Tolerance for convergence
54    pub tolerance: f64,
55
56    /// Whether to save convergence history
57    pub save_convergence_history: bool,
58
59    /// Whether to use real FFT (more efficient for real-valued functions)
60    pub use_real_transform: bool,
61
62    /// Whether to use dealiasing (e.g., 2/3 rule for Fourier)
63    pub use_dealiasing: bool,
64
65    /// Print detailed progress information
66    pub verbose: bool,
67}
68
69impl Default for SpectralOptions {
70    fn default() -> Self {
71        SpectralOptions {
72            basis: SpectralBasis::Fourier,
73            num_modes: 64,
74            max_iterations: 1000,
75            tolerance: 1e-10,
76            save_convergence_history: false,
77            use_real_transform: true,
78            use_dealiasing: true,
79            verbose: false,
80        }
81    }
82}
83
84/// Result of spectral method solutions
85#[derive(Debug, Clone)]
86pub struct SpectralResult {
87    /// Solution values on the grid
88    pub u: Array1<f64>,
89
90    /// Spectral coefficients
91    pub coefficients: Array1<f64>,
92
93    /// Grid points
94    pub grid: Array1<f64>,
95
96    /// Residual norm
97    pub residual_norm: f64,
98
99    /// Number of iterations performed
100    pub num_iterations: usize,
101
102    /// Computation time
103    pub computation_time: f64,
104
105    /// Convergence history
106    pub convergence_history: Option<Vec<f64>>,
107}
108
109/// Create a Chebyshev differentiation matrix of size n×n
110///
111/// This matrix D satisfies Du = u' where u is a vector of function values
112/// at Chebyshev points x_j = cos(jπ/n), j=0...n
113#[allow(dead_code)]
114pub fn chebyshev_diff_matrix(n: usize) -> Array2<f64> {
115    let mut d = Array2::zeros((n, n));
116
117    // Compute Chebyshev points
118    let mut x = Array1::zeros(n);
119    for j in 0..n {
120        x[j] = (j as f64 * PI / (n - 1) as f64).cos();
121    }
122
123    // Compute the matrix entries
124    for i in 0..n {
125        for j in 0..n {
126            if i != j {
127                let c_i = if i == 0 || i == n - 1 { 2.0 } else { 1.0 };
128                let c_j = if j == 0 || j == n - 1 { 2.0 } else { 1.0 };
129
130                d[[i, j]] = c_i / c_j * (-1.0_f64).powf((i + j) as f64) / (x[i] - x[j]);
131            }
132        }
133    }
134
135    // Compute diagonal entries by enforcing row sums to be zero
136    for i in 0..n {
137        let mut row_sum = 0.0;
138        for j in 0..n {
139            if i != j {
140                row_sum += d[[i, j]];
141            }
142        }
143        d[[i, i]] = -row_sum;
144    }
145
146    d
147}
148
149/// Create a second-derivative Chebyshev differentiation matrix
150#[allow(dead_code)]
151pub fn chebyshev_diff2_matrix(n: usize) -> Array2<f64> {
152    let d1 = chebyshev_diff_matrix(n);
153    d1.dot(&d1) // D^2 = D * D
154}
155
156/// Generate Chebyshev grid points x_j = cos(jπ/n), j=0...n-1
157#[allow(dead_code)]
158pub fn chebyshev_points(n: usize) -> Array1<f64> {
159    let mut x = Array1::zeros(n);
160    for j in 0..n {
161        x[j] = (j as f64 * PI / (n - 1) as f64).cos();
162    }
163    x
164}
165
166/// Transform from physical space to Chebyshev coefficient space
167#[allow(dead_code)]
168pub fn chebyshev_transform(u: &ArrayView1<f64>) -> Array1<f64> {
169    let n = u.len();
170    let mut coeffs = Array1::zeros(n);
171
172    // Simple direct transform
173    for k in 0..n {
174        let mut sum = 0.0;
175        for j in 0..n {
176            let _x_j = (j as f64 * PI / (n - 1) as f64).cos();
177            sum += u[j] * (k as f64 * j as f64 * PI / (n - 1) as f64).cos();
178        }
179
180        let norm = if k == 0 || k == n - 1 {
181            n - 1
182        } else {
183            2 * (n - 1)
184        };
185        coeffs[k] = 2.0 * sum / norm as f64;
186    }
187
188    coeffs
189}
190
191/// Transform from Chebyshev coefficient space to physical space
192#[allow(dead_code)]
193pub fn chebyshev_inverse_transform(coeffs: &ArrayView1<f64>) -> Array1<f64> {
194    let n = coeffs.len();
195    let mut u = Array1::zeros(n);
196
197    // Generate Chebyshev points
198    let _x = chebyshev_points(n);
199
200    // Evaluate the Chebyshev series at each point
201    for j in 0..n {
202        let mut sum = 0.0;
203        for k in 0..n {
204            sum += coeffs[k] * (k as f64 * j as f64 * PI / (n - 1) as f64).cos();
205        }
206        u[j] = sum;
207    }
208
209    u
210}
211
212/// Generate Legendre-Gauss-Lobatto quadrature points and weights
213///
214/// These are the zeros of (1-x²)P'ₙ₋₁(x) in [-1, 1] where Pₙ is the
215/// Legendre polynomial of degree n. The points include the endpoints ±1.
216#[allow(dead_code)]
217pub fn legendre_points(n: usize) -> (Array1<f64>, Array1<f64>) {
218    if n <= 1 {
219        return (Array1::zeros(1), Array1::ones(1));
220    }
221
222    // For small n, we can use precomputed Gauss-Legendre quadrature
223    // and add the endpoints
224    if n <= 12 {
225        // Get the inner Gauss-Legendre points (n-2 points)
226        let quadrature = GaussLegendreQuadrature::<f64>::new(n - 2).unwrap();
227
228        // Create arrays for Gauss-Lobatto points (including endpoints)
229        let mut points = Array1::zeros(n);
230        let mut weights = Array1::zeros(n);
231
232        // Add endpoints
233        points[0] = -1.0;
234        points[n - 1] = 1.0;
235
236        // Add interior points (reversed because GaussLegendreQuadrature gives ascending points)
237        for i in 0..n - 2 {
238            points[i + 1] = quadrature.nodes[n - 3 - i]; // Reverse order
239        }
240
241        // Compute weights: the formula for Legendre-Gauss-Lobatto weights is
242        // wⱼ = 2/[n(n-1)[Pₙ₋₁(xⱼ)]²] for all j
243        let factor = 2.0 / (n as f64 * (n - 1) as f64);
244
245        // Endpoints have the same weight
246        weights[0] = factor;
247        weights[n - 1] = factor;
248
249        // Interior weights
250        for i in 1..n - 1 {
251            let x = points[i];
252            let p = legendre_polynomial(n - 1, x);
253            weights[i] = factor / (p * p);
254        }
255
256        return (points, weights);
257    }
258
259    // For larger n, we need to compute the points by finding the zeros of the derivative
260    // of the Legendre polynomial, and then compute the weights separately
261
262    // Initial guess: Chebyshev points (good approximation for Legendre)
263    let mut points = Array1::zeros(n);
264    for i in 0..n {
265        points[i] = -(i as f64 * PI / (n - 1) as f64).cos();
266    }
267
268    // Refine using Newton-Raphson iteration
269    for i in 1..n - 1 {
270        // Skip endpoints that are always ±1
271        let mut x = points[i];
272        let mut delta;
273
274        // Newton-Raphson to find zeros of (1-x²)P'ₙ₋₁(x)
275        // We compute P'ₙ₋₁(x) and P''ₙ₋₁(x) using recurrence relations
276        for _ in 0..10 {
277            // Usually converges in a few iterations
278            let _p = legendre_polynomial(n - 1, x);
279            let dp = legendre_polynomial_derivative(n - 1, x);
280            // d/dx[(1-x²)P'ₙ₋₁(x)] = -2x·P'ₙ₋₁(x) + (1-x²)P''ₙ₋₁(x)
281            // We approximate P''ₙ₋₁(x) using a central difference on P'ₙ₋₁
282            let h = 1e-6;
283            let dp_plus = legendre_polynomial_derivative(n - 1, x + h);
284            let dp_minus = legendre_polynomial_derivative(n - 1, x - h);
285            let ddp = (dp_plus - dp_minus) / (2.0 * h);
286
287            // Function value: (1-x²)P'ₙ₋₁(x)
288            let f = (1.0 - x * x) * dp;
289            // Derivative: -2x·P'ₙ₋₁(x) + (1-x²)P''ₙ₋₁(x)
290            let df = -2.0 * x * dp + (1.0 - x * x) * ddp;
291
292            // Newton update
293            delta = f / df;
294            x -= delta;
295
296            if delta.abs() < 1e-12 {
297                break;
298            }
299        }
300
301        points[i] = x;
302    }
303
304    // Compute weights using the formula wⱼ = 2/[n(n-1)[Pₙ₋₁(xⱼ)]²]
305    let mut weights = Array1::zeros(n);
306    let factor = 2.0 / (n as f64 * (n - 1) as f64);
307
308    for i in 0..n {
309        let x = points[i];
310        let p = legendre_polynomial(n - 1, x);
311        weights[i] = factor / (p * p);
312    }
313
314    (points, weights)
315}
316
317/// Evaluate the Legendre polynomial of degree n at point x
318#[allow(dead_code)]
319fn legendre_polynomial(n: usize, x: f64) -> f64 {
320    if n == 0 {
321        return 1.0;
322    }
323    if n == 1 {
324        return x;
325    }
326
327    // Use recurrence relation for Legendre polynomials:
328    // (n+1)Pₙ₊₁(x) = (2n+1)xPₙ(x) - nPₙ₋₁(x)
329    let mut p_prev = 1.0; // P₀(x)
330    let mut p = x; // P₁(x)
331
332    for k in 2..=n {
333        let k_f64 = k as f64;
334        let p_next = ((2.0 * k_f64 - 1.0) * x * p - (k_f64 - 1.0) * p_prev) / k_f64;
335        p_prev = p;
336        p = p_next;
337    }
338
339    p
340}
341
342/// Evaluate the derivative of the Legendre polynomial of degree n at point x
343#[allow(dead_code)]
344fn legendre_polynomial_derivative(n: usize, x: f64) -> f64 {
345    if n == 0 {
346        return 0.0;
347    }
348    if n == 1 {
349        return 1.0;
350    }
351
352    // Use relation: (1-x²)P'ₙ(x) = n(Pₙ₋₁(x) - xPₙ(x))
353    // => P'ₙ(x) = n(Pₙ₋₁(x) - xPₙ(x))/(1-x²)
354
355    let pn = legendre_polynomial(n, x);
356    let pn_minus_1 = legendre_polynomial(n - 1, x);
357
358    // Handle numerical issues at endpoints where the denominator is zero
359    if (1.0 - x * x).abs() < 1e-10 {
360        if x > 0.0 {
361            // At x = 1, use P'ₙ(1) = n(n+1)/2
362            return n as f64 * (n + 1) as f64 / 2.0;
363        } else {
364            // At x = -1, use P'ₙ(-1) = (-1)^(n+1) * n(n+1)/2
365            return if n.is_multiple_of(2) { -1.0 } else { 1.0 } * n as f64 * (n + 1) as f64 / 2.0;
366        }
367    }
368
369    n as f64 * (pn_minus_1 - x * pn) / (1.0 - x * x)
370}
371
372/// Create a Legendre differentiation matrix of size n×n
373///
374/// This matrix D satisfies Du = u' where u is a vector of function values
375/// at Legendre-Gauss-Lobatto points
376#[allow(dead_code)]
377pub fn legendre_diff_matrix(n: usize) -> Array2<f64> {
378    let mut d = Array2::zeros((n, n));
379
380    // Compute Legendre-Gauss-Lobatto points and weights
381    let (x_, weights) = legendre_points(n);
382
383    // Compute the differentiation matrix entries
384    for i in 0..n {
385        for j in 0..n {
386            if i != j {
387                let p_i = legendre_polynomial(n - 1, x_[i]);
388                let p_j = legendre_polynomial(n - 1, x_[j]);
389
390                d[[i, j]] = p_i / (p_j * (x_[i] - x_[j]));
391
392                if j == 0 || j == n - 1 {
393                    d[[i, j]] *= 2.0;
394                }
395            }
396        }
397    }
398
399    // Compute diagonal entries by enforcing row sum = 0
400    for i in 0..n {
401        d[[i, i]] = 0.0;
402        for j in 0..n {
403            if i != j {
404                d[[i, i]] -= d[[i, j]];
405            }
406        }
407    }
408
409    d
410}
411
412/// Create a second-derivative Legendre differentiation matrix
413#[allow(dead_code)]
414pub fn legendre_diff2_matrix(n: usize) -> Array2<f64> {
415    let d1 = legendre_diff_matrix(n);
416    d1.dot(&d1) // D^2 = D * D
417}
418
419/// Transform from physical space to Legendre coefficient space
420#[allow(dead_code)]
421pub fn legendre_transform(u: &ArrayView1<f64>) -> Array1<f64> {
422    let n = u.len();
423    let mut coeffs = Array1::zeros(n);
424
425    // Get Legendre-Gauss-Lobatto points and weights
426    let (x, w) = legendre_points(n);
427
428    // Compute coefficients using quadrature
429    for k in 0..n {
430        let mut sum = 0.0;
431        for j in 0..n {
432            let p_k = legendre_polynomial(k, x[j]);
433            sum += u[j] * p_k * w[j];
434        }
435
436        // Normalization: (k+1/2) for the orthogonality relation of Legendre polynomials
437        let norm = k as f64 + 0.5;
438        coeffs[k] = sum * norm;
439    }
440
441    coeffs
442}
443
444/// Transform from Legendre coefficient space to physical space
445#[allow(dead_code)]
446pub fn legendre_inverse_transform(
447    coeffs: &ArrayView1<f64>,
448    x_points: Option<&ArrayView1<f64>>,
449) -> Array1<f64> {
450    let n = coeffs.len();
451    let mut u = Array1::zeros(n);
452
453    // Use provided points or generate Legendre-Gauss-Lobatto points
454    let x = if let Some(points) = x_points {
455        points.to_owned()
456    } else {
457        legendre_points(n).0
458    };
459
460    // Evaluate the Legendre series at each point
461    for j in 0..n {
462        let mut sum = 0.0;
463        for k in 0..n {
464            // Normalization factor
465            let norm = 1.0 / (k as f64 + 0.5);
466            sum += coeffs[k] * norm * legendre_polynomial(k, x[j]);
467        }
468        u[j] = sum;
469    }
470
471    u
472}
473
474/// Fourier spectral solver for 1D periodic PDEs
475pub struct FourierSpectralSolver1D {
476    /// Domain for the problem
477    domain: Domain,
478
479    /// Source term function f(x)
480    source_term: Box<dyn Fn(f64) -> f64 + Send + Sync>,
481
482    /// Boundary conditions (must be periodic for Fourier methods)
483    #[allow(dead_code)]
484    boundary_conditions: Vec<BoundaryCondition<f64>>,
485
486    /// Solver options
487    options: SpectralOptions,
488}
489
490impl FourierSpectralSolver1D {
491    /// Create a new Fourier spectral solver for 1D periodic PDEs
492    pub fn new(
493        domain: Domain,
494        source_term: impl Fn(f64) -> f64 + Send + Sync + 'static,
495        boundary_conditions: Vec<BoundaryCondition<f64>>,
496        options: Option<SpectralOptions>,
497    ) -> PDEResult<Self> {
498        // Validate domain
499        if domain.dimensions() != 1 {
500            return Err(PDEError::DomainError(
501                "Domain must be 1-dimensional for 1D Fourier spectral solver".to_string(),
502            ));
503        }
504
505        // Check that the domain is suitable for Fourier methods (periodic)
506        let mut has_periodic = false;
507        for bc in &boundary_conditions {
508            if bc.bc_type == BoundaryConditionType::Periodic {
509                has_periodic = true;
510                break;
511            }
512        }
513
514        if !has_periodic {
515            return Err(PDEError::BoundaryConditions(
516                "Fourier spectral methods require periodic boundary _conditions".to_string(),
517            ));
518        }
519
520        let mut options = options.unwrap_or_default();
521        options.basis = SpectralBasis::Fourier; // Ensure Fourier basis
522
523        Ok(FourierSpectralSolver1D {
524            domain,
525            source_term: Box::new(source_term),
526            boundary_conditions,
527            options,
528        })
529    }
530
531    /// Solve a 1D periodic PDE using Fourier spectral method
532    ///
533    /// Solves the equation: d²u/dx² = f(x) with periodic boundary conditions
534    pub fn solve(&self) -> PDEResult<SpectralResult> {
535        let start_time = Instant::now();
536
537        // Extract domain information
538        let range = &self.domain.ranges[0];
539        let length = range.end - range.start;
540        let n = self.options.num_modes;
541
542        // Create uniform grid
543        let mut grid = Array1::zeros(n);
544        for i in 0..n {
545            grid[i] = range.start + i as f64 * length / n as f64;
546        }
547
548        // Compute source term on the grid
549        let mut f_values = Array1::zeros(n);
550        for (i, &x) in grid.iter().enumerate() {
551            f_values[i] = (self.source_term)(x);
552        }
553
554        // Transform source term to frequency domain
555        let f_hat = if self.options.use_real_transform {
556            rfft(&f_values.to_owned())
557        } else {
558            fft(&f_values.to_owned())
559        };
560
561        // Set up wavenumbers (accounting for rfft vs fft)
562        let n_freq = if self.options.use_real_transform {
563            n / 2 + 1
564        } else {
565            n
566        };
567        let mut k = Array1::zeros(n_freq);
568
569        for i in 0..n_freq {
570            if i <= n / 2 {
571                k[i] = 2.0 * PI * i as f64 / length;
572            } else {
573                k[i] = -2.0 * PI * (n - i) as f64 / length;
574            }
575        }
576
577        // Solve in frequency domain: -k²û = f̂ => û = -f̂/k²
578        let mut u_hat =
579            Array1::from_elem(f_hat.len(), scirs2_core::numeric::Complex::new(0.0, 0.0));
580
581        for i in 0..n_freq {
582            if i == 0 {
583                // k=0 mode corresponds to the constant/mean of the solution
584                // For Poisson's equation, this is determined by the source term's mean
585                // Typically, we set it to zero (solution is determined up to a constant)
586                u_hat[i] = scirs2_core::numeric::Complex::new(0.0, 0.0);
587            } else {
588                // For all other modes, solve using the inverse Laplacian
589                u_hat[i] = -f_hat[i] / (k[i] * k[i]);
590            }
591        }
592
593        // Apply dealiasing if requested (2/3 rule)
594        if self.options.use_dealiasing {
595            let cutoff = 2 * n / 3;
596            for i in cutoff..n_freq {
597                u_hat[i] = scirs2_core::numeric::Complex::new(0.0, 0.0);
598            }
599        }
600
601        // Transform back to physical space
602        let u = if self.options.use_real_transform {
603            irfft(&u_hat)
604        } else {
605            ifft(&u_hat).mapv(|c| c.re)
606        };
607
608        // Compute residual norm
609        let mut residual = Array1::zeros(n);
610
611        // Second derivative using spectral accuracy
612        let u_xx = if self.options.use_real_transform {
613            // Compute second derivative in frequency domain
614            let mut u_xx_hat = Array1::zeros(u_hat.len());
615            for i in 0..n_freq {
616                u_xx_hat[i] = -k[i] * k[i] * u_hat[i];
617            }
618            irfft(&u_xx_hat)
619        } else {
620            // Compute second derivative in frequency domain
621            let mut u_xx_hat = Array1::zeros(u_hat.len());
622            for i in 0..n_freq {
623                u_xx_hat[i] = -k[i] * k[i] * u_hat[i];
624            }
625            ifft(&u_xx_hat).mapv(|c| c.re)
626        };
627
628        // Residual: d²u/dx² - f(x)
629        for i in 0..n {
630            residual[i] = u_xx[i] - f_values[i];
631        }
632
633        // Compute L2 norm of residual
634        let residual_norm = (residual.mapv(|r| r * r).sum() / n as f64).sqrt();
635
636        let computation_time = start_time.elapsed().as_secs_f64();
637
638        Ok(SpectralResult {
639            u,
640            coefficients: u_hat.mapv(|c| c.re),
641            grid,
642            residual_norm,
643            num_iterations: 1, // Direct method, one iteration
644            computation_time,
645            convergence_history: None,
646        })
647    }
648}
649
650/// Chebyshev spectral solver for 1D non-periodic PDEs
651pub struct ChebyshevSpectralSolver1D {
652    /// Domain for the problem
653    domain: Domain,
654
655    /// Source term function f(x)
656    source_term: Box<dyn Fn(f64) -> f64 + Send + Sync>,
657
658    /// Boundary conditions (Dirichlet, Neumann, or Robin)
659    boundary_conditions: Vec<BoundaryCondition<f64>>,
660
661    /// Solver options
662    options: SpectralOptions,
663}
664
665impl ChebyshevSpectralSolver1D {
666    /// Create a new Chebyshev spectral solver for 1D non-periodic PDEs
667    pub fn new(
668        domain: Domain,
669        source_term: impl Fn(f64) -> f64 + Send + Sync + 'static,
670        boundary_conditions: Vec<BoundaryCondition<f64>>,
671        options: Option<SpectralOptions>,
672    ) -> PDEResult<Self> {
673        // Validate domain
674        if domain.dimensions() != 1 {
675            return Err(PDEError::DomainError(
676                "Domain must be 1-dimensional for 1D Chebyshev spectral solver".to_string(),
677            ));
678        }
679
680        // Validate boundary _conditions
681        if boundary_conditions.len() != 2 {
682            return Err(PDEError::BoundaryConditions(
683                "1D Chebyshev spectral solver requires exactly 2 boundary _conditions".to_string(),
684            ));
685        }
686
687        let mut has_lower = false;
688        let mut has_upper = false;
689
690        for bc in &boundary_conditions {
691            match bc.location {
692                BoundaryLocation::Lower => has_lower = true,
693                BoundaryLocation::Upper => has_upper = true,
694            }
695
696            // Periodic boundary _conditions are not suitable for Chebyshev methods
697            if bc.bc_type == BoundaryConditionType::Periodic {
698                return Err(PDEError::BoundaryConditions(
699                    "Chebyshev spectral methods are not suitable for periodic boundary _conditions"
700                        .to_string(),
701                ));
702            }
703        }
704
705        if !has_lower || !has_upper {
706            return Err(PDEError::BoundaryConditions(
707                "Chebyshev spectral solver requires boundary _conditions at both domain endpoints"
708                    .to_string(),
709            ));
710        }
711
712        let mut options = options.unwrap_or_default();
713        options.basis = SpectralBasis::Chebyshev; // Ensure Chebyshev basis
714
715        Ok(ChebyshevSpectralSolver1D {
716            domain,
717            source_term: Box::new(source_term),
718            boundary_conditions,
719            options,
720        })
721    }
722
723    /// Solve a 1D non-periodic PDE using Chebyshev spectral method
724    ///
725    /// Solves the equation: d²u/dx² = f(x) with Dirichlet, Neumann, or Robin boundary conditions
726    pub fn solve(&self) -> PDEResult<SpectralResult> {
727        let start_time = Instant::now();
728
729        // Extract domain information
730        let range = &self.domain.ranges[0];
731        let a = range.start;
732        let b = range.end;
733
734        // Number of Chebyshev points
735        let n = self.options.num_modes;
736
737        // Generate Chebyshev grid in [-1, 1]
738        let mut cheb_grid = Array1::zeros(n);
739        for j in 0..n {
740            cheb_grid[j] = (j as f64 * PI / (n - 1) as f64).cos();
741        }
742
743        // Map Chebyshev grid to the domain [a, b]
744        let mut grid = Array1::zeros(n);
745        for j in 0..n {
746            grid[j] = a + (b - a) * (cheb_grid[j] + 1.0) / 2.0;
747        }
748
749        // Compute source term on the grid
750        let mut f_values = Array1::zeros(n);
751        for j in 0..n {
752            f_values[j] = (self.source_term)(grid[j]);
753        }
754
755        // Create second-derivative Chebyshev differentiation matrix
756        let mut d2 = chebyshev_diff2_matrix(n);
757
758        // Scale the differentiation matrix to account for the domain mapping
759        let scale = 4.0 / ((b - a) * (b - a));
760        d2.mapv_inplace(|x| x * scale);
761
762        // Set up the linear system Au = f
763        let mut a_matrix = d2;
764        let mut rhs = f_values;
765
766        // Apply boundary conditions
767        for bc in &self.boundary_conditions {
768            match bc.location {
769                BoundaryLocation::Lower => {
770                    // Lower boundary (x = a, or Chebyshev point = 1)
771                    let j = 0; // First grid point (Chebyshev coordinate 1)
772
773                    match bc.bc_type {
774                        BoundaryConditionType::Dirichlet => {
775                            // u(a) = bc.value
776                            for k in 0..n {
777                                a_matrix[[j, k]] = 0.0;
778                            }
779                            a_matrix[[j, j]] = 1.0;
780                            rhs[j] = bc.value;
781                        }
782                        BoundaryConditionType::Neumann => {
783                            // du/dx(a) = bc.value
784                            // Use first-derivative Chebyshev differentiation matrix
785                            let d1 = chebyshev_diff_matrix(n);
786
787                            // Scale the derivative matrix for domain mapping
788                            let deriv_scale = 2.0 / (b - a);
789
790                            for k in 0..n {
791                                a_matrix[[j, k]] = d1[[j, k]] * deriv_scale;
792                            }
793                            rhs[j] = bc.value;
794                        }
795                        BoundaryConditionType::Robin => {
796                            // a*u(a) + b*du/dx(a) = c
797                            if let Some([a_coef, b_coef, c_coef]) = bc.coefficients {
798                                // Use first-derivative Chebyshev differentiation matrix for the derivative term
799                                let d1 = chebyshev_diff_matrix(n);
800
801                                // Scale the derivative matrix for domain mapping
802                                let deriv_scale = 2.0 / (b - a);
803
804                                for k in 0..n {
805                                    a_matrix[[j, k]] = a_coef + b_coef * d1[[j, k]] * deriv_scale;
806                                }
807                                rhs[j] = c_coef;
808                            }
809                        }
810                        _ => {
811                            return Err(PDEError::BoundaryConditions(
812                                "Unsupported boundary condition type for Chebyshev spectral method"
813                                    .to_string(),
814                            ))
815                        }
816                    }
817                }
818                BoundaryLocation::Upper => {
819                    // Upper boundary (x = b, or Chebyshev point = -1)
820                    let j = n - 1; // Last grid point (Chebyshev coordinate -1)
821
822                    match bc.bc_type {
823                        BoundaryConditionType::Dirichlet => {
824                            // u(b) = bc.value
825                            for k in 0..n {
826                                a_matrix[[j, k]] = 0.0;
827                            }
828                            a_matrix[[j, j]] = 1.0;
829                            rhs[j] = bc.value;
830                        }
831                        BoundaryConditionType::Neumann => {
832                            // du/dx(b) = bc.value
833                            // Use first-derivative Chebyshev differentiation matrix
834                            let d1 = chebyshev_diff_matrix(n);
835
836                            // Scale the derivative matrix for domain mapping
837                            let deriv_scale = 2.0 / (b - a);
838
839                            for k in 0..n {
840                                a_matrix[[j, k]] = d1[[j, k]] * deriv_scale;
841                            }
842                            rhs[j] = bc.value;
843                        }
844                        BoundaryConditionType::Robin => {
845                            // a*u(b) + b*du/dx(b) = c
846                            if let Some([a_coef, b_coef, c_coef]) = bc.coefficients {
847                                // Use first-derivative Chebyshev differentiation matrix for the derivative term
848                                let d1 = chebyshev_diff_matrix(n);
849
850                                // Scale the derivative matrix for domain mapping
851                                let deriv_scale = 2.0 / (b - a);
852
853                                for k in 0..n {
854                                    a_matrix[[j, k]] = a_coef + b_coef * d1[[j, k]] * deriv_scale;
855                                }
856                                rhs[j] = c_coef;
857                            }
858                        }
859                        _ => {
860                            return Err(PDEError::BoundaryConditions(
861                                "Unsupported boundary condition type for Chebyshev spectral method"
862                                    .to_string(),
863                            ))
864                        }
865                    }
866                }
867            }
868        }
869
870        // Solve the linear system
871        let u = ChebyshevSpectralSolver1D::solve_linear_system(&a_matrix, &rhs.view())?;
872
873        // Compute Chebyshev coefficients (optional)
874        let coefficients = chebyshev_transform(&u.view());
875
876        // Compute residual
877        let mut residual = Array1::zeros(n);
878        let a_times_u = a_matrix.dot(&u);
879
880        for i in 0..n {
881            residual[i] = a_times_u[i] - rhs[i];
882        }
883
884        // Exclude boundary points from residual calculation
885        let mut residual_norm = 0.0;
886        for i in 1..n - 1 {
887            residual_norm += residual[i] * residual[i];
888        }
889        residual_norm = (residual_norm / (n - 2) as f64).sqrt();
890
891        let computation_time = start_time.elapsed().as_secs_f64();
892
893        Ok(SpectralResult {
894            u,
895            coefficients,
896            grid,
897            residual_norm,
898            num_iterations: 1, // Direct method, one iteration
899            computation_time,
900            convergence_history: None,
901        })
902    }
903
904    /// Solve the linear system Ax = b
905    fn solve_linear_system(a: &Array2<f64>, b: &ArrayView1<f64>) -> PDEResult<Array1<f64>> {
906        let n = b.len();
907
908        // Simple Gaussian elimination with partial pivoting
909        // For a real implementation, use a specialized linear algebra library
910
911        // Create copies of A and b
912        let mut a_copy = a.clone();
913        let mut b_copy = b.to_owned();
914
915        // Forward elimination
916        for i in 0..n {
917            // Find pivot
918            let mut max_val = a_copy[[i, i]].abs();
919            let mut max_row = i;
920
921            for k in i + 1..n {
922                if a_copy[[k, i]].abs() > max_val {
923                    max_val = a_copy[[k, i]].abs();
924                    max_row = k;
925                }
926            }
927
928            // Check if matrix is singular
929            if max_val < 1e-10 {
930                return Err(PDEError::Other(
931                    "Matrix is singular or nearly singular".to_string(),
932                ));
933            }
934
935            // Swap rows if necessary
936            if max_row != i {
937                for j in i..n {
938                    let temp = a_copy[[i, j]];
939                    a_copy[[i, j]] = a_copy[[max_row, j]];
940                    a_copy[[max_row, j]] = temp;
941                }
942
943                let temp = b_copy[i];
944                b_copy[i] = b_copy[max_row];
945                b_copy[max_row] = temp;
946            }
947
948            // Eliminate below
949            for k in i + 1..n {
950                let factor = a_copy[[k, i]] / a_copy[[i, i]];
951
952                for j in i..n {
953                    a_copy[[k, j]] -= factor * a_copy[[i, j]];
954                }
955
956                b_copy[k] -= factor * b_copy[i];
957            }
958        }
959
960        // Back substitution
961        let mut x = Array1::zeros(n);
962        for i in (0..n).rev() {
963            let mut sum = 0.0;
964            for j in i + 1..n {
965                sum += a_copy[[i, j]] * x[j];
966            }
967
968            x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
969        }
970
971        Ok(x)
972    }
973}
974
975/// Convert SpectralResult to PDESolution
976/// Legendre spectral solver for 1D non-periodic PDEs
977pub struct LegendreSpectralSolver1D {
978    /// Domain for the problem
979    domain: Domain,
980
981    /// Source term function f(x)
982    source_term: Box<dyn Fn(f64) -> f64 + Send + Sync>,
983
984    /// Boundary conditions (Dirichlet, Neumann, or Robin)
985    boundary_conditions: Vec<BoundaryCondition<f64>>,
986
987    /// Solver options
988    options: SpectralOptions,
989}
990
991impl LegendreSpectralSolver1D {
992    /// Create a new Legendre spectral solver for 1D non-periodic PDEs
993    pub fn new(
994        domain: Domain,
995        source_term: impl Fn(f64) -> f64 + Send + Sync + 'static,
996        boundary_conditions: Vec<BoundaryCondition<f64>>,
997        options: Option<SpectralOptions>,
998    ) -> PDEResult<Self> {
999        // Validate domain
1000        if domain.dimensions() != 1 {
1001            return Err(PDEError::DomainError(
1002                "Domain must be 1-dimensional for 1D Legendre spectral solver".to_string(),
1003            ));
1004        }
1005
1006        // Validate boundary _conditions
1007        if boundary_conditions.len() != 2 {
1008            return Err(PDEError::BoundaryConditions(
1009                "1D Legendre spectral solver requires exactly 2 boundary _conditions".to_string(),
1010            ));
1011        }
1012
1013        let mut has_lower = false;
1014        let mut has_upper = false;
1015
1016        for bc in &boundary_conditions {
1017            match bc.location {
1018                BoundaryLocation::Lower => has_lower = true,
1019                BoundaryLocation::Upper => has_upper = true,
1020            }
1021
1022            // Periodic boundary _conditions are not suitable for Legendre methods
1023            if bc.bc_type == BoundaryConditionType::Periodic {
1024                return Err(PDEError::BoundaryConditions(
1025                    "Legendre spectral methods are not suitable for periodic boundary _conditions"
1026                        .to_string(),
1027                ));
1028            }
1029        }
1030
1031        if !has_lower || !has_upper {
1032            return Err(PDEError::BoundaryConditions(
1033                "Legendre spectral solver requires boundary _conditions at both domain endpoints"
1034                    .to_string(),
1035            ));
1036        }
1037
1038        let mut options = options.unwrap_or_default();
1039        options.basis = SpectralBasis::Legendre; // Ensure Legendre basis
1040
1041        Ok(LegendreSpectralSolver1D {
1042            domain,
1043            source_term: Box::new(source_term),
1044            boundary_conditions,
1045            options,
1046        })
1047    }
1048
1049    /// Solve a 1D non-periodic PDE using Legendre spectral method
1050    ///
1051    /// Solves the equation: d²u/dx² = f(x) with Dirichlet, Neumann, or Robin boundary conditions
1052    pub fn solve(&self) -> PDEResult<SpectralResult> {
1053        let start_time = Instant::now();
1054
1055        // Extract domain information
1056        let range = &self.domain.ranges[0];
1057        let a = range.start;
1058        let b = range.end;
1059
1060        // Number of Legendre points
1061        let n = self.options.num_modes;
1062
1063        // Generate Legendre-Gauss-Lobatto grid in [-1, 1] and weights
1064        let (lgb_grid_, weights) = legendre_points(n);
1065
1066        // Map Legendre grid to the domain [a, b]
1067        let mut grid = Array1::zeros(n);
1068        for j in 0..n {
1069            grid[j] = a + (b - a) * (lgb_grid_[j] + 1.0) / 2.0;
1070        }
1071
1072        // Compute source term on the grid
1073        let mut f_values = Array1::zeros(n);
1074        for j in 0..n {
1075            f_values[j] = (self.source_term)(grid[j]);
1076        }
1077
1078        // Create second-derivative Legendre differentiation matrix
1079        let mut d2 = legendre_diff2_matrix(n);
1080
1081        // Scale the differentiation matrix to account for the domain mapping
1082        let scale = 4.0 / ((b - a) * (b - a));
1083        d2.mapv_inplace(|x| x * scale);
1084
1085        // Set up the linear system Au = f
1086        let mut a_matrix = d2;
1087        let mut rhs = f_values;
1088
1089        // Apply boundary conditions
1090        for bc in &self.boundary_conditions {
1091            match bc.location {
1092                BoundaryLocation::Lower => {
1093                    // Lower boundary (x = a, first grid point)
1094                    let j = 0;
1095
1096                    match bc.bc_type {
1097                        BoundaryConditionType::Dirichlet => {
1098                            // u(a) = bc.value
1099                            for k in 0..n {
1100                                a_matrix[[j, k]] = 0.0;
1101                            }
1102                            a_matrix[[j, j]] = 1.0;
1103                            rhs[j] = bc.value;
1104                        }
1105                        BoundaryConditionType::Neumann => {
1106                            // du/dx(a) = bc.value
1107                            // Use first-derivative Legendre differentiation matrix
1108                            let d1 = legendre_diff_matrix(n);
1109
1110                            // Scale the derivative matrix for domain mapping
1111                            let deriv_scale = 2.0 / (b - a);
1112
1113                            for k in 0..n {
1114                                a_matrix[[j, k]] = d1[[j, k]] * deriv_scale;
1115                            }
1116                            rhs[j] = bc.value;
1117                        }
1118                        BoundaryConditionType::Robin => {
1119                            // a*u(a) + b*du/dx(a) = c
1120                            if let Some([a_coef, b_coef, c_coef]) = bc.coefficients {
1121                                // Use first-derivative Legendre differentiation matrix for the derivative term
1122                                let d1 = legendre_diff_matrix(n);
1123
1124                                // Scale the derivative matrix for domain mapping
1125                                let deriv_scale = 2.0 / (b - a);
1126
1127                                for k in 0..n {
1128                                    a_matrix[[j, k]] = a_coef + b_coef * d1[[j, k]] * deriv_scale;
1129                                }
1130                                rhs[j] = c_coef;
1131                            }
1132                        }
1133                        _ => {
1134                            return Err(PDEError::BoundaryConditions(
1135                                "Unsupported boundary condition type for Legendre spectral method"
1136                                    .to_string(),
1137                            ))
1138                        }
1139                    }
1140                }
1141                BoundaryLocation::Upper => {
1142                    // Upper boundary (x = b, last grid point)
1143                    let j = n - 1;
1144
1145                    match bc.bc_type {
1146                        BoundaryConditionType::Dirichlet => {
1147                            // u(b) = bc.value
1148                            for k in 0..n {
1149                                a_matrix[[j, k]] = 0.0;
1150                            }
1151                            a_matrix[[j, j]] = 1.0;
1152                            rhs[j] = bc.value;
1153                        }
1154                        BoundaryConditionType::Neumann => {
1155                            // du/dx(b) = bc.value
1156                            // Use first-derivative Legendre differentiation matrix
1157                            let d1 = legendre_diff_matrix(n);
1158
1159                            // Scale the derivative matrix for domain mapping
1160                            let deriv_scale = 2.0 / (b - a);
1161
1162                            for k in 0..n {
1163                                a_matrix[[j, k]] = d1[[j, k]] * deriv_scale;
1164                            }
1165                            rhs[j] = bc.value;
1166                        }
1167                        BoundaryConditionType::Robin => {
1168                            // a*u(b) + b*du/dx(b) = c
1169                            if let Some([a_coef, b_coef, c_coef]) = bc.coefficients {
1170                                // Use first-derivative Legendre differentiation matrix for the derivative term
1171                                let d1 = legendre_diff_matrix(n);
1172
1173                                // Scale the derivative matrix for domain mapping
1174                                let deriv_scale = 2.0 / (b - a);
1175
1176                                for k in 0..n {
1177                                    a_matrix[[j, k]] = a_coef + b_coef * d1[[j, k]] * deriv_scale;
1178                                }
1179                                rhs[j] = c_coef;
1180                            }
1181                        }
1182                        _ => {
1183                            return Err(PDEError::BoundaryConditions(
1184                                "Unsupported boundary condition type for Legendre spectral method"
1185                                    .to_string(),
1186                            ))
1187                        }
1188                    }
1189                }
1190            }
1191        }
1192
1193        // Solve the linear system
1194        let u = LegendreSpectralSolver1D::solve_linear_system(&a_matrix, &rhs.view())?;
1195
1196        // Compute Legendre coefficients
1197        let coefficients = legendre_transform(&u.view());
1198
1199        // Compute residual
1200        let mut residual = Array1::zeros(n);
1201        let a_times_u = a_matrix.dot(&u);
1202
1203        for i in 0..n {
1204            residual[i] = a_times_u[i] - rhs[i];
1205        }
1206
1207        // Exclude boundary points from residual calculation
1208        let mut residual_norm = 0.0;
1209        for i in 1..n - 1 {
1210            residual_norm += residual[i] * residual[i];
1211        }
1212        residual_norm = (residual_norm / (n - 2) as f64).sqrt();
1213
1214        let computation_time = start_time.elapsed().as_secs_f64();
1215
1216        Ok(SpectralResult {
1217            u,
1218            coefficients,
1219            grid,
1220            residual_norm,
1221            num_iterations: 1, // Direct method, one iteration
1222            computation_time,
1223            convergence_history: None,
1224        })
1225    }
1226
1227    /// Solve the linear system Ax = b
1228    fn solve_linear_system(a: &Array2<f64>, b: &ArrayView1<f64>) -> PDEResult<Array1<f64>> {
1229        let n = b.len();
1230
1231        // Simple Gaussian elimination with partial pivoting
1232        // For a real implementation, use a specialized linear algebra library
1233
1234        // Create copies of A and b
1235        let mut a_copy = a.clone();
1236        let mut b_copy = b.to_owned();
1237
1238        // Forward elimination
1239        for i in 0..n {
1240            // Find pivot
1241            let mut max_val = a_copy[[i, i]].abs();
1242            let mut max_row = i;
1243
1244            for k in i + 1..n {
1245                if a_copy[[k, i]].abs() > max_val {
1246                    max_val = a_copy[[k, i]].abs();
1247                    max_row = k;
1248                }
1249            }
1250
1251            // Check if matrix is singular
1252            if max_val < 1e-10 {
1253                return Err(PDEError::Other(
1254                    "Matrix is singular or nearly singular".to_string(),
1255                ));
1256            }
1257
1258            // Swap rows if necessary
1259            if max_row != i {
1260                for j in i..n {
1261                    let temp = a_copy[[i, j]];
1262                    a_copy[[i, j]] = a_copy[[max_row, j]];
1263                    a_copy[[max_row, j]] = temp;
1264                }
1265
1266                let temp = b_copy[i];
1267                b_copy[i] = b_copy[max_row];
1268                b_copy[max_row] = temp;
1269            }
1270
1271            // Eliminate below
1272            for k in i + 1..n {
1273                let factor = a_copy[[k, i]] / a_copy[[i, i]];
1274
1275                for j in i..n {
1276                    a_copy[[k, j]] -= factor * a_copy[[i, j]];
1277                }
1278
1279                b_copy[k] -= factor * b_copy[i];
1280            }
1281        }
1282
1283        // Back substitution
1284        let mut x = Array1::zeros(n);
1285        for i in (0..n).rev() {
1286            let mut sum = 0.0;
1287            for j in i + 1..n {
1288                sum += a_copy[[i, j]] * x[j];
1289            }
1290
1291            x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
1292        }
1293
1294        Ok(x)
1295    }
1296}
1297
1298impl From<SpectralResult> for PDESolution<f64> {
1299    fn from(result: SpectralResult) -> Self {
1300        let grids = vec![result.grid.clone()];
1301
1302        // Create solution values as a 2D array with one column
1303        let mut values = Vec::new();
1304        // Clone the result.u to avoid the move issue
1305        let u_clone = result.u.clone();
1306        let u_len = u_clone.len();
1307        let u_reshaped = u_clone.into_shape_with_order((u_len, 1)).unwrap();
1308        values.push(u_reshaped);
1309
1310        // Create solver info
1311        let info = PDESolverInfo {
1312            num_iterations: result.num_iterations,
1313            computation_time: result.computation_time,
1314            residual_norm: Some(result.residual_norm),
1315            convergence_history: result.convergence_history,
1316            method: "Spectral Method".to_string(),
1317        };
1318
1319        PDESolution {
1320            grids,
1321            values,
1322            error_estimate: None,
1323            info,
1324        }
1325    }
1326}
1327
1328// Stub FFT implementations
1329// These are temporary placeholders for the missing FFT functions
1330// In a real implementation, these would use a proper FFT library
1331
1332/// Perform a Fast Fourier Transform (FFT) on a real-valued array using Cooley-Tukey algorithm
1333///
1334/// # Arguments
1335/// * `x` - The input array
1336///
1337/// # Returns
1338/// * A complex-valued array containing the FFT result
1339#[allow(dead_code)]
1340fn fft(x: &Array1<f64>) -> Array1<scirs2_core::numeric::Complex<f64>> {
1341    // Convert to _complex array
1342    let mut input: Vec<scirs2_core::numeric::Complex<f64>> = x
1343        .iter()
1344        .map(|&val| scirs2_core::numeric::Complex::new(val, 0.0))
1345        .collect();
1346
1347    // Perform FFT
1348    fft_complex(&mut input);
1349
1350    // Convert back to Array1
1351    Array1::from_vec(input)
1352}
1353
1354/// Cooley-Tukey FFT algorithm for complex input (in-place)
1355#[allow(dead_code)]
1356fn fft_complex(x: &mut [scirs2_core::numeric::Complex<f64>]) {
1357    let n = x.len();
1358
1359    if n <= 1 {
1360        return;
1361    }
1362
1363    // For power-of-2 lengths, use radix-2 FFT
1364    if n.is_power_of_two() {
1365        fft_radix2(x);
1366    } else {
1367        // Fall back to mixed-radix or DFT for non-power-of-2
1368        fft_mixed_radix(x);
1369    }
1370}
1371
1372/// Radix-2 Cooley-Tukey FFT for power-of-2 lengths
1373#[allow(dead_code)]
1374fn fft_radix2(x: &mut [scirs2_core::numeric::Complex<f64>]) {
1375    let n = x.len();
1376
1377    if n <= 1 {
1378        return;
1379    }
1380
1381    // Bit-reversal permutation
1382    let mut j = 0;
1383    for i in 1..n {
1384        let mut bit = n >> 1;
1385        while j & bit != 0 {
1386            j ^= bit;
1387            bit >>= 1;
1388        }
1389        j ^= bit;
1390
1391        if j > i {
1392            x.swap(i, j);
1393        }
1394    }
1395
1396    // Cooley-Tukey FFT
1397    let mut length = 2;
1398    while length <= n {
1399        let angle = -2.0 * PI / (length as f64);
1400        let wlen = scirs2_core::numeric::Complex::new(angle.cos(), angle.sin());
1401
1402        for i in (0..n).step_by(length) {
1403            let mut w = scirs2_core::numeric::Complex::new(1.0, 0.0);
1404
1405            for j in 0..length / 2 {
1406                let u = x[i + j];
1407                let v = x[i + j + length / 2] * w;
1408
1409                x[i + j] = u + v;
1410                x[i + j + length / 2] = u - v;
1411
1412                w *= wlen;
1413            }
1414        }
1415
1416        length <<= 1;
1417    }
1418}
1419
1420/// Mixed-radix FFT for non-power-of-2 lengths
1421#[allow(dead_code)]
1422fn fft_mixed_radix(x: &mut [scirs2_core::numeric::Complex<f64>]) {
1423    let n = x.len();
1424
1425    // Simple DFT for small sizes or non-power-of-2
1426    if n < 32 || !n.is_power_of_two() {
1427        let input = x.to_vec();
1428        for k in 0..n {
1429            let mut sum = scirs2_core::numeric::Complex::new(0.0, 0.0);
1430            for j in 0..n {
1431                let angle = -2.0 * PI * (j as f64) * (k as f64) / (n as f64);
1432                let factor = scirs2_core::numeric::Complex::new(angle.cos(), angle.sin());
1433                sum += factor * input[j];
1434            }
1435            x[k] = sum;
1436        }
1437    } else {
1438        fft_radix2(x);
1439    }
1440}
1441
1442/// Perform an Inverse Fast Fourier Transform (IFFT) on a complex-valued array
1443///
1444/// # Arguments
1445/// * `x` - The input array
1446///
1447/// # Returns
1448/// * A complex-valued array containing the IFFT result
1449#[allow(dead_code)]
1450fn ifft(
1451    x: &Array1<scirs2_core::numeric::Complex<f64>>,
1452) -> Array1<scirs2_core::numeric::Complex<f64>> {
1453    let n = x.len();
1454    let mut input: Vec<scirs2_core::numeric::Complex<f64>> = x.to_vec();
1455
1456    // Take _complex conjugate
1457    for val in &mut input {
1458        *val = val.conj();
1459    }
1460
1461    // Perform FFT
1462    fft_complex(&mut input);
1463
1464    // Take _complex conjugate and scale by 1/n
1465    let scale = 1.0 / (n as f64);
1466    for val in &mut input {
1467        *val = val.conj() * scale;
1468    }
1469
1470    Array1::from_vec(input)
1471}
1472
1473/// Perform a Real Fast Fourier Transform (RFFT) on a real-valued array
1474///
1475/// # Arguments
1476/// * `x` - The input array
1477///
1478/// # Returns
1479/// * A complex-valued array containing the RFFT result (only positive frequencies)
1480#[allow(dead_code)]
1481fn rfft(x: &Array1<f64>) -> Array1<scirs2_core::numeric::Complex<f64>> {
1482    let n = x.len();
1483    let full_fft = fft(x);
1484
1485    // For real input, the FFT is symmetric: X[n-k] = X[k]^*
1486    // We only need the first n/2 + 1 components
1487    let rfft_size = n / 2 + 1;
1488    let mut result = Array1::zeros(rfft_size);
1489
1490    for i in 0..rfft_size {
1491        result[i] = full_fft[i];
1492    }
1493
1494    result
1495}
1496
1497/// Perform an Inverse Real Fast Fourier Transform (IRFFT) on a complex-valued array
1498///
1499/// # Arguments
1500/// * `x` - The input array (RFFT coefficients)
1501/// * `n` - The desired output length (must be even for proper reconstruction)
1502///
1503/// # Returns
1504/// * A real-valued array containing the IRFFT result
1505#[allow(dead_code)]
1506fn irfft_with_size(x: &Array1<scirs2_core::numeric::Complex<f64>>, n: usize) -> Array1<f64> {
1507    // Reconstruct the full _complex spectrum using Hermitian symmetry
1508    let mut full_spectrum = Array1::zeros(n);
1509    let rfft_size = x.len();
1510
1511    // Copy the positive frequencies
1512    for i in 0..rfft_size {
1513        full_spectrum[i] = x[i];
1514    }
1515
1516    // Fill in the negative frequencies using Hermitian symmetry: X[n-k] = X[k]^*
1517    for i in 1..n / 2 {
1518        full_spectrum[n - i] = x[i].conj();
1519    }
1520
1521    // Perform IFFT
1522    let complex_result = ifft(&full_spectrum);
1523
1524    // Extract real parts (imaginary parts should be negligible for real input)
1525    let mut result = Array1::zeros(n);
1526    for i in 0..n {
1527        result[i] = complex_result[i].re;
1528    }
1529
1530    result
1531}
1532
1533/// Perform an Inverse Real Fast Fourier Transform (IRFFT) on a complex-valued array
1534///
1535/// # Arguments
1536/// * `x` - The input array (RFFT coefficients)
1537///
1538/// # Returns
1539/// * A real-valued array containing the IRFFT result
1540#[allow(dead_code)]
1541fn irfft(x: &Array1<scirs2_core::numeric::Complex<f64>>) -> Array1<f64> {
1542    // Infer the output size from the input size
1543    // For RFFT output of size k, the original input was size 2*(k-1)
1544    let n = 2 * (x.len() - 1);
1545    irfft_with_size(x, n)
1546}
1547
1548// Import spectral element module
1549pub mod spectral_element;
1550pub use spectral_element::{
1551    QuadElement, SpectralElementMesh2D, SpectralElementOptions, SpectralElementPoisson2D,
1552    SpectralElementResult,
1553};