Skip to main content

scirs2_fft/
planning.rs

1//! FFT Planning Module
2//!
3//! This module provides an advanced planning system for FFT operations, combining
4//! caching, serialization, and auto-tuning to optimize performance. It builds on the
5//! functionality provided by the plan_cache and plan_serialization modules.
6//!
7//! Key features:
8//! - Unified planning interface for different FFT backends
9//! - Multi-dimensional FFT planning
10//! - Adaptive plan selection based on input characteristics
11//! - Integration with auto-tuning for hardware-specific optimizations
12//! - Support for both runtime and ahead-of-time planning
13//! - Plan pruning and management to optimize memory usage
14
15use crate::auto_tuning::{AutoTuneConfig, AutoTuner, FftVariant};
16use crate::backend::BackendContext;
17use crate::error::{FFTError, FFTResult};
18#[cfg(feature = "oxifft")]
19use crate::oxifft_plan_cache;
20use crate::plan_serialization::{PlanMetrics, PlanSerializationManager};
21#[cfg(feature = "oxifft")]
22use oxifft::{Complex as OxiComplex, Direction};
23
24use scirs2_core::ndarray::{ArrayBase, Data, Dimension};
25use scirs2_core::numeric::Complex64;
26use std::collections::HashMap;
27use std::sync::{Arc, Mutex};
28use std::time::{Duration, Instant};
29
30/// Enum for different planning strategies
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
32pub enum PlanningStrategy {
33    /// Always create a new plan
34    AlwaysNew,
35    /// Try cache first, create new plan if not found
36    #[default]
37    CacheFirst,
38    /// Try serialized plans first, then cache, then create new
39    SerializedFirst,
40    /// Auto-tune to find the best plan for the current hardware
41    AutoTuned,
42}
43
44/// Configuration options for FFT planning
45#[derive(Debug, Clone)]
46pub struct PlanningConfig {
47    /// Planning strategy to use
48    pub strategy: PlanningStrategy,
49    /// Whether to measure plan performance
50    pub measure_performance: bool,
51    /// Path to serialized plans database
52    pub serialized_db_path: Option<String>,
53    /// Auto-tuning configuration (if AutoTuned strategy is selected)
54    pub auto_tune_config: Option<AutoTuneConfig>,
55    /// Maximum number of plans to keep in memory
56    pub max_cached_plans: usize,
57    /// Maximum age for cached plans
58    pub max_plan_age: Duration,
59    /// Whether to use parallel execution for planning
60    pub parallel_planning: bool,
61}
62
63impl Default for PlanningConfig {
64    fn default() -> Self {
65        Self {
66            strategy: PlanningStrategy::default(),
67            measure_performance: true,
68            serialized_db_path: None,
69            auto_tune_config: None,
70            max_cached_plans: 128,
71            max_plan_age: Duration::from_secs(3600), // 1 hour
72            parallel_planning: true,
73        }
74    }
75}
76
77/// Multidimensional FFT plan that handles different array shapes and layouts
78#[derive(Clone)]
79pub struct FftPlan {
80    /// Array shape the plan is optimized for
81    shape: Vec<usize>,
82    /// Whether the plan is for a forward or inverse transform
83    forward: bool,
84    /// Performance metrics for this plan
85    metrics: Option<PlanMetrics>,
86    /// Backend used to create this plan
87    /// Preserved for potential switching between backends
88    #[allow(dead_code)]
89    backend: PlannerBackend,
90    /// Auto-tuning information (if applicable)
91    pub(crate) auto_tune_info: Option<FftVariant>,
92    /// Last time this plan was used
93    pub(crate) last_used: Instant,
94    /// Number of times this plan has been used
95    pub(crate) usage_count: usize,
96}
97
98impl FftPlan {
99    /// Create a new FFT plan for the given shape and direction.
100    /// Uses OxiFFT as the execution backend (COOLJAPAN Pure Rust policy).
101    pub fn new(shape: &[usize], forward: bool, backend: PlannerBackend) -> Self {
102        Self {
103            shape: shape.to_vec(),
104            forward,
105            metrics: None,
106            backend,
107            auto_tune_info: None,
108            last_used: Instant::now(),
109            usage_count: 0,
110        }
111    }
112
113    /// Get the forward direction of this plan
114    pub fn is_forward(&self) -> bool {
115        self.forward
116    }
117
118    /// Record usage of this plan
119    pub fn record_usage(&mut self) {
120        self.usage_count += 1;
121        self.last_used = Instant::now();
122    }
123
124    /// Get shape this plan is optimized for
125    pub fn shape(&self) -> &[usize] {
126        &self.shape
127    }
128
129    /// Check if this plan is compatible with the given shape
130    pub fn is_compatible_with(&self, shape: &[usize]) -> bool {
131        if self.shape.len() != shape.len() {
132            return false;
133        }
134
135        self.shape.iter().zip(shape.iter()).all(|(&a, &b)| a == b)
136    }
137
138    /// Get plan metrics
139    pub fn metrics(&self) -> Option<&PlanMetrics> {
140        self.metrics.as_ref()
141    }
142
143    /// Set plan metrics
144    pub fn set_metrics(&mut self, metrics: PlanMetrics) {
145        self.metrics = Some(metrics);
146    }
147}
148
149/// Planner for FFT operations that combines caching, serialization and auto-tuning.
150/// Uses OxiFFT as the execution backend (COOLJAPAN Pure Rust policy).
151pub struct AdvancedFftPlanner {
152    /// Configuration options
153    config: PlanningConfig,
154    /// In-memory plan cache
155    cache: Arc<Mutex<HashMap<PlanKey, FftPlan>>>,
156    /// Plan serialization manager (if enabled)
157    serialization_manager: Option<PlanSerializationManager>,
158    /// Auto-tuner (if enabled)
159    auto_tuner: Option<AutoTuner>,
160}
161
162/// Backend type for FFT operations
163#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
164pub enum PlannerBackend {
165    /// OxiFFT - Pure Rust high-performance FFT (default, COOLJAPAN policy)
166    #[default]
167    OxiFFT,
168    /// CUDA-accelerated backend
169    CUDA,
170    /// Custom backend implementation
171    Custom(String),
172}
173
174/// Key for plan cache lookup
175#[derive(Clone, Debug, Hash, PartialEq, Eq)]
176struct PlanKey {
177    /// Shape of the FFT
178    shape: Vec<usize>,
179    /// Direction of the transform
180    forward: bool,
181    /// Backend identifier
182    backend: PlannerBackend,
183}
184
185/// Advanced FFT planner for managing plans
186impl AdvancedFftPlanner {
187    /// Create a new FftPlanner with default configuration
188    pub fn new() -> Self {
189        Self::with_config(PlanningConfig::default())
190    }
191
192    /// Create a new FftPlanner with custom configuration
193    pub fn with_config(config: PlanningConfig) -> Self {
194        let serialization_manager = config
195            .serialized_db_path
196            .as_ref()
197            .map(PlanSerializationManager::new);
198
199        let auto_tuner = if config.strategy == PlanningStrategy::AutoTuned {
200            let tuner = AutoTuner::new();
201            if let Some(_autoconfig) = &config.auto_tune_config {
202                // Configure the auto-tuner here if needed
203                // This is a simplified implementation
204            }
205            Some(tuner)
206        } else {
207            None
208        };
209
210        Self {
211            config,
212            cache: Arc::new(Mutex::new(HashMap::new())),
213            serialization_manager,
214            auto_tuner,
215        }
216    }
217
218    /// Clear the plan cache
219    pub fn clear_cache(&self) {
220        if let Ok(mut cache) = self.cache.lock() {
221            cache.clear();
222        }
223    }
224
225    /// Get or create a plan for the given shape and direction
226    pub fn plan_fft(
227        &mut self,
228        shape: &[usize],
229        forward: bool,
230        backend: PlannerBackend,
231    ) -> FFTResult<Arc<FftPlan>> {
232        let key = PlanKey {
233            shape: shape.to_vec(),
234            forward,
235            backend: backend.clone(),
236        };
237
238        // Check in-memory cache first if strategy allows
239        if self.config.strategy == PlanningStrategy::CacheFirst
240            || self.config.strategy == PlanningStrategy::SerializedFirst
241        {
242            if let Ok(mut cache) = self.cache.lock() {
243                if let Some(plan) = cache.get_mut(&key) {
244                    plan.record_usage();
245                    return Ok(Arc::new(plan.clone()));
246                }
247            }
248        }
249
250        // Check serialized plans if strategy is SerializedFirst
251        if self.config.strategy == PlanningStrategy::SerializedFirst {
252            if let Some(manager) = &self.serialization_manager {
253                // For simplicity, we'll use the primary dimension size for serialization
254                // In a full implementation, this would handle multidimensional plans better
255                let size = shape.iter().product();
256
257                if manager.plan_exists(size, forward) {
258                    if let Some((_plan_info, metrics)) =
259                        manager.get_best_plan_metrics(size, forward)
260                    {
261                        // Create a new plan using the cached metadata
262                        let mut plan = self.create_new_plan(shape, forward, backend.clone())?;
263                        plan.set_metrics(metrics.clone());
264
265                        // Store in cache
266                        if let Ok(mut cache) = self.cache.lock() {
267                            cache.insert(key, plan.clone());
268                        }
269
270                        return Ok(Arc::new(plan));
271                    }
272                }
273            }
274        }
275
276        // Use auto-tuning if strategy is AutoTuned
277        if self.config.strategy == PlanningStrategy::AutoTuned {
278            if let Some(tuner) = &self.auto_tuner {
279                // This is a simplified example - a full implementation would do much more
280                let size = shape.iter().product();
281                let variant = tuner.get_best_variant(size, forward);
282
283                // In a real implementation, we would use the variant to create an optimized plan
284                let mut plan = self.create_new_plan(shape, forward, backend)?;
285                plan.auto_tune_info = Some(variant);
286
287                // Store in cache
288                if let Ok(mut cache) = self.cache.lock() {
289                    cache.insert(key, plan.clone());
290                }
291
292                return Ok(Arc::new(plan));
293            }
294        }
295
296        // Default: create a new plan
297        let plan = self.create_new_plan(shape, forward, backend)?;
298
299        // Store in cache if caching is enabled
300        if self.config.strategy != PlanningStrategy::AlwaysNew {
301            if let Ok(mut cache) = self.cache.lock() {
302                // Clean up old entries if we're at capacity
303                if cache.len() >= self.config.max_cached_plans {
304                    self.evict_old_entries(&mut cache);
305                }
306
307                cache.insert(key, plan.clone());
308            }
309        }
310
311        Ok(Arc::new(plan))
312    }
313
314    /// Create a new plan without using the cache
315    fn create_new_plan(
316        &mut self,
317        shape: &[usize],
318        forward: bool,
319        backend: PlannerBackend,
320    ) -> FFTResult<FftPlan> {
321        // Measure plan creation time if enabled
322        let start = Instant::now();
323
324        let plan = FftPlan::new(shape, forward, backend);
325
326        let elapsed = start.elapsed();
327
328        // Record metrics for the new plan if measurement is enabled
329        if self.config.measure_performance {
330            if let Some(manager) = &self.serialization_manager {
331                // For simplicity, we'll use the primary dimension size
332                // In a full implementation, this would handle multidimensional plans better
333                let size = shape.iter().product();
334
335                let plan_info = manager.create_plan_info(size, forward);
336                let _ = manager.record_plan_usage(&plan_info, elapsed.as_nanos() as u64);
337            }
338        }
339
340        Ok(plan)
341    }
342
343    /// Evict old entries from the cache (LRU-style)
344    fn evict_old_entries(&self, cache: &mut HashMap<PlanKey, FftPlan>) {
345        // Remove entries older than max_age
346        let max_age = self.config.max_plan_age;
347        cache.retain(|_, v| v.last_used.elapsed() <= max_age);
348
349        // If still over capacity, remove least recently used
350        let max_entries = self.config.max_cached_plans;
351        while cache.len() >= max_entries {
352            if let Some((key_to_remove_, _)) = cache
353                .iter()
354                .min_by_key(|(_, v)| (v.last_used, v.usage_count))
355                .map(|(k_, _)| (k_.clone(), ()))
356            {
357                cache.remove(&key_to_remove_);
358            } else {
359                break;
360            }
361        }
362    }
363
364    /// Plan a 1D FFT
365    pub fn plan_fft_1d<S, D>(
366        &mut self,
367        arr: &ArrayBase<S, D>,
368        forward: bool,
369    ) -> FFTResult<Arc<FftPlan>>
370    where
371        S: Data<Elem = Complex64>,
372        D: Dimension,
373    {
374        let shape = arr.shape().to_vec();
375        self.plan_fft(&shape, forward, PlannerBackend::default())
376    }
377
378    /// Plan a 2D FFT
379    pub fn plan_fft_2d<S, D>(
380        &mut self,
381        arr: &ArrayBase<S, D>,
382        forward: bool,
383    ) -> FFTResult<Arc<FftPlan>>
384    where
385        S: Data<Elem = Complex64>,
386        D: Dimension,
387    {
388        if arr.ndim() != 2 {
389            return Err(FFTError::ValueError(
390                "Input array must be 2-dimensional".to_string(),
391            ));
392        }
393
394        let shape = arr.shape().to_vec();
395        self.plan_fft(&shape, forward, PlannerBackend::default())
396    }
397
398    /// Plan an N-dimensional FFT
399    pub fn plan_fft_nd<S, D>(
400        &mut self,
401        arr: &ArrayBase<S, D>,
402        forward: bool,
403    ) -> FFTResult<Arc<FftPlan>>
404    where
405        S: Data<Elem = Complex64>,
406        D: Dimension,
407    {
408        let shape = arr.shape().to_vec();
409        self.plan_fft(&shape, forward, PlannerBackend::default())
410    }
411
412    /// Pre-compute plans for common sizes
413    pub fn precompute_commonsizes(&mut self, sizes: &[&[usize]]) -> FFTResult<()> {
414        for &shape in sizes {
415            // Create both forward and inverse plans
416            let _ = self.plan_fft(shape, true, PlannerBackend::default())?;
417            let _ = self.plan_fft(shape, false, PlannerBackend::default())?;
418        }
419
420        Ok(())
421    }
422
423    /// Save all plans to disk
424    pub fn save_plans(&self) -> FFTResult<()> {
425        if let Some(manager) = &self.serialization_manager {
426            manager.save_database()?;
427        }
428
429        Ok(())
430    }
431}
432
433impl Default for AdvancedFftPlanner {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439/// Global FFT planner instance
440static GLOBAL_FFT_PLANNER: std::sync::OnceLock<Mutex<AdvancedFftPlanner>> =
441    std::sync::OnceLock::new();
442
443/// Get the global FFT planner instance
444#[allow(dead_code)]
445pub fn get_global_planner() -> &'static Mutex<AdvancedFftPlanner> {
446    GLOBAL_FFT_PLANNER.get_or_init(|| Mutex::new(AdvancedFftPlanner::new()))
447}
448
449/// Initialize the global FFT planner with custom configuration
450#[allow(dead_code)]
451pub fn init_global_planner(config: PlanningConfig) -> Result<(), &'static str> {
452    GLOBAL_FFT_PLANNER
453        .set(Mutex::new(AdvancedFftPlanner::with_config(config)))
454        .map_err(|_| "Global FFT planner already initialized")
455}
456
457/// Plan wrapper that provides context and execution methods
458pub struct FftPlanExecutor {
459    /// The underlying FFT plan
460    plan: Arc<FftPlan>,
461    /// Execution context for this plan
462    /// Reserved for future optimizations
463    #[allow(dead_code)]
464    context: Option<BackendContext>,
465}
466
467impl FftPlanExecutor {
468    /// Create a new executor for the given plan
469    pub fn new(plan: Arc<FftPlan>) -> Self {
470        Self {
471            plan,
472            context: None,
473        }
474    }
475
476    /// Create a new executor with a specific context
477    pub fn with_context(plan: Arc<FftPlan>, context: BackendContext) -> Self {
478        Self {
479            plan,
480            context: Some(context),
481        }
482    }
483
484    /// Execute the plan on the given input/output buffers.
485    /// Uses OxiFFT as the backend (COOLJAPAN Pure Rust policy).
486    pub fn execute(&self, input: &[Complex64], output: &mut [Complex64]) -> FFTResult<()> {
487        let expected_size: usize = self.plan.shape().iter().product();
488        if input.len() != expected_size || output.len() != expected_size {
489            return Err(FFTError::ValueError(format!(
490                "Buffer size mismatch: expected {}, got input={}, output={}",
491                expected_size,
492                input.len(),
493                output.len()
494            )));
495        }
496
497        #[cfg(feature = "oxifft")]
498        {
499            let direction = if self.plan.is_forward() {
500                Direction::Forward
501            } else {
502                Direction::Backward
503            };
504            let input_oxi: Vec<OxiComplex<f64>> =
505                input.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
506            let mut output_oxi: Vec<OxiComplex<f64>> =
507                vec![OxiComplex::new(0.0, 0.0); expected_size];
508            oxifft_plan_cache::execute_c2c(&input_oxi, &mut output_oxi, direction)?;
509            for (i, c) in output_oxi.iter().enumerate() {
510                output[i] = Complex64::new(c.re, c.im);
511            }
512            Ok(())
513        }
514
515        #[cfg(not(feature = "oxifft"))]
516        {
517            Err(FFTError::ComputationError(
518                "No FFT backend available. Enable 'oxifft' feature.".to_string(),
519            ))
520        }
521    }
522
523    /// Execute the plan in-place on the given buffer.
524    /// Uses OxiFFT as the backend (COOLJAPAN Pure Rust policy).
525    pub fn execute_inplace(&self, buffer: &mut [Complex64]) -> FFTResult<()> {
526        let expected_size: usize = self.plan.shape().iter().product();
527        if buffer.len() != expected_size {
528            return Err(FFTError::ValueError(format!(
529                "Buffer size mismatch: expected {}, got {}",
530                expected_size,
531                buffer.len()
532            )));
533        }
534
535        #[cfg(feature = "oxifft")]
536        {
537            let direction = if self.plan.is_forward() {
538                Direction::Forward
539            } else {
540                Direction::Backward
541            };
542            let input_oxi: Vec<OxiComplex<f64>> =
543                buffer.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
544            let mut output_oxi: Vec<OxiComplex<f64>> =
545                vec![OxiComplex::new(0.0, 0.0); expected_size];
546            oxifft_plan_cache::execute_c2c(&input_oxi, &mut output_oxi, direction)?;
547            for (i, c) in output_oxi.iter().enumerate() {
548                buffer[i] = Complex64::new(c.re, c.im);
549            }
550            Ok(())
551        }
552
553        #[cfg(not(feature = "oxifft"))]
554        {
555            Err(FFTError::ComputationError(
556                "No FFT backend available. Enable 'oxifft' feature.".to_string(),
557            ))
558        }
559    }
560
561    /// Get the plan this executor uses
562    pub fn plan(&self) -> &FftPlan {
563        &self.plan
564    }
565}
566
567/// Builder for creating customized FFT plans
568pub struct PlanBuilder {
569    /// Planning configuration
570    config: PlanningConfig,
571    /// Shape for the transform
572    shape: Option<Vec<usize>>,
573    /// Direction of the transform
574    forward: bool,
575    /// Backend to use
576    backend: PlannerBackend,
577}
578
579impl PlanBuilder {
580    /// Create a new plan builder
581    pub fn new() -> Self {
582        Self {
583            config: PlanningConfig::default(),
584            shape: None,
585            forward: true,
586            backend: PlannerBackend::default(),
587        }
588    }
589
590    /// Set the shape for the plan
591    pub fn shape(mut self, shape: &[usize]) -> Self {
592        self.shape = Some(shape.to_vec());
593        self
594    }
595
596    /// Set the direction for the plan
597    pub fn forward(mut self, forward: bool) -> Self {
598        self.forward = forward;
599        self
600    }
601
602    /// Set the backend for the plan
603    pub fn backend(mut self, backend: PlannerBackend) -> Self {
604        self.backend = backend;
605        self
606    }
607
608    /// Set the planning strategy
609    pub fn strategy(mut self, strategy: PlanningStrategy) -> Self {
610        self.config.strategy = strategy;
611        self
612    }
613
614    /// Enable or disable performance measurement
615    pub fn measure_performance(mut self, enable: bool) -> Self {
616        self.config.measure_performance = enable;
617        self
618    }
619
620    /// Set the path for serialized plans
621    pub fn serialized_db_path(mut self, path: &str) -> Self {
622        self.config.serialized_db_path = Some(path.to_string());
623        self
624    }
625
626    /// Set auto-tuning configuration
627    pub fn auto_tune_config(mut self, config: AutoTuneConfig) -> Self {
628        self.config.auto_tune_config = Some(config);
629        self
630    }
631
632    /// Set maximum number of cached plans
633    pub fn max_cached_plans(mut self, max: usize) -> Self {
634        self.config.max_cached_plans = max;
635        self
636    }
637
638    /// Set maximum age for cached plans
639    pub fn max_plan_age(mut self, age: Duration) -> Self {
640        self.config.max_plan_age = age;
641        self
642    }
643
644    /// Enable or disable parallel planning
645    pub fn parallel_planning(mut self, enable: bool) -> Self {
646        self.config.parallel_planning = enable;
647        self
648    }
649
650    /// Build the plan
651    pub fn build(self) -> FFTResult<Arc<FftPlan>> {
652        let shape = self
653            .shape
654            .ok_or_else(|| FFTError::ValueError("Cannot build plan without shape".to_string()))?;
655
656        let mut planner = AdvancedFftPlanner::with_config(self.config);
657        planner.plan_fft(&shape, self.forward, self.backend)
658    }
659}
660
661impl Default for PlanBuilder {
662    fn default() -> Self {
663        Self::new()
664    }
665}
666
667/// Plan ahead-of-time for common FFT sizes
668///
669/// This function pre-computes plans for commonly used FFT sizes
670/// and stores them in the cache for faster initialization later.
671///
672/// # Arguments
673///
674/// * `sizes` - List of common FFT sizes to pre-compute plans for
675/// * `db_path` - Optional path to store serialized plans
676///
677/// # Returns
678///
679/// Result indicating success or failure
680#[allow(dead_code)]
681pub fn plan_ahead_of_time(sizes: &[usize], dbpath: Option<&str>) -> FFTResult<()> {
682    let mut config = PlanningConfig::default();
683    if let Some(_path) = dbpath {
684        config.serialized_db_path = Some(_path.to_string());
685        config.strategy = PlanningStrategy::SerializedFirst;
686    }
687
688    let mut planner = AdvancedFftPlanner::with_config(config);
689
690    // Convert to shapes (assuming 1D transforms for simplicity)
691    let shapes: Vec<Vec<usize>> = sizes.iter().map(|&s| vec![s]).collect();
692
693    for shape in shapes {
694        // Create both forward and inverse plans
695        let _ = planner.plan_fft(&shape, true, PlannerBackend::default())?;
696        let _ = planner.plan_fft(&shape, false, PlannerBackend::default())?;
697    }
698
699    // Save plans if serialization is enabled
700    planner.save_plans()?;
701
702    Ok(())
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use scirs2_core::numeric::Complex64;
709    use tempfile::tempdir;
710
711    #[test]
712    fn test_plan_basic() {
713        let mut planner = AdvancedFftPlanner::new();
714        let shape = vec![8, 8];
715
716        // Create a plan
717        let plan = planner
718            .plan_fft(&shape, true, PlannerBackend::default())
719            .expect("Operation failed");
720
721        // Check that the plan has the right shape
722        assert_eq!(plan.shape(), &shape);
723        assert!(plan.is_compatible_with(&shape));
724
725        // Check that a different shape is not compatible
726        assert!(!plan.is_compatible_with(&[16, 16]));
727    }
728
729    #[test]
730    fn test_plan_executor() {
731        let mut planner = AdvancedFftPlanner::new();
732        let shape = vec![8];
733
734        // Create a plan
735        let plan = planner
736            .plan_fft(&shape, true, PlannerBackend::default())
737            .expect("Operation failed");
738
739        // Create an executor
740        let executor = FftPlanExecutor::new(plan);
741
742        // Create some test data
743        let input = vec![
744            Complex64::new(1.0, 0.0),
745            Complex64::new(0.0, 0.0),
746            Complex64::new(0.0, 0.0),
747            Complex64::new(0.0, 0.0),
748            Complex64::new(0.0, 0.0),
749            Complex64::new(0.0, 0.0),
750            Complex64::new(0.0, 0.0),
751            Complex64::new(0.0, 0.0),
752        ];
753        let mut output = vec![Complex64::default(); 8];
754
755        // Execute the plan
756        executor
757            .execute(&input, &mut output)
758            .expect("Operation failed");
759
760        // Check that the output makes sense for this input
761        // (an impulse at the beginning should have a flat frequency response)
762        for val in &output {
763            // The magnitude should be approximately the same for all frequencies
764            let magnitude = (val.re.powi(2) + val.im.powi(2)).sqrt();
765            assert!((magnitude - 1.0).abs() < 1e-10);
766        }
767    }
768
769    #[test]
770    fn test_plan_builder() {
771        let builder = PlanBuilder::new()
772            .shape(&[16])
773            .forward(true)
774            .strategy(PlanningStrategy::AlwaysNew)
775            .measure_performance(true);
776
777        let plan = builder.build().expect("Operation failed");
778
779        assert_eq!(plan.shape(), &[16]);
780    }
781
782    #[test]
783    fn test_serialization() {
784        // Create a temporary directory for test
785        let temp_dir = tempdir().expect("Operation failed");
786        let db_path = temp_dir.path().join("test_plan_db.json");
787
788        // Create a planner with serialization enabled
789        let mut config = PlanningConfig::default();
790        config.serialized_db_path = Some(db_path.to_str().expect("Operation failed").to_string());
791        config.strategy = PlanningStrategy::SerializedFirst;
792
793        let mut planner = AdvancedFftPlanner::with_config(config);
794
795        // Create a plan
796        let shape = vec![32];
797        let _ = planner
798            .plan_fft(&shape, true, PlannerBackend::default())
799            .expect("Operation failed");
800
801        // Save plans
802        planner.save_plans().expect("Operation failed");
803
804        // Check that the file exists
805        assert!(db_path.exists());
806    }
807
808    #[test]
809    fn test_global_planner() {
810        // Get the global planner
811        let planner = get_global_planner();
812
813        // Create a plan with the global planner
814        let mut planner_guard = planner.lock().expect("Operation failed");
815        let shape = vec![64];
816        let plan = planner_guard
817            .plan_fft(&shape, true, PlannerBackend::default())
818            .expect("Operation failed");
819
820        assert_eq!(plan.shape(), &shape);
821    }
822
823    #[test]
824    fn test_ahead_of_time_planning() {
825        // Create a temporary directory for test
826        let temp_dir = tempdir().expect("Operation failed");
827        let db_path = temp_dir.path().join("ahead_of_time.json");
828
829        // Plan ahead of time for some common sizes
830        let sizes = [8, 16, 32, 64];
831        plan_ahead_of_time(&sizes, Some(db_path.to_str().expect("Operation failed")))
832            .expect("Operation failed");
833
834        // Check that the file exists
835        assert!(db_path.exists());
836    }
837}