Skip to main content

scirs2_optimize/
distributed.rs

1//! Distributed optimization using MPI for large-scale parallel computation
2//!
3//! This module provides distributed optimization algorithms that can scale across
4//! multiple nodes using Message Passing Interface (MPI), enabling optimization
5//! of computationally expensive problems across compute clusters.
6
7use crate::error::{ScirsError, ScirsResult};
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
9use scirs2_core::random::RngExt;
10use scirs2_core::Rng;
11use statrs::statistics::Statistics;
12use std::any::Any;
13use std::sync::{Arc, Barrier, Condvar, Mutex};
14use std::thread;
15
16/// MPI interface abstraction for distributed optimization
17///
18/// Generic methods require `T: 'static` (in addition to the obvious
19/// `Clone + Send + Sync`) so that implementations backed by real threads
20/// (see [`ThreadRankMpi`]) can move data between ranks via `Box<dyn Any +
21/// Send>` type erasure. Every real caller in this module only ever
22/// instantiates `T = f64`, so this is not a practical restriction.
23pub trait MPIInterface {
24    /// Get the rank of this process
25    fn rank(&self) -> i32;
26
27    /// Get the total number of processes
28    fn size(&self) -> i32;
29
30    /// Broadcast data from root to all processes
31    fn broadcast<T>(&self, data: &mut [T], root: i32) -> ScirsResult<()>
32    where
33        T: Clone + Send + Sync + 'static;
34
35    /// Gather data from all processes to root
36    fn gather<T>(&self, send_data: &[T], recv_data: Option<&mut [T]>, root: i32) -> ScirsResult<()>
37    where
38        T: Clone + Send + Sync + 'static;
39
40    /// All-to-all reduction operation
41    fn allreduce<T>(
42        &self,
43        send_data: &[T],
44        recv_data: &mut [T],
45        op: ReductionOp,
46    ) -> ScirsResult<()>
47    where
48        T: Clone + Send + Sync + 'static + std::ops::Add<Output = T> + PartialOrd;
49
50    /// Barrier synchronization
51    fn barrier(&self) -> ScirsResult<()>;
52
53    /// Send data to specific process
54    fn send<T>(&self, data: &[T], dest: i32, tag: i32) -> ScirsResult<()>
55    where
56        T: Clone + Send + Sync + 'static;
57
58    /// Receive data from specific process
59    fn recv<T>(&self, data: &mut [T], source: i32, tag: i32) -> ScirsResult<()>
60    where
61        T: Clone + Send + Sync + 'static;
62}
63
64/// Reduction operations for MPI
65#[derive(Debug, Clone, Copy)]
66pub enum ReductionOp {
67    Sum,
68    Min,
69    Max,
70    Prod,
71}
72
73/// Configuration for distributed optimization
74#[derive(Debug, Clone)]
75pub struct DistributedConfig {
76    /// Strategy for distributing work
77    pub distribution_strategy: DistributionStrategy,
78    /// Load balancing configuration
79    pub load_balancing: LoadBalancingConfig,
80    /// Communication optimization settings
81    pub communication: CommunicationConfig,
82    /// Fault tolerance configuration
83    pub fault_tolerance: FaultToleranceConfig,
84}
85
86impl Default for DistributedConfig {
87    fn default() -> Self {
88        Self {
89            distribution_strategy: DistributionStrategy::DataParallel,
90            load_balancing: LoadBalancingConfig::default(),
91            communication: CommunicationConfig::default(),
92            fault_tolerance: FaultToleranceConfig::default(),
93        }
94    }
95}
96
97/// Work distribution strategies
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum DistributionStrategy {
100    /// Distribute data across processes
101    DataParallel,
102    /// Distribute parameters across processes
103    ModelParallel,
104    /// Hybrid data and model parallelism
105    Hybrid,
106    /// Master-worker with dynamic task assignment
107    MasterWorker,
108}
109
110/// Load balancing configuration
111#[derive(Debug, Clone)]
112pub struct LoadBalancingConfig {
113    /// Whether to enable dynamic load balancing
114    pub dynamic: bool,
115    /// Threshold for load imbalance (0.0 to 1.0)
116    pub imbalance_threshold: f64,
117    /// Rebalancing interval (in iterations)
118    pub rebalance_interval: usize,
119}
120
121impl Default for LoadBalancingConfig {
122    fn default() -> Self {
123        Self {
124            dynamic: true,
125            imbalance_threshold: 0.2,
126            rebalance_interval: 100,
127        }
128    }
129}
130
131/// Communication optimization configuration
132#[derive(Debug, Clone)]
133pub struct CommunicationConfig {
134    /// Whether to use asynchronous communication
135    pub async_communication: bool,
136    /// Communication buffer size
137    pub buffer_size: usize,
138    /// Compression for large data transfers
139    pub use_compression: bool,
140    /// Overlap computation with communication
141    pub overlap_computation: bool,
142}
143
144impl Default for CommunicationConfig {
145    fn default() -> Self {
146        Self {
147            async_communication: true,
148            buffer_size: 1024 * 1024, // 1MB
149            use_compression: false,
150            overlap_computation: true,
151        }
152    }
153}
154
155/// Fault tolerance configuration
156#[derive(Debug, Clone)]
157pub struct FaultToleranceConfig {
158    /// Enable checkpointing
159    pub checkpointing: bool,
160    /// Checkpoint interval (in iterations)
161    pub checkpoint_interval: usize,
162    /// Maximum number of retries for failed operations
163    pub max_retries: usize,
164    /// Timeout for MPI operations (in seconds)
165    pub timeout: f64,
166}
167
168impl Default for FaultToleranceConfig {
169    fn default() -> Self {
170        Self {
171            checkpointing: false,
172            checkpoint_interval: 1000,
173            max_retries: 3,
174            timeout: 30.0,
175        }
176    }
177}
178
179/// Distributed optimization context
180pub struct DistributedOptimizationContext<M: MPIInterface> {
181    mpi: M,
182    config: DistributedConfig,
183    rank: i32,
184    size: i32,
185    work_distribution: WorkDistribution,
186    performance_stats: DistributedStats,
187}
188
189impl<M: MPIInterface> DistributedOptimizationContext<M> {
190    /// Create a new distributed optimization context
191    pub fn new(mpi: M, config: DistributedConfig) -> Self {
192        let rank = mpi.rank();
193        let size = mpi.size();
194        let work_distribution = WorkDistribution::new(rank, size, config.distribution_strategy);
195
196        Self {
197            mpi,
198            config,
199            rank,
200            size,
201            work_distribution,
202            performance_stats: DistributedStats::new(),
203        }
204    }
205
206    /// Get the MPI rank of this process
207    pub fn rank(&self) -> i32 {
208        self.rank
209    }
210
211    /// Get the total number of MPI processes
212    pub fn size(&self) -> i32 {
213        self.size
214    }
215
216    /// Check if this is the master process
217    pub fn is_master(&self) -> bool {
218        self.rank == 0
219    }
220
221    /// Distribute work among processes
222    pub fn distribute_work(&mut self, total_work: usize) -> WorkAssignment {
223        self.work_distribution.assign_work(total_work)
224    }
225
226    /// Synchronize all processes
227    pub fn synchronize(&self) -> ScirsResult<()> {
228        self.mpi.barrier()
229    }
230
231    /// Broadcast parameters from master to all workers
232    pub fn broadcast_parameters(&self, params: &mut Array1<f64>) -> ScirsResult<()> {
233        let data = params.as_slice_mut().expect("Operation failed");
234        self.mpi.broadcast(data, 0)
235    }
236
237    /// Gather results from all workers to master
238    pub fn gather_results(&self, local_result: &Array1<f64>) -> ScirsResult<Option<Array2<f64>>> {
239        if self.is_master() {
240            let total_size = local_result.len() * self.size as usize;
241            let mut gathered_data = vec![0.0; total_size];
242            self.mpi.gather(
243                local_result.as_slice().expect("Operation failed"),
244                Some(&mut gathered_data),
245                0,
246            )?;
247
248            // Reshape into 2D array
249            let result =
250                Array2::from_shape_vec((self.size as usize, local_result.len()), gathered_data)
251                    .map_err(|e| {
252                        ScirsError::InvalidInput(scirs2_core::error::ErrorContext::new(format!(
253                            "Failed to reshape gathered data: {}",
254                            e
255                        )))
256                    })?;
257            Ok(Some(result))
258        } else {
259            self.mpi
260                .gather(local_result.as_slice().expect("Operation failed"), None, 0)?;
261            Ok(None)
262        }
263    }
264
265    /// Perform all-reduce operation (sum)
266    pub fn allreduce_sum(&self, local_data: &Array1<f64>) -> ScirsResult<Array1<f64>> {
267        let mut result = Array1::zeros(local_data.len());
268        self.mpi.allreduce(
269            local_data.as_slice().expect("Operation failed"),
270            result.as_slice_mut().expect("Operation failed"),
271            ReductionOp::Sum,
272        )?;
273        Ok(result)
274    }
275
276    /// Perform all-reduce operation (element-wise minimum)
277    pub fn allreduce_min(&self, local_data: &Array1<f64>) -> ScirsResult<Array1<f64>> {
278        let mut result = Array1::zeros(local_data.len());
279        self.mpi.allreduce(
280            local_data.as_slice().expect("Operation failed"),
281            result.as_slice_mut().expect("Operation failed"),
282            ReductionOp::Min,
283        )?;
284        Ok(result)
285    }
286
287    /// Perform all-reduce operation (element-wise maximum)
288    pub fn allreduce_max(&self, local_data: &Array1<f64>) -> ScirsResult<Array1<f64>> {
289        let mut result = Array1::zeros(local_data.len());
290        self.mpi.allreduce(
291            local_data.as_slice().expect("Operation failed"),
292            result.as_slice_mut().expect("Operation failed"),
293            ReductionOp::Max,
294        )?;
295        Ok(result)
296    }
297
298    /// Broadcast a vector from an arbitrary root rank to all processes
299    pub fn broadcast_from(&self, params: &mut Array1<f64>, root: i32) -> ScirsResult<()> {
300        let data = params.as_slice_mut().expect("Operation failed");
301        self.mpi.broadcast(data, root)
302    }
303
304    /// Select the globally-best candidate across the whole communicator.
305    ///
306    /// Given each process's local best point and its objective value, returns
307    /// the best `(point, value)` pair over all processes. The global-best value
308    /// is obtained with a `Min` all-reduce; the owning rank (lowest rank on
309    /// ties) is identified by a second `Min` all-reduce over rank indices, and
310    /// its point is broadcast so every process agrees on the winning candidate.
311    pub fn select_global_best(
312        &self,
313        local_best: &Array1<f64>,
314        local_value: f64,
315    ) -> ScirsResult<(Array1<f64>, f64)> {
316        if self.size <= 1 {
317            return Ok((local_best.to_owned(), local_value));
318        }
319
320        // Global-best objective value.
321        let value_buf = Array1::from_elem(1, local_value);
322        let global_value = self.allreduce_min(&value_buf)?[0];
323
324        // Identify the owning rank: holders of the global-best value propose
325        // their own rank, everyone else proposes a sentinel larger than any
326        // valid rank. The minimum proposal is the lowest rank holding the best.
327        let owner_proposal = if local_value <= global_value {
328            self.rank as f64
329        } else {
330            self.size as f64
331        };
332        let owner_buf = Array1::from_elem(1, owner_proposal);
333        let owner_rank = self.allreduce_min(&owner_buf)?[0] as i32;
334
335        // Broadcast the winning point from its owner to all processes.
336        let mut winner = local_best.to_owned();
337        self.broadcast_from(&mut winner, owner_rank)?;
338
339        Ok((winner, global_value))
340    }
341
342    /// Exchange a vector around a unidirectional process ring.
343    ///
344    /// Sends `outgoing` to rank `(rank + 1) % size` and returns the vector
345    /// received from rank `(rank - 1 + size) % size`. A deadlock-free ordering
346    /// is used (rank 0 receives before sending, every other rank sends before
347    /// receiving), so it is safe with blocking point-to-point primitives for any
348    /// communicator size. Returns `None` when there is only a single process.
349    pub fn ring_exchange(&self, outgoing: &Array1<f64>) -> ScirsResult<Option<Array1<f64>>> {
350        if self.size <= 1 {
351            return Ok(None);
352        }
353
354        let next = (self.rank + 1).rem_euclid(self.size);
355        let prev = (self.rank - 1).rem_euclid(self.size);
356        let tag = 0;
357        let send = outgoing.as_slice().expect("Operation failed");
358        let mut incoming = vec![0.0_f64; outgoing.len()];
359
360        if self.rank == 0 {
361            // Rank 0 receives first to break the cyclic dependency.
362            self.mpi.recv(&mut incoming, prev, tag)?;
363            self.mpi.send(send, next, tag)?;
364        } else {
365            self.mpi.send(send, next, tag)?;
366            self.mpi.recv(&mut incoming, prev, tag)?;
367        }
368
369        Ok(Some(Array1::from_vec(incoming)))
370    }
371
372    /// Get performance statistics
373    pub fn stats(&self) -> &DistributedStats {
374        &self.performance_stats
375    }
376}
377
378/// Work distribution manager
379struct WorkDistribution {
380    rank: i32,
381    size: i32,
382    strategy: DistributionStrategy,
383}
384
385impl WorkDistribution {
386    fn new(rank: i32, size: i32, strategy: DistributionStrategy) -> Self {
387        Self {
388            rank,
389            size,
390            strategy,
391        }
392    }
393
394    fn assign_work(&self, total_work: usize) -> WorkAssignment {
395        match self.strategy {
396            DistributionStrategy::DataParallel => self.data_parallel_assignment(total_work),
397            DistributionStrategy::ModelParallel => self.model_parallel_assignment(total_work),
398            DistributionStrategy::Hybrid => self.hybrid_assignment(total_work),
399            DistributionStrategy::MasterWorker => self.master_worker_assignment(total_work),
400        }
401    }
402
403    fn data_parallel_assignment(&self, total_work: usize) -> WorkAssignment {
404        let work_per_process = total_work / self.size as usize;
405        let remainder = total_work % self.size as usize;
406
407        let start = self.rank as usize * work_per_process + (self.rank as usize).min(remainder);
408        let extra = if (self.rank as usize) < remainder {
409            1
410        } else {
411            0
412        };
413        let count = work_per_process + extra;
414
415        WorkAssignment {
416            start_index: start,
417            count,
418            strategy: DistributionStrategy::DataParallel,
419        }
420    }
421
422    fn model_parallel_assignment(&self, total_work: usize) -> WorkAssignment {
423        // For model parallelism, each process handles different parameters
424        WorkAssignment {
425            start_index: 0,
426            count: total_work, // Each process sees all data but handles different parameters
427            strategy: DistributionStrategy::ModelParallel,
428        }
429    }
430
431    fn hybrid_assignment(&self, total_work: usize) -> WorkAssignment {
432        // Hybrid data/model parallelism: arrange the processes into a 2-D grid
433        // of `data_groups` x `model_groups`. The data dimension partitions the
434        // work range (as in data parallelism), while processes that share a
435        // data range form a model-parallel group splitting the parameter space.
436        //
437        // For a single process, or a communicator that cannot be factored into
438        // more than one model group (e.g. a prime size), this reduces exactly
439        // to data parallelism over all processes.
440        let size = (self.size.max(1)) as usize;
441
442        // Choose the number of model-parallel groups as the largest divisor of
443        // `size` not exceeding sqrt(size); this yields a balanced process grid.
444        let mut model_groups = 1usize;
445        let mut divisor = 2usize;
446        while divisor * divisor <= size {
447            if size % divisor == 0 {
448                model_groups = divisor;
449            }
450            divisor += 1;
451        }
452        let data_groups = size / model_groups;
453
454        // Coordinate of this process along the data dimension of the grid.
455        let rank = (self.rank.max(0)) as usize;
456        let data_coord = (rank / model_groups).min(data_groups.saturating_sub(1));
457
458        // Partition the work range across the data groups.
459        let work_per_group = total_work / data_groups;
460        let remainder = total_work % data_groups;
461        let start = data_coord * work_per_group + data_coord.min(remainder);
462        let extra = if data_coord < remainder { 1 } else { 0 };
463        let count = work_per_group + extra;
464
465        WorkAssignment {
466            start_index: start,
467            count,
468            strategy: DistributionStrategy::Hybrid,
469        }
470    }
471
472    fn master_worker_assignment(&self, total_work: usize) -> WorkAssignment {
473        if self.rank == 0 {
474            // Master coordinates but may not do computation
475            WorkAssignment {
476                start_index: 0,
477                count: 0,
478                strategy: DistributionStrategy::MasterWorker,
479            }
480        } else {
481            // Workers split the work
482            let worker_count = self.size - 1;
483            let work_per_worker = total_work / worker_count as usize;
484            let remainder = total_work % worker_count as usize;
485            let worker_rank = self.rank - 1;
486
487            let start =
488                worker_rank as usize * work_per_worker + (worker_rank as usize).min(remainder);
489            let extra = if (worker_rank as usize) < remainder {
490                1
491            } else {
492                0
493            };
494            let count = work_per_worker + extra;
495
496            WorkAssignment {
497                start_index: start,
498                count,
499                strategy: DistributionStrategy::MasterWorker,
500            }
501        }
502    }
503}
504
505/// Work assignment for a process
506#[derive(Debug, Clone)]
507pub struct WorkAssignment {
508    /// Starting index for this process
509    pub start_index: usize,
510    /// Number of work items for this process
511    pub count: usize,
512    /// Distribution strategy used
513    pub strategy: DistributionStrategy,
514}
515
516impl WorkAssignment {
517    /// Get the range of indices assigned to this process
518    pub fn range(&self) -> std::ops::Range<usize> {
519        self.start_index..(self.start_index + self.count)
520    }
521
522    /// Check if this assignment is empty
523    pub fn is_empty(&self) -> bool {
524        self.count == 0
525    }
526}
527
528/// Distributed optimization algorithms
529pub mod algorithms {
530    use super::*;
531    use crate::result::OptimizeResults;
532
533    /// Distributed differential evolution
534    pub struct DistributedDifferentialEvolution<M: MPIInterface> {
535        context: DistributedOptimizationContext<M>,
536        population_size: usize,
537        max_nit: usize,
538        f_scale: f64,
539        crossover_rate: f64,
540    }
541
542    impl<M: MPIInterface> DistributedDifferentialEvolution<M> {
543        /// Create a new distributed differential evolution optimizer
544        pub fn new(
545            context: DistributedOptimizationContext<M>,
546            population_size: usize,
547            max_nit: usize,
548        ) -> Self {
549            Self {
550                context,
551                population_size,
552                max_nit,
553                f_scale: 0.8,
554                crossover_rate: 0.7,
555            }
556        }
557
558        /// Set mutation parameters
559        pub fn with_parameters(mut self, f_scale: f64, crossover_rate: f64) -> Self {
560            self.f_scale = f_scale;
561            self.crossover_rate = crossover_rate;
562            self
563        }
564
565        /// Optimize function using distributed differential evolution
566        pub fn optimize<F>(
567            &mut self,
568            function: F,
569            bounds: &[(f64, f64)],
570        ) -> ScirsResult<OptimizeResults<f64>>
571        where
572            F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
573        {
574            let dims = bounds.len();
575
576            // Initialize local population
577            let local_pop_size = self.population_size / self.context.size() as usize;
578            let mut local_population = self.initialize_local_population(local_pop_size, bounds)?;
579            let mut local_fitness = self.evaluate_local_population(&function, &local_population)?;
580
581            // Find global best across all processes
582            let mut global_best = self.find_global_best(&local_population, &local_fitness)?;
583            let mut global_best_fitness = global_best.1;
584
585            let mut total_evaluations = self.population_size;
586
587            for iteration in 0..self.max_nit {
588                // Generate trial population
589                let trial_population = self.generate_trial_population(&local_population)?;
590                let trial_fitness = self.evaluate_local_population(&function, &trial_population)?;
591
592                // Selection
593                self.selection(
594                    &mut local_population,
595                    &mut local_fitness,
596                    &trial_population,
597                    &trial_fitness,
598                );
599
600                total_evaluations += local_pop_size;
601
602                // Exchange information between processes
603                if iteration % 10 == 0 {
604                    let new_global_best =
605                        self.find_global_best(&local_population, &local_fitness)?;
606                    if new_global_best.1 < global_best_fitness {
607                        global_best = new_global_best;
608                        global_best_fitness = global_best.1;
609                    }
610
611                    // Migration between processes
612                    self.migrate_individuals(&mut local_population, &mut local_fitness)?;
613                }
614
615                // Convergence check (simplified)
616                if iteration % 50 == 0 {
617                    let convergence = self.check_convergence(&local_fitness)?;
618                    if convergence {
619                        break;
620                    }
621                }
622            }
623
624            // Final global best search
625            let final_best = self.find_global_best(&local_population, &local_fitness)?;
626            if final_best.1 < global_best_fitness {
627                global_best = final_best.clone();
628                global_best_fitness = final_best.1;
629            }
630
631            Ok(OptimizeResults::<f64> {
632                x: global_best.0,
633                fun: global_best_fitness,
634                success: true,
635                message: "Distributed differential evolution completed".to_string(),
636                nit: self.max_nit,
637                nfev: total_evaluations,
638                ..OptimizeResults::default()
639            })
640        }
641
642        fn initialize_local_population(
643            &self,
644            local_size: usize,
645            bounds: &[(f64, f64)],
646        ) -> ScirsResult<Array2<f64>> {
647            let mut rng = scirs2_core::random::rng();
648
649            let dims = bounds.len();
650            let mut population = Array2::zeros((local_size, dims));
651
652            for i in 0..local_size {
653                for j in 0..dims {
654                    let (low, high) = bounds[j];
655                    population[[i, j]] = rng.random_range(low..=high);
656                }
657            }
658
659            Ok(population)
660        }
661
662        fn evaluate_local_population<F>(
663            &self,
664            function: &F,
665            population: &Array2<f64>,
666        ) -> ScirsResult<Array1<f64>>
667        where
668            F: Fn(&ArrayView1<f64>) -> f64,
669        {
670            let mut fitness = Array1::zeros(population.nrows());
671
672            for i in 0..population.nrows() {
673                let individual = population.row(i);
674                fitness[i] = function(&individual);
675            }
676
677            Ok(fitness)
678        }
679
680        fn find_global_best(
681            &mut self,
682            local_population: &Array2<f64>,
683            local_fitness: &Array1<f64>,
684        ) -> ScirsResult<(Array1<f64>, f64)> {
685            // Find local best
686            let mut best_idx = 0;
687            let mut best_fitness = local_fitness[0];
688            for (i, &fitness) in local_fitness.iter().enumerate() {
689                if fitness < best_fitness {
690                    best_fitness = fitness;
691                    best_idx = i;
692                }
693            }
694
695            let local_best = local_population.row(best_idx).to_owned();
696
697            // Reduce to the true global best across all processes: the global
698            // minimum fitness and a broadcast of the individual that owns it.
699            self.context.select_global_best(&local_best, best_fitness)
700        }
701
702        fn generate_trial_population(&self, population: &Array2<f64>) -> ScirsResult<Array2<f64>> {
703            let mut rng = scirs2_core::random::rng();
704
705            let (pop_size, dims) = population.dim();
706            let mut trial_population = Array2::zeros((pop_size, dims));
707
708            for i in 0..pop_size {
709                // Select three random individuals
710                let mut indices = Vec::new();
711                while indices.len() < 3 {
712                    let idx = rng.random_range(0..pop_size);
713                    if idx != i && !indices.contains(&idx) {
714                        indices.push(idx);
715                    }
716                }
717
718                let a = indices[0];
719                let b = indices[1];
720                let c = indices[2];
721
722                // Mutation and crossover
723                let j_rand = rng.random_range(0..dims);
724                for j in 0..dims {
725                    if rng.random::<f64>() < self.crossover_rate || j == j_rand {
726                        trial_population[[i, j]] = population[[a, j]]
727                            + self.f_scale * (population[[b, j]] - population[[c, j]]);
728                    } else {
729                        trial_population[[i, j]] = population[[i, j]];
730                    }
731                }
732            }
733
734            Ok(trial_population)
735        }
736
737        fn selection(
738            &self,
739            population: &mut Array2<f64>,
740            fitness: &mut Array1<f64>,
741            trial_population: &Array2<f64>,
742            trial_fitness: &Array1<f64>,
743        ) {
744            for i in 0..population.nrows() {
745                if trial_fitness[i] <= fitness[i] {
746                    for j in 0..population.ncols() {
747                        population[[i, j]] = trial_population[[i, j]];
748                    }
749                    fitness[i] = trial_fitness[i];
750                }
751            }
752        }
753
754        fn migrate_individuals(
755            &mut self,
756            population: &mut Array2<f64>,
757            fitness: &mut Array1<f64>,
758        ) -> ScirsResult<()> {
759            // Island-model migration around a unidirectional process ring: send
760            // this island's best individual to the next process and adopt the
761            // migrant received from the previous process if it improves on the
762            // local worst individual.
763            if self.context.size() <= 1 {
764                return Ok(());
765            }
766
767            let best_idx = fitness
768                .iter()
769                .enumerate()
770                .min_by(|(_, a), (_, b)| a.partial_cmp(b).expect("Operation failed"))
771                .map(|(i, _)| i)
772                .unwrap_or(0);
773
774            // Pack the best individual together with its fitness so the migrant
775            // arrives with a valid objective value (the islands share the same
776            // objective, so the value transfers without re-evaluation).
777            let dims = population.ncols();
778            let mut payload = population.row(best_idx).to_vec();
779            payload.push(fitness[best_idx]);
780            let payload = Array1::from_vec(payload);
781
782            if let Some(received) = self.context.ring_exchange(&payload)? {
783                let migrant_fitness = received[dims];
784
785                // Replace the local worst individual when the migrant is better.
786                let worst_idx = fitness
787                    .iter()
788                    .enumerate()
789                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("Operation failed"))
790                    .map(|(i, _)| i)
791                    .unwrap_or(0);
792
793                if migrant_fitness < fitness[worst_idx] {
794                    for j in 0..dims {
795                        population[[worst_idx, j]] = received[j];
796                    }
797                    fitness[worst_idx] = migrant_fitness;
798                }
799            }
800
801            Ok(())
802        }
803
804        fn check_convergence(&mut self, local_fitness: &Array1<f64>) -> ScirsResult<bool> {
805            let mean = local_fitness.view().mean();
806            let variance = local_fitness
807                .iter()
808                .map(|&x| (x - mean).powi(2))
809                .sum::<f64>()
810                / local_fitness.len() as f64;
811
812            let std_dev = variance.sqrt();
813
814            // The stop decision must be identical on every rank: each rank's
815            // local population is independently randomly initialized, so
816            // local std_dev can cross the threshold on some ranks well before
817            // others. If ranks disagreed here, one would `break` out of the
818            // main loop while others kept calling `find_global_best`/
819            // `migrate_individuals` every 10 iterations — permanently
820            // desynchronizing the collective call sequence and deadlocking
821            // every remaining rank on its next barrier wait. Agreeing on the
822            // worst (largest) std_dev across all ranks keeps the decision,
823            // and therefore the collective call count, identical everywhere.
824            let global_max_std_dev = self.context.allreduce_max(&Array1::from_elem(1, std_dev))?[0];
825
826            Ok(global_max_std_dev < 1e-12)
827        }
828    }
829
830    /// Distributed particle swarm optimization
831    pub struct DistributedParticleSwarm<M: MPIInterface> {
832        context: DistributedOptimizationContext<M>,
833        swarm_size: usize,
834        max_nit: usize,
835        w: f64,  // Inertia weight
836        c1: f64, // Cognitive parameter
837        c2: f64, // Social parameter
838    }
839
840    impl<M: MPIInterface> DistributedParticleSwarm<M> {
841        /// Create a new distributed particle swarm optimizer
842        pub fn new(
843            context: DistributedOptimizationContext<M>,
844            swarm_size: usize,
845            max_nit: usize,
846        ) -> Self {
847            Self {
848                context,
849                swarm_size,
850                max_nit,
851                w: 0.729,
852                c1: 1.49445,
853                c2: 1.49445,
854            }
855        }
856
857        /// Set PSO parameters
858        pub fn with_parameters(mut self, w: f64, c1: f64, c2: f64) -> Self {
859            self.w = w;
860            self.c1 = c1;
861            self.c2 = c2;
862            self
863        }
864
865        /// Optimize function using distributed particle swarm optimization
866        pub fn optimize<F>(
867            &mut self,
868            function: F,
869            bounds: &[(f64, f64)],
870        ) -> ScirsResult<OptimizeResults<f64>>
871        where
872            F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
873        {
874            let dims = bounds.len();
875            let local_swarm_size = self.swarm_size / self.context.size() as usize;
876
877            // Initialize local swarm
878            let mut positions = self.initialize_positions(local_swarm_size, bounds)?;
879            let mut velocities = Array2::zeros((local_swarm_size, dims));
880            let mut personal_best = positions.clone();
881            let mut personal_best_fitness = self.evaluate_swarm(&function, &positions)?;
882
883            // Find global best
884            let mut global_best = self.find_global_best(&personal_best, &personal_best_fitness)?;
885            let mut global_best_fitness = global_best.1;
886
887            let mut function_evaluations = local_swarm_size;
888
889            for iteration in 0..self.max_nit {
890                // Update swarm
891                self.update_swarm(
892                    &mut positions,
893                    &mut velocities,
894                    &personal_best,
895                    &global_best.0,
896                    bounds,
897                )?;
898
899                // Evaluate new positions
900                let fitness = self.evaluate_swarm(&function, &positions)?;
901                function_evaluations += local_swarm_size;
902
903                // Update personal bests
904                for i in 0..local_swarm_size {
905                    if fitness[i] < personal_best_fitness[i] {
906                        personal_best_fitness[i] = fitness[i];
907                        for j in 0..dims {
908                            personal_best[[i, j]] = positions[[i, j]];
909                        }
910                    }
911                }
912
913                // Update global best
914                if iteration % 10 == 0 {
915                    let new_global_best =
916                        self.find_global_best(&personal_best, &personal_best_fitness)?;
917                    if new_global_best.1 < global_best_fitness {
918                        global_best = new_global_best;
919                        global_best_fitness = global_best.1;
920                    }
921                }
922            }
923
924            Ok(OptimizeResults::<f64> {
925                x: global_best.0,
926                fun: global_best_fitness,
927                success: true,
928                message: "Distributed particle swarm optimization completed".to_string(),
929                nit: self.max_nit,
930                nfev: function_evaluations,
931                ..OptimizeResults::default()
932            })
933        }
934
935        fn initialize_positions(
936            &self,
937            local_size: usize,
938            bounds: &[(f64, f64)],
939        ) -> ScirsResult<Array2<f64>> {
940            let mut rng = scirs2_core::random::rng();
941
942            let dims = bounds.len();
943            let mut positions = Array2::zeros((local_size, dims));
944
945            for i in 0..local_size {
946                for j in 0..dims {
947                    let (low, high) = bounds[j];
948                    positions[[i, j]] = rng.random_range(low..=high);
949                }
950            }
951
952            Ok(positions)
953        }
954
955        fn evaluate_swarm<F>(
956            &self,
957            function: &F,
958            positions: &Array2<f64>,
959        ) -> ScirsResult<Array1<f64>>
960        where
961            F: Fn(&ArrayView1<f64>) -> f64,
962        {
963            let mut fitness = Array1::zeros(positions.nrows());
964
965            for i in 0..positions.nrows() {
966                let particle = positions.row(i);
967                fitness[i] = function(&particle);
968            }
969
970            Ok(fitness)
971        }
972
973        fn find_global_best(
974            &mut self,
975            positions: &Array2<f64>,
976            fitness: &Array1<f64>,
977        ) -> ScirsResult<(Array1<f64>, f64)> {
978            // Find local best
979            let mut best_idx = 0;
980            let mut best_fitness = fitness[0];
981            for (i, &f) in fitness.iter().enumerate() {
982                if f < best_fitness {
983                    best_fitness = f;
984                    best_idx = i;
985                }
986            }
987
988            let local_best = positions.row(best_idx).to_owned();
989
990            // Reduce to the true global best across all processes: the global
991            // minimum fitness and a broadcast of the particle that owns it.
992            self.context.select_global_best(&local_best, best_fitness)
993        }
994
995        fn update_swarm(
996            &self,
997            positions: &mut Array2<f64>,
998            velocities: &mut Array2<f64>,
999            personal_best: &Array2<f64>,
1000            global_best: &Array1<f64>,
1001            bounds: &[(f64, f64)],
1002        ) -> ScirsResult<()> {
1003            let mut rng = scirs2_core::random::rng();
1004
1005            let (swarm_size, dims) = positions.dim();
1006
1007            for i in 0..swarm_size {
1008                for j in 0..dims {
1009                    let r1: f64 = rng.random();
1010                    let r2: f64 = rng.random();
1011
1012                    // Update velocity
1013                    velocities[[i, j]] = self.w * velocities[[i, j]]
1014                        + self.c1 * r1 * (personal_best[[i, j]] - positions[[i, j]])
1015                        + self.c2 * r2 * (global_best[j] - positions[[i, j]]);
1016
1017                    // Update position
1018                    positions[[i, j]] += velocities[[i, j]];
1019
1020                    // Apply bounds
1021                    let (low, high) = bounds[j];
1022                    if positions[[i, j]] < low {
1023                        positions[[i, j]] = low;
1024                        velocities[[i, j]] = 0.0;
1025                    } else if positions[[i, j]] > high {
1026                        positions[[i, j]] = high;
1027                        velocities[[i, j]] = 0.0;
1028                    }
1029                }
1030            }
1031
1032            Ok(())
1033        }
1034    }
1035}
1036
1037/// Performance statistics for distributed optimization
1038#[derive(Debug, Clone)]
1039pub struct DistributedStats {
1040    /// Communication time statistics
1041    pub communication_time: f64,
1042    /// Computation time statistics
1043    pub computation_time: f64,
1044    /// Load balancing statistics
1045    pub load_balance_ratio: f64,
1046    /// Number of synchronization points
1047    pub synchronizations: usize,
1048    /// Data transfer statistics (bytes)
1049    pub bytes_transferred: usize,
1050}
1051
1052impl DistributedStats {
1053    fn new() -> Self {
1054        Self {
1055            communication_time: 0.0,
1056            computation_time: 0.0,
1057            load_balance_ratio: 1.0,
1058            synchronizations: 0,
1059            bytes_transferred: 0,
1060        }
1061    }
1062
1063    /// Calculate parallel efficiency
1064    pub fn parallel_efficiency(&self) -> f64 {
1065        if self.communication_time + self.computation_time == 0.0 {
1066            1.0
1067        } else {
1068            self.computation_time / (self.communication_time + self.computation_time)
1069        }
1070    }
1071
1072    /// Generate performance report
1073    pub fn generate_report(&self) -> String {
1074        format!(
1075            "Distributed Optimization Performance Report\n\
1076             ==========================================\n\
1077             Computation Time: {:.3}s\n\
1078             Communication Time: {:.3}s\n\
1079             Parallel Efficiency: {:.1}%\n\
1080             Load Balance Ratio: {:.3}\n\
1081             Synchronizations: {}\n\
1082             Data Transferred: {} bytes\n",
1083            self.computation_time,
1084            self.communication_time,
1085            self.parallel_efficiency() * 100.0,
1086            self.load_balance_ratio,
1087            self.synchronizations,
1088            self.bytes_transferred
1089        )
1090    }
1091}
1092
1093/// Minimal rank/size-only stub, for tests of logic that only reads
1094/// [`MPIInterface::rank`]/[`MPIInterface::size`] (e.g. the crate-private
1095/// `WorkDistribution`) and never actually exercises cross-rank communication.
1096///
1097/// Its collective methods are deliberately no-ops (`broadcast`/`gather` leave
1098/// `data` untouched; `send`/`recv` never actually transfer anything) — with
1099/// only one instance in existence there is no "other side" to communicate
1100/// with, so pretending otherwise would silently fabricate results. Using
1101/// this with `size > 1` to drive an actual algorithm (e.g.
1102/// [`algorithms::DistributedDifferentialEvolution`]) will silently skip all
1103/// cross-rank exchange. For real multi-rank end-to-end testing, use
1104/// [`ThreadRankMpi`], which backs every collective with genuine
1105/// cross-thread synchronization.
1106pub struct MockMPI {
1107    rank: i32,
1108    size: i32,
1109}
1110
1111impl MockMPI {
1112    pub fn new(rank: i32, size: i32) -> Self {
1113        Self { rank, size }
1114    }
1115}
1116
1117impl MPIInterface for MockMPI {
1118    fn rank(&self) -> i32 {
1119        self.rank
1120    }
1121    fn size(&self) -> i32 {
1122        self.size
1123    }
1124
1125    fn broadcast<T>(&self, _data: &mut [T], _root: i32) -> ScirsResult<()>
1126    where
1127        T: Clone + Send + Sync + 'static,
1128    {
1129        Ok(())
1130    }
1131
1132    fn gather<T>(
1133        &self,
1134        _send_data: &[T],
1135        _recv_data: Option<&mut [T]>,
1136        _root: i32,
1137    ) -> ScirsResult<()>
1138    where
1139        T: Clone + Send + Sync + 'static,
1140    {
1141        Ok(())
1142    }
1143
1144    fn allreduce<T>(
1145        &self,
1146        send_data: &[T],
1147        recv_data: &mut [T],
1148        _op: ReductionOp,
1149    ) -> ScirsResult<()>
1150    where
1151        T: Clone + Send + Sync + 'static + std::ops::Add<Output = T> + PartialOrd,
1152    {
1153        // Single simulated rank: the "reduction" over one contribution is
1154        // itself, regardless of op (sum/min/max of one value is that value).
1155        for (i, item) in send_data.iter().enumerate() {
1156            if i < recv_data.len() {
1157                recv_data[i] = item.clone();
1158            }
1159        }
1160        Ok(())
1161    }
1162
1163    fn barrier(&self) -> ScirsResult<()> {
1164        Ok(())
1165    }
1166    fn send<T>(&self, _data: &[T], _dest: i32, _tag: i32) -> ScirsResult<()>
1167    where
1168        T: Clone + Send + Sync + 'static,
1169    {
1170        Ok(())
1171    }
1172    fn recv<T>(&self, _data: &mut [T], _source: i32, _tag: i32) -> ScirsResult<()>
1173    where
1174        T: Clone + Send + Sync + 'static,
1175    {
1176        Ok(())
1177    }
1178}
1179
1180// ─────────────────────────────────────────────────────────────────────────
1181// Thread-simulated multi-rank MPI (no MPI runtime / process spawning needed)
1182// ─────────────────────────────────────────────────────────────────────────
1183
1184/// Per-communicator state shared by every [`ThreadRankMpi`] handle in the
1185/// same simulated communicator. Collectives rendezvous through `slots`
1186/// (one entry per rank) guarded by a pair of reusable barriers; point-to-
1187/// point `send`/`recv` rendezvous through per-destination `mailboxes`.
1188struct ThreadMpiShared {
1189    size: i32,
1190    slots: Mutex<Vec<Option<Box<dyn Any + Send>>>>,
1191    deposited: Barrier,
1192    consumed: Barrier,
1193    explicit_barrier: Barrier,
1194    mailboxes: Mutex<Vec<Vec<(i32, i32, Box<dyn Any + Send>)>>>,
1195    mail_cv: Condvar,
1196}
1197
1198/// An [`MPIInterface`] implementation that simulates `size` MPI ranks using
1199/// real OS threads within a single process, so distributed algorithms
1200/// ([`algorithms::DistributedDifferentialEvolution`],
1201/// [`algorithms::DistributedParticleSwarm`]) can be exercised end-to-end —
1202/// with genuine cross-rank broadcast/gather/allreduce/send/recv — without an
1203/// actual MPI runtime or multi-process setup. See [`spawn_ranks`] to
1204/// construct and drive a full communicator.
1205///
1206/// Every generic method requires `T: 'static` (per [`MPIInterface`]) because
1207/// values cross the simulated thread boundary via `Box<dyn Any + Send>`.
1208pub struct ThreadRankMpi {
1209    rank: i32,
1210    shared: Arc<ThreadMpiShared>,
1211}
1212
1213impl ThreadRankMpi {
1214    /// Clone the `Vec<T>` behind a type-erased slot without consuming it —
1215    /// collectives where multiple ranks all read the same slot (broadcast,
1216    /// allreduce) must not `.take()` it, or only the first rank to acquire
1217    /// the lock would see `Some` and every other rank would panic on `None`.
1218    fn clone_from_slot<T: Clone + 'static>(boxed: &Box<dyn Any + Send>) -> Vec<T> {
1219        boxed
1220            .downcast_ref::<Vec<T>>()
1221            .expect("ThreadRankMpi: type mismatch between ranks in the same collective call")
1222            .clone()
1223    }
1224}
1225
1226/// Build a `size`-rank simulated communicator and run `body` on each
1227/// simulated rank in its own thread, returning each rank's result in rank
1228/// order. `body` receives a ready-to-use [`ThreadRankMpi`] handle; construct
1229/// a [`DistributedOptimizationContext`] from it to drive a real algorithm.
1230///
1231/// # Panics
1232/// Propagates a panic from `body` on any rank (via `JoinHandle::join`'s
1233/// `Result::expect`) rather than silently losing a rank's failure.
1234pub fn spawn_ranks<F, R>(size: usize, body: F) -> Vec<R>
1235where
1236    F: Fn(ThreadRankMpi) -> R + Send + Sync + Clone + 'static,
1237    R: Send + 'static,
1238{
1239    let shared = Arc::new(ThreadMpiShared {
1240        size: size as i32,
1241        slots: Mutex::new((0..size).map(|_| None).collect()),
1242        deposited: Barrier::new(size),
1243        consumed: Barrier::new(size),
1244        explicit_barrier: Barrier::new(size),
1245        mailboxes: Mutex::new((0..size).map(|_| Vec::new()).collect()),
1246        mail_cv: Condvar::new(),
1247    });
1248
1249    let handles: Vec<_> = (0..size)
1250        .map(|rank| {
1251            let mpi = ThreadRankMpi {
1252                rank: rank as i32,
1253                shared: Arc::clone(&shared),
1254            };
1255            let body = body.clone();
1256            thread::spawn(move || body(mpi))
1257        })
1258        .collect();
1259
1260    handles
1261        .into_iter()
1262        .map(|h| h.join().expect("rank thread panicked"))
1263        .collect()
1264}
1265
1266impl MPIInterface for ThreadRankMpi {
1267    fn rank(&self) -> i32 {
1268        self.rank
1269    }
1270
1271    fn size(&self) -> i32 {
1272        self.shared.size
1273    }
1274
1275    fn broadcast<T>(&self, data: &mut [T], root: i32) -> ScirsResult<()>
1276    where
1277        T: Clone + Send + Sync + 'static,
1278    {
1279        if self.rank == root {
1280            let mut slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1281            slots[root as usize] = Some(Box::new(data.to_vec()) as Box<dyn Any + Send>);
1282        }
1283        // Every rank (root included) waits here until root has deposited,
1284        // then every rank reads the same slot below via a shared borrow
1285        // (`clone_from_slot`) rather than consuming it — otherwise only
1286        // whichever rank's thread acquired the lock first would see `Some`
1287        // and every other rank would panic on a `None` it raced away.
1288        self.shared.deposited.wait();
1289        {
1290            let slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1291            let boxed = slots[root as usize]
1292                .as_ref()
1293                .expect("broadcast root did not deposit data");
1294            let received: Vec<T> = Self::clone_from_slot(boxed);
1295            for (dst, src) in data.iter_mut().zip(received.into_iter()) {
1296                *dst = src;
1297            }
1298        }
1299        self.shared.consumed.wait();
1300        Ok(())
1301    }
1302
1303    fn gather<T>(&self, send_data: &[T], recv_data: Option<&mut [T]>, root: i32) -> ScirsResult<()>
1304    where
1305        T: Clone + Send + Sync + 'static,
1306    {
1307        {
1308            let mut slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1309            slots[self.rank as usize] = Some(Box::new(send_data.to_vec()) as Box<dyn Any + Send>);
1310        }
1311        self.shared.deposited.wait();
1312
1313        if self.rank == root {
1314            let recv_data = recv_data.expect("gather root must supply recv_data");
1315            let per_rank = send_data.len();
1316            let slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1317            for (r, slot) in slots.iter().enumerate().take(self.shared.size as usize) {
1318                let boxed = slot.as_ref().expect("gather: rank did not deposit");
1319                let v: Vec<T> = Self::clone_from_slot(boxed);
1320                let start = r * per_rank;
1321                for (i, val) in v.into_iter().enumerate() {
1322                    if start + i < recv_data.len() {
1323                        recv_data[start + i] = val;
1324                    }
1325                }
1326            }
1327        }
1328        self.shared.consumed.wait();
1329        Ok(())
1330    }
1331
1332    fn allreduce<T>(&self, send_data: &[T], recv_data: &mut [T], op: ReductionOp) -> ScirsResult<()>
1333    where
1334        T: Clone + Send + Sync + 'static + std::ops::Add<Output = T> + PartialOrd,
1335    {
1336        {
1337            let mut slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1338            slots[self.rank as usize] = Some(Box::new(send_data.to_vec()) as Box<dyn Any + Send>);
1339        }
1340        self.shared.deposited.wait();
1341
1342        // Compute into a local result first — every rank must reach the
1343        // `consumed` barrier below regardless of outcome (an early `return`
1344        // from inside the reduction, e.g. on an unsupported op, would leave
1345        // every other rank blocked on that barrier forever).
1346        let result: ScirsResult<()> = (|| {
1347            let len = send_data.len();
1348            let slots = self.shared.slots.lock().expect("mpi slots lock poisoned");
1349            let per_rank: Vec<Vec<T>> = slots
1350                .iter()
1351                .take(self.shared.size as usize)
1352                .map(|slot| {
1353                    Self::clone_from_slot::<T>(
1354                        slot.as_ref().expect("allreduce: rank did not deposit"),
1355                    )
1356                })
1357                .collect();
1358            for i in 0..len {
1359                let mut acc = per_rank[0][i].clone();
1360                for contribution in per_rank.iter().skip(1) {
1361                    let v = contribution[i].clone();
1362                    acc = match op {
1363                        ReductionOp::Sum => acc + v,
1364                        ReductionOp::Min => {
1365                            if v < acc {
1366                                v
1367                            } else {
1368                                acc
1369                            }
1370                        }
1371                        ReductionOp::Max => {
1372                            if v > acc {
1373                                v
1374                            } else {
1375                                acc
1376                            }
1377                        }
1378                        ReductionOp::Prod => {
1379                            return Err(ScirsError::InvalidInput(
1380                                scirs2_core::error::ErrorContext::new(
1381                                    "ThreadRankMpi::allreduce: ReductionOp::Prod is not \
1382                                     supported (MPIInterface's trait bound only provides Add \
1383                                     + PartialOrd, not Mul)"
1384                                        .to_string(),
1385                                ),
1386                            ));
1387                        }
1388                    };
1389                }
1390                recv_data[i] = acc;
1391            }
1392            Ok(())
1393        })();
1394        self.shared.consumed.wait();
1395        result
1396    }
1397
1398    fn barrier(&self) -> ScirsResult<()> {
1399        self.shared.explicit_barrier.wait();
1400        Ok(())
1401    }
1402
1403    fn send<T>(&self, data: &[T], dest: i32, tag: i32) -> ScirsResult<()>
1404    where
1405        T: Clone + Send + Sync + 'static,
1406    {
1407        let mut mailboxes = self.shared.mailboxes.lock().expect("mailbox lock poisoned");
1408        mailboxes[dest as usize].push((
1409            self.rank,
1410            tag,
1411            Box::new(data.to_vec()) as Box<dyn Any + Send>,
1412        ));
1413        self.shared.mail_cv.notify_all();
1414        Ok(())
1415    }
1416
1417    fn recv<T>(&self, data: &mut [T], source: i32, tag: i32) -> ScirsResult<()>
1418    where
1419        T: Clone + Send + Sync + 'static,
1420    {
1421        let mut mailboxes = self.shared.mailboxes.lock().expect("mailbox lock poisoned");
1422        loop {
1423            let inbox = &mut mailboxes[self.rank as usize];
1424            if let Some(pos) = inbox
1425                .iter()
1426                .position(|(src, t, _)| *src == source && *t == tag)
1427            {
1428                let (_, _, boxed) = inbox.remove(pos);
1429                let received: Vec<T> = *boxed
1430                    .downcast::<Vec<T>>()
1431                    .expect("ThreadRankMpi::recv: type mismatch with matching send");
1432                for (dst, src) in data.iter_mut().zip(received.into_iter()) {
1433                    *dst = src;
1434                }
1435                return Ok(());
1436            }
1437            mailboxes = self
1438                .shared
1439                .mail_cv
1440                .wait(mailboxes)
1441                .expect("mailbox condvar wait poisoned");
1442        }
1443    }
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448    use super::*;
1449
1450    #[test]
1451    fn test_work_distribution() {
1452        let distribution = WorkDistribution::new(0, 4, DistributionStrategy::DataParallel);
1453        let assignment = distribution.assign_work(100);
1454
1455        assert_eq!(assignment.count, 25);
1456        assert_eq!(assignment.start_index, 0);
1457        assert_eq!(assignment.range(), 0..25);
1458    }
1459
1460    #[test]
1461    fn test_work_assignment_remainder() {
1462        let distribution = WorkDistribution::new(3, 4, DistributionStrategy::DataParallel);
1463        let assignment = distribution.assign_work(10);
1464
1465        // 10 items, 4 processes: 2, 3, 3, 2
1466        assert_eq!(assignment.count, 2);
1467        assert_eq!(assignment.start_index, 8);
1468    }
1469
1470    #[test]
1471    fn test_master_worker_distribution() {
1472        let master_distribution = WorkDistribution::new(0, 4, DistributionStrategy::MasterWorker);
1473        let master_assignment = master_distribution.assign_work(100);
1474
1475        assert_eq!(master_assignment.count, 0); // Master doesn't do computation
1476
1477        let worker_distribution = WorkDistribution::new(1, 4, DistributionStrategy::MasterWorker);
1478        let worker_assignment = worker_distribution.assign_work(100);
1479
1480        assert!(worker_assignment.count > 0); // Worker does computation
1481    }
1482
1483    #[test]
1484    fn test_distributed_context() {
1485        let mpi = MockMPI::new(0, 4);
1486        let config = DistributedConfig::default();
1487        let context = DistributedOptimizationContext::new(mpi, config);
1488
1489        assert_eq!(context.rank(), 0);
1490        assert_eq!(context.size(), 4);
1491        assert!(context.is_master());
1492    }
1493
1494    #[test]
1495    fn test_distributed_stats() {
1496        let mut stats = DistributedStats::new();
1497        stats.computation_time = 80.0;
1498        stats.communication_time = 20.0;
1499
1500        assert_eq!(stats.parallel_efficiency(), 0.8);
1501    }
1502
1503    // ─────────────────────────────────────────────────────────────────────
1504    // ThreadRankMpi: real cross-thread collective semantics
1505    // ─────────────────────────────────────────────────────────────────────
1506
1507    #[test]
1508    fn test_thread_rank_mpi_broadcast_allreduce_sendrecv() {
1509        let results = spawn_ranks(4, |mpi| {
1510            let rank = mpi.rank();
1511
1512            // Broadcast: root (rank 2) deposits, every rank must see its data.
1513            let mut buf = vec![rank as f64; 3];
1514            mpi.broadcast(&mut buf, 2).expect("broadcast failed");
1515            assert_eq!(buf, vec![2.0, 2.0, 2.0]);
1516
1517            // Allreduce Sum over one contribution per rank: 0+1+2+3 = 6.
1518            let send = vec![rank as f64];
1519            let mut sum = vec![0.0];
1520            mpi.allreduce(&send, &mut sum, ReductionOp::Sum)
1521                .expect("allreduce sum failed");
1522            assert_eq!(sum, vec![6.0]);
1523
1524            // Allreduce Min/Max over the same contributions.
1525            let mut min = vec![0.0];
1526            mpi.allreduce(&send, &mut min, ReductionOp::Min)
1527                .expect("allreduce min failed");
1528            assert_eq!(min, vec![0.0]);
1529
1530            let mut max = vec![0.0];
1531            mpi.allreduce(&send, &mut max, ReductionOp::Max)
1532                .expect("allreduce max failed");
1533            assert_eq!(max, vec![3.0]);
1534
1535            // Ring send/recv: every rank sends to its successor and receives
1536            // from its predecessor. `send` is non-blocking (mailbox deposit),
1537            // so this ordering cannot deadlock regardless of thread scheduling.
1538            let next = (rank + 1) % 4;
1539            let prev = (rank + 3) % 4; // (rank - 1).rem_euclid(4)
1540            mpi.send(&[rank as f64], next, 0).expect("send failed");
1541            let mut incoming = vec![0.0];
1542            mpi.recv(&mut incoming, prev, 0).expect("recv failed");
1543            assert_eq!(incoming, vec![prev as f64]);
1544
1545            mpi.barrier().expect("barrier failed");
1546            rank
1547        });
1548
1549        let mut sorted = results;
1550        sorted.sort_unstable();
1551        assert_eq!(sorted, vec![0, 1, 2, 3]);
1552    }
1553
1554    #[test]
1555    fn test_thread_rank_mpi_gather() {
1556        let results = spawn_ranks(3, |mpi| {
1557            let rank = mpi.rank();
1558            let send = vec![rank as f64, (rank * 10) as f64];
1559
1560            if mpi.rank() == 0 {
1561                let mut recv = vec![0.0; 6];
1562                mpi.gather(&send, Some(&mut recv), 0)
1563                    .expect("gather failed");
1564                Some(recv)
1565            } else {
1566                mpi.gather(&send, None, 0).expect("gather failed");
1567                None
1568            }
1569        });
1570
1571        let root_result = results.into_iter().flatten().next().expect("root present");
1572        assert_eq!(root_result, vec![0.0, 0.0, 1.0, 10.0, 2.0, 20.0]);
1573    }
1574
1575    // ─────────────────────────────────────────────────────────────────────
1576    // End-to-end distributed algorithms driven across simulated ranks
1577    // ─────────────────────────────────────────────────────────────────────
1578
1579    #[test]
1580    fn test_distributed_differential_evolution_thread_ranks() {
1581        // Sphere function: f(x) = sum(x_i^2), global minimum 0 at the origin.
1582        let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
1583
1584        let results = spawn_ranks(4, move |mpi| {
1585            let context = DistributedOptimizationContext::new(mpi, DistributedConfig::default());
1586            let mut de = algorithms::DistributedDifferentialEvolution::new(context, 40, 300);
1587            de.optimize(
1588                |x: &ArrayView1<f64>| x.iter().map(|v| v * v).sum::<f64>(),
1589                &bounds,
1590            )
1591            .expect("distributed DE optimize failed")
1592        });
1593
1594        assert_eq!(results.len(), 4);
1595        for result in &results {
1596            assert!(result.success);
1597            assert!(
1598                result.fun < 1e-3,
1599                "expected convergence near the sphere minimum, got fun={}",
1600                result.fun
1601            );
1602            for &xi in result.x.iter() {
1603                assert!(
1604                    xi.abs() < 0.5,
1605                    "expected x near the origin, got {:?}",
1606                    result.x
1607                );
1608            }
1609        }
1610        // All ranks must agree on the reduced global best (it is the result
1611        // of a `Min` all-reduce followed by a broadcast from the owning rank).
1612        for result in &results[1..] {
1613            assert_eq!(result.fun, results[0].fun);
1614            assert_eq!(result.x, results[0].x);
1615        }
1616    }
1617
1618    #[test]
1619    fn test_distributed_particle_swarm_thread_ranks() {
1620        // Sphere function again, exercised through the PSO algorithm instead.
1621        let bounds = vec![(-5.0, 5.0), (-5.0, 5.0), (-5.0, 5.0)];
1622
1623        let results = spawn_ranks(3, move |mpi| {
1624            let context = DistributedOptimizationContext::new(mpi, DistributedConfig::default());
1625            let mut pso = algorithms::DistributedParticleSwarm::new(context, 30, 300);
1626            pso.optimize(
1627                |x: &ArrayView1<f64>| x.iter().map(|v| v * v).sum::<f64>(),
1628                &bounds,
1629            )
1630            .expect("distributed PSO optimize failed")
1631        });
1632
1633        assert_eq!(results.len(), 3);
1634        for result in &results {
1635            assert!(result.success);
1636            assert!(
1637                result.fun < 1e-1,
1638                "expected convergence near the sphere minimum, got fun={}",
1639                result.fun
1640            );
1641        }
1642        for result in &results[1..] {
1643            assert_eq!(result.fun, results[0].fun);
1644            assert_eq!(result.x, results[0].x);
1645        }
1646    }
1647}