Skip to main content

math_audio_optimisation/
parallel_eval.rs

1use ndarray::{Array1, Array2};
2use rayon::prelude::*;
3use std::sync::Arc;
4
5/// Parallel evaluation configuration
6#[derive(Debug, Clone)]
7pub struct ParallelConfig {
8    /// Enable parallel evaluation
9    pub enabled: bool,
10    /// Number of threads to use (None = use rayon default)
11    pub num_threads: Option<usize>,
12}
13
14impl Default for ParallelConfig {
15    fn default() -> Self {
16        Self {
17            enabled: true,
18            num_threads: None, // Use rayon's default (typically num_cpus)
19        }
20    }
21}
22
23/// Evaluate a population in parallel
24///
25/// # Arguments
26/// * `population` - 2D array where each row is an individual
27/// * `eval_fn` - Function to evaluate each individual
28/// * `config` - Parallel configuration
29///
30/// # Returns
31/// Array of fitness values for each individual
32pub fn evaluate_population_parallel<F>(
33    population: &Array2<f64>,
34    eval_fn: Arc<F>,
35    config: &ParallelConfig,
36) -> Array1<f64>
37where
38    F: Fn(&Array1<f64>) -> f64 + Send + Sync,
39{
40    let npop = population.nrows();
41
42    if !config.enabled || npop < 4 {
43        // Sequential evaluation for small populations or when disabled
44        let mut energies = Array1::zeros(npop);
45        for i in 0..npop {
46            let individual = population.row(i).to_owned();
47            energies[i] = eval_fn(&individual);
48        }
49        return energies;
50    }
51
52    // Always use global thread pool (configured once in solver)
53    let results = (0..npop)
54        .into_par_iter()
55        .map(|i| {
56            let individual = population.row(i).to_owned();
57            eval_fn(&individual)
58        })
59        .collect::<Vec<f64>>();
60
61    Array1::from_vec(results)
62}
63
64/// Evaluate trials in parallel for differential evolution
65///
66/// This function evaluates multiple trial vectors in parallel, which is useful
67/// during the main DE loop where we generate and evaluate one trial per individual.
68///
69/// # Arguments
70/// * `trials` - Vector of trial vectors to evaluate
71/// * `eval_fn` - Function to evaluate each trial
72/// * `config` - Parallel configuration
73///
74/// # Returns
75/// Vector of fitness values for each trial
76pub fn evaluate_trials_parallel<F>(
77    trials: &[Array1<f64>],
78    eval_fn: Arc<F>,
79    config: &ParallelConfig,
80) -> Vec<f64>
81where
82    F: Fn(&Array1<f64>) -> f64 + Send + Sync,
83{
84    if !config.enabled || trials.len() < 4 {
85        // Sequential evaluation for small batches or when disabled
86        return trials.iter().map(|trial| eval_fn(trial)).collect();
87    }
88
89    // Always use global thread pool (configured once in solver)
90    trials.par_iter().map(|trial| eval_fn(trial)).collect()
91}
92
93/// Structure to batch evaluate individuals with their indices.
94pub struct IndexedEvaluation {
95    /// Index of the individual in the population.
96    pub index: usize,
97    /// The individual's solution vector.
98    pub individual: Array1<f64>,
99    /// The computed fitness value.
100    pub fitness: f64,
101}
102
103/// Evaluate population with indices preserved for tracking
104pub fn evaluate_population_indexed<F>(
105    population: &Array2<f64>,
106    eval_fn: Arc<F>,
107    config: &ParallelConfig,
108) -> Vec<IndexedEvaluation>
109where
110    F: Fn(&Array1<f64>) -> f64 + Send + Sync,
111{
112    let npop = population.nrows();
113
114    if !config.enabled || npop < 4 {
115        // Sequential evaluation
116        let mut results = Vec::with_capacity(npop);
117        for i in 0..npop {
118            let individual = population.row(i).to_owned();
119            let fitness = eval_fn(&individual);
120            results.push(IndexedEvaluation {
121                index: i,
122                individual,
123                fitness,
124            });
125        }
126        return results;
127    }
128
129    // Parallel evaluation (global thread pool)
130    (0..npop)
131        .into_par_iter()
132        .map(|i| {
133            let individual = population.row(i).to_owned();
134            let fitness = eval_fn(&individual);
135            IndexedEvaluation {
136                index: i,
137                individual,
138                fitness,
139            }
140        })
141        .collect()
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_parallel_evaluation() {
150        // Simple quadratic function
151        let eval_fn = Arc::new(|x: &Array1<f64>| -> f64 { x.iter().map(|&xi| xi * xi).sum() });
152
153        // Create a small population
154        let mut population = Array2::zeros((10, 3));
155        for i in 0..10 {
156            for j in 0..3 {
157                population[[i, j]] = (i as f64) * 0.1 + (j as f64) * 0.01;
158            }
159        }
160
161        // Test with parallel enabled
162        let config = ParallelConfig {
163            enabled: true,
164            num_threads: Some(2),
165        };
166        let energies = evaluate_population_parallel(&population, eval_fn.clone(), &config);
167
168        // Verify results
169        assert_eq!(energies.len(), 10);
170        for i in 0..10 {
171            let expected = population.row(i).iter().map(|&x| x * x).sum::<f64>();
172            assert!((energies[i] - expected).abs() < 1e-10);
173        }
174
175        // Test with parallel disabled
176        let config_seq = ParallelConfig {
177            enabled: false,
178            num_threads: None,
179        };
180        let energies_seq = evaluate_population_parallel(&population, eval_fn, &config_seq);
181
182        // Results should be identical
183        for i in 0..10 {
184            assert_eq!(energies[i], energies_seq[i]);
185        }
186    }
187
188    #[test]
189    fn test_indexed_evaluation() {
190        let eval_fn = Arc::new(|x: &Array1<f64>| -> f64 { x.iter().sum() });
191
192        let mut population = Array2::zeros((5, 2));
193        for i in 0..5 {
194            population[[i, 0]] = i as f64;
195            population[[i, 1]] = (i * 2) as f64;
196        }
197
198        let config = ParallelConfig::default();
199        let results = evaluate_population_indexed(&population, eval_fn, &config);
200
201        assert_eq!(results.len(), 5);
202        for result in results {
203            let expected = population.row(result.index).sum();
204            assert_eq!(result.fitness, expected);
205        }
206    }
207}