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;
12
13/// MPI interface abstraction for distributed optimization
14pub trait MPIInterface {
15    /// Get the rank of this process
16    fn rank(&self) -> i32;
17
18    /// Get the total number of processes
19    fn size(&self) -> i32;
20
21    /// Broadcast data from root to all processes
22    fn broadcast<T>(&self, data: &mut [T], root: i32) -> ScirsResult<()>
23    where
24        T: Clone + Send + Sync;
25
26    /// Gather data from all processes to root
27    fn gather<T>(&self, send_data: &[T], recv_data: Option<&mut [T]>, root: i32) -> ScirsResult<()>
28    where
29        T: Clone + Send + Sync;
30
31    /// All-to-all reduction operation
32    fn allreduce<T>(
33        &self,
34        send_data: &[T],
35        recv_data: &mut [T],
36        op: ReductionOp,
37    ) -> ScirsResult<()>
38    where
39        T: Clone + Send + Sync + std::ops::Add<Output = T> + PartialOrd;
40
41    /// Barrier synchronization
42    fn barrier(&self) -> ScirsResult<()>;
43
44    /// Send data to specific process
45    fn send<T>(&self, data: &[T], dest: i32, tag: i32) -> ScirsResult<()>
46    where
47        T: Clone + Send + Sync;
48
49    /// Receive data from specific process
50    fn recv<T>(&self, data: &mut [T], source: i32, tag: i32) -> ScirsResult<()>
51    where
52        T: Clone + Send + Sync;
53}
54
55/// Reduction operations for MPI
56#[derive(Debug, Clone, Copy)]
57pub enum ReductionOp {
58    Sum,
59    Min,
60    Max,
61    Prod,
62}
63
64/// Configuration for distributed optimization
65#[derive(Debug, Clone)]
66pub struct DistributedConfig {
67    /// Strategy for distributing work
68    pub distribution_strategy: DistributionStrategy,
69    /// Load balancing configuration
70    pub load_balancing: LoadBalancingConfig,
71    /// Communication optimization settings
72    pub communication: CommunicationConfig,
73    /// Fault tolerance configuration
74    pub fault_tolerance: FaultToleranceConfig,
75}
76
77impl Default for DistributedConfig {
78    fn default() -> Self {
79        Self {
80            distribution_strategy: DistributionStrategy::DataParallel,
81            load_balancing: LoadBalancingConfig::default(),
82            communication: CommunicationConfig::default(),
83            fault_tolerance: FaultToleranceConfig::default(),
84        }
85    }
86}
87
88/// Work distribution strategies
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub enum DistributionStrategy {
91    /// Distribute data across processes
92    DataParallel,
93    /// Distribute parameters across processes
94    ModelParallel,
95    /// Hybrid data and model parallelism
96    Hybrid,
97    /// Master-worker with dynamic task assignment
98    MasterWorker,
99}
100
101/// Load balancing configuration
102#[derive(Debug, Clone)]
103pub struct LoadBalancingConfig {
104    /// Whether to enable dynamic load balancing
105    pub dynamic: bool,
106    /// Threshold for load imbalance (0.0 to 1.0)
107    pub imbalance_threshold: f64,
108    /// Rebalancing interval (in iterations)
109    pub rebalance_interval: usize,
110}
111
112impl Default for LoadBalancingConfig {
113    fn default() -> Self {
114        Self {
115            dynamic: true,
116            imbalance_threshold: 0.2,
117            rebalance_interval: 100,
118        }
119    }
120}
121
122/// Communication optimization configuration
123#[derive(Debug, Clone)]
124pub struct CommunicationConfig {
125    /// Whether to use asynchronous communication
126    pub async_communication: bool,
127    /// Communication buffer size
128    pub buffer_size: usize,
129    /// Compression for large data transfers
130    pub use_compression: bool,
131    /// Overlap computation with communication
132    pub overlap_computation: bool,
133}
134
135impl Default for CommunicationConfig {
136    fn default() -> Self {
137        Self {
138            async_communication: true,
139            buffer_size: 1024 * 1024, // 1MB
140            use_compression: false,
141            overlap_computation: true,
142        }
143    }
144}
145
146/// Fault tolerance configuration
147#[derive(Debug, Clone)]
148pub struct FaultToleranceConfig {
149    /// Enable checkpointing
150    pub checkpointing: bool,
151    /// Checkpoint interval (in iterations)
152    pub checkpoint_interval: usize,
153    /// Maximum number of retries for failed operations
154    pub max_retries: usize,
155    /// Timeout for MPI operations (in seconds)
156    pub timeout: f64,
157}
158
159impl Default for FaultToleranceConfig {
160    fn default() -> Self {
161        Self {
162            checkpointing: false,
163            checkpoint_interval: 1000,
164            max_retries: 3,
165            timeout: 30.0,
166        }
167    }
168}
169
170/// Distributed optimization context
171pub struct DistributedOptimizationContext<M: MPIInterface> {
172    mpi: M,
173    config: DistributedConfig,
174    rank: i32,
175    size: i32,
176    work_distribution: WorkDistribution,
177    performance_stats: DistributedStats,
178}
179
180impl<M: MPIInterface> DistributedOptimizationContext<M> {
181    /// Create a new distributed optimization context
182    pub fn new(mpi: M, config: DistributedConfig) -> Self {
183        let rank = mpi.rank();
184        let size = mpi.size();
185        let work_distribution = WorkDistribution::new(rank, size, config.distribution_strategy);
186
187        Self {
188            mpi,
189            config,
190            rank,
191            size,
192            work_distribution,
193            performance_stats: DistributedStats::new(),
194        }
195    }
196
197    /// Get the MPI rank of this process
198    pub fn rank(&self) -> i32 {
199        self.rank
200    }
201
202    /// Get the total number of MPI processes
203    pub fn size(&self) -> i32 {
204        self.size
205    }
206
207    /// Check if this is the master process
208    pub fn is_master(&self) -> bool {
209        self.rank == 0
210    }
211
212    /// Distribute work among processes
213    pub fn distribute_work(&mut self, total_work: usize) -> WorkAssignment {
214        self.work_distribution.assign_work(total_work)
215    }
216
217    /// Synchronize all processes
218    pub fn synchronize(&self) -> ScirsResult<()> {
219        self.mpi.barrier()
220    }
221
222    /// Broadcast parameters from master to all workers
223    pub fn broadcast_parameters(&self, params: &mut Array1<f64>) -> ScirsResult<()> {
224        let data = params.as_slice_mut().expect("Operation failed");
225        self.mpi.broadcast(data, 0)
226    }
227
228    /// Gather results from all workers to master
229    pub fn gather_results(&self, local_result: &Array1<f64>) -> ScirsResult<Option<Array2<f64>>> {
230        if self.is_master() {
231            let total_size = local_result.len() * self.size as usize;
232            let mut gathered_data = vec![0.0; total_size];
233            self.mpi.gather(
234                local_result.as_slice().expect("Operation failed"),
235                Some(&mut gathered_data),
236                0,
237            )?;
238
239            // Reshape into 2D array
240            let result =
241                Array2::from_shape_vec((self.size as usize, local_result.len()), gathered_data)
242                    .map_err(|e| {
243                        ScirsError::InvalidInput(scirs2_core::error::ErrorContext::new(format!(
244                            "Failed to reshape gathered data: {}",
245                            e
246                        )))
247                    })?;
248            Ok(Some(result))
249        } else {
250            self.mpi
251                .gather(local_result.as_slice().expect("Operation failed"), None, 0)?;
252            Ok(None)
253        }
254    }
255
256    /// Perform all-reduce operation (sum)
257    pub fn allreduce_sum(&self, local_data: &Array1<f64>) -> ScirsResult<Array1<f64>> {
258        let mut result = Array1::zeros(local_data.len());
259        self.mpi.allreduce(
260            local_data.as_slice().expect("Operation failed"),
261            result.as_slice_mut().expect("Operation failed"),
262            ReductionOp::Sum,
263        )?;
264        Ok(result)
265    }
266
267    /// Perform all-reduce operation (element-wise minimum)
268    pub fn allreduce_min(&self, local_data: &Array1<f64>) -> ScirsResult<Array1<f64>> {
269        let mut result = Array1::zeros(local_data.len());
270        self.mpi.allreduce(
271            local_data.as_slice().expect("Operation failed"),
272            result.as_slice_mut().expect("Operation failed"),
273            ReductionOp::Min,
274        )?;
275        Ok(result)
276    }
277
278    /// Broadcast a vector from an arbitrary root rank to all processes
279    pub fn broadcast_from(&self, params: &mut Array1<f64>, root: i32) -> ScirsResult<()> {
280        let data = params.as_slice_mut().expect("Operation failed");
281        self.mpi.broadcast(data, root)
282    }
283
284    /// Select the globally-best candidate across the whole communicator.
285    ///
286    /// Given each process's local best point and its objective value, returns
287    /// the best `(point, value)` pair over all processes. The global-best value
288    /// is obtained with a `Min` all-reduce; the owning rank (lowest rank on
289    /// ties) is identified by a second `Min` all-reduce over rank indices, and
290    /// its point is broadcast so every process agrees on the winning candidate.
291    pub fn select_global_best(
292        &self,
293        local_best: &Array1<f64>,
294        local_value: f64,
295    ) -> ScirsResult<(Array1<f64>, f64)> {
296        if self.size <= 1 {
297            return Ok((local_best.to_owned(), local_value));
298        }
299
300        // Global-best objective value.
301        let value_buf = Array1::from_elem(1, local_value);
302        let global_value = self.allreduce_min(&value_buf)?[0];
303
304        // Identify the owning rank: holders of the global-best value propose
305        // their own rank, everyone else proposes a sentinel larger than any
306        // valid rank. The minimum proposal is the lowest rank holding the best.
307        let owner_proposal = if local_value <= global_value {
308            self.rank as f64
309        } else {
310            self.size as f64
311        };
312        let owner_buf = Array1::from_elem(1, owner_proposal);
313        let owner_rank = self.allreduce_min(&owner_buf)?[0] as i32;
314
315        // Broadcast the winning point from its owner to all processes.
316        let mut winner = local_best.to_owned();
317        self.broadcast_from(&mut winner, owner_rank)?;
318
319        Ok((winner, global_value))
320    }
321
322    /// Exchange a vector around a unidirectional process ring.
323    ///
324    /// Sends `outgoing` to rank `(rank + 1) % size` and returns the vector
325    /// received from rank `(rank - 1 + size) % size`. A deadlock-free ordering
326    /// is used (rank 0 receives before sending, every other rank sends before
327    /// receiving), so it is safe with blocking point-to-point primitives for any
328    /// communicator size. Returns `None` when there is only a single process.
329    pub fn ring_exchange(&self, outgoing: &Array1<f64>) -> ScirsResult<Option<Array1<f64>>> {
330        if self.size <= 1 {
331            return Ok(None);
332        }
333
334        let next = (self.rank + 1).rem_euclid(self.size);
335        let prev = (self.rank - 1).rem_euclid(self.size);
336        let tag = 0;
337        let send = outgoing.as_slice().expect("Operation failed");
338        let mut incoming = vec![0.0_f64; outgoing.len()];
339
340        if self.rank == 0 {
341            // Rank 0 receives first to break the cyclic dependency.
342            self.mpi.recv(&mut incoming, prev, tag)?;
343            self.mpi.send(send, next, tag)?;
344        } else {
345            self.mpi.send(send, next, tag)?;
346            self.mpi.recv(&mut incoming, prev, tag)?;
347        }
348
349        Ok(Some(Array1::from_vec(incoming)))
350    }
351
352    /// Get performance statistics
353    pub fn stats(&self) -> &DistributedStats {
354        &self.performance_stats
355    }
356}
357
358/// Work distribution manager
359struct WorkDistribution {
360    rank: i32,
361    size: i32,
362    strategy: DistributionStrategy,
363}
364
365impl WorkDistribution {
366    fn new(rank: i32, size: i32, strategy: DistributionStrategy) -> Self {
367        Self {
368            rank,
369            size,
370            strategy,
371        }
372    }
373
374    fn assign_work(&self, total_work: usize) -> WorkAssignment {
375        match self.strategy {
376            DistributionStrategy::DataParallel => self.data_parallel_assignment(total_work),
377            DistributionStrategy::ModelParallel => self.model_parallel_assignment(total_work),
378            DistributionStrategy::Hybrid => self.hybrid_assignment(total_work),
379            DistributionStrategy::MasterWorker => self.master_worker_assignment(total_work),
380        }
381    }
382
383    fn data_parallel_assignment(&self, total_work: usize) -> WorkAssignment {
384        let work_per_process = total_work / self.size as usize;
385        let remainder = total_work % self.size as usize;
386
387        let start = self.rank as usize * work_per_process + (self.rank as usize).min(remainder);
388        let extra = if (self.rank as usize) < remainder {
389            1
390        } else {
391            0
392        };
393        let count = work_per_process + extra;
394
395        WorkAssignment {
396            start_index: start,
397            count,
398            strategy: DistributionStrategy::DataParallel,
399        }
400    }
401
402    fn model_parallel_assignment(&self, total_work: usize) -> WorkAssignment {
403        // For model parallelism, each process handles different parameters
404        WorkAssignment {
405            start_index: 0,
406            count: total_work, // Each process sees all data but handles different parameters
407            strategy: DistributionStrategy::ModelParallel,
408        }
409    }
410
411    fn hybrid_assignment(&self, total_work: usize) -> WorkAssignment {
412        // Hybrid data/model parallelism: arrange the processes into a 2-D grid
413        // of `data_groups` x `model_groups`. The data dimension partitions the
414        // work range (as in data parallelism), while processes that share a
415        // data range form a model-parallel group splitting the parameter space.
416        //
417        // For a single process, or a communicator that cannot be factored into
418        // more than one model group (e.g. a prime size), this reduces exactly
419        // to data parallelism over all processes.
420        let size = (self.size.max(1)) as usize;
421
422        // Choose the number of model-parallel groups as the largest divisor of
423        // `size` not exceeding sqrt(size); this yields a balanced process grid.
424        let mut model_groups = 1usize;
425        let mut divisor = 2usize;
426        while divisor * divisor <= size {
427            if size % divisor == 0 {
428                model_groups = divisor;
429            }
430            divisor += 1;
431        }
432        let data_groups = size / model_groups;
433
434        // Coordinate of this process along the data dimension of the grid.
435        let rank = (self.rank.max(0)) as usize;
436        let data_coord = (rank / model_groups).min(data_groups.saturating_sub(1));
437
438        // Partition the work range across the data groups.
439        let work_per_group = total_work / data_groups;
440        let remainder = total_work % data_groups;
441        let start = data_coord * work_per_group + data_coord.min(remainder);
442        let extra = if data_coord < remainder { 1 } else { 0 };
443        let count = work_per_group + extra;
444
445        WorkAssignment {
446            start_index: start,
447            count,
448            strategy: DistributionStrategy::Hybrid,
449        }
450    }
451
452    fn master_worker_assignment(&self, total_work: usize) -> WorkAssignment {
453        if self.rank == 0 {
454            // Master coordinates but may not do computation
455            WorkAssignment {
456                start_index: 0,
457                count: 0,
458                strategy: DistributionStrategy::MasterWorker,
459            }
460        } else {
461            // Workers split the work
462            let worker_count = self.size - 1;
463            let work_per_worker = total_work / worker_count as usize;
464            let remainder = total_work % worker_count as usize;
465            let worker_rank = self.rank - 1;
466
467            let start =
468                worker_rank as usize * work_per_worker + (worker_rank as usize).min(remainder);
469            let extra = if (worker_rank as usize) < remainder {
470                1
471            } else {
472                0
473            };
474            let count = work_per_worker + extra;
475
476            WorkAssignment {
477                start_index: start,
478                count,
479                strategy: DistributionStrategy::MasterWorker,
480            }
481        }
482    }
483}
484
485/// Work assignment for a process
486#[derive(Debug, Clone)]
487pub struct WorkAssignment {
488    /// Starting index for this process
489    pub start_index: usize,
490    /// Number of work items for this process
491    pub count: usize,
492    /// Distribution strategy used
493    pub strategy: DistributionStrategy,
494}
495
496impl WorkAssignment {
497    /// Get the range of indices assigned to this process
498    pub fn range(&self) -> std::ops::Range<usize> {
499        self.start_index..(self.start_index + self.count)
500    }
501
502    /// Check if this assignment is empty
503    pub fn is_empty(&self) -> bool {
504        self.count == 0
505    }
506}
507
508/// Distributed optimization algorithms
509pub mod algorithms {
510    use super::*;
511    use crate::result::OptimizeResults;
512
513    /// Distributed differential evolution
514    pub struct DistributedDifferentialEvolution<M: MPIInterface> {
515        context: DistributedOptimizationContext<M>,
516        population_size: usize,
517        max_nit: usize,
518        f_scale: f64,
519        crossover_rate: f64,
520    }
521
522    impl<M: MPIInterface> DistributedDifferentialEvolution<M> {
523        /// Create a new distributed differential evolution optimizer
524        pub fn new(
525            context: DistributedOptimizationContext<M>,
526            population_size: usize,
527            max_nit: usize,
528        ) -> Self {
529            Self {
530                context,
531                population_size,
532                max_nit,
533                f_scale: 0.8,
534                crossover_rate: 0.7,
535            }
536        }
537
538        /// Set mutation parameters
539        pub fn with_parameters(mut self, f_scale: f64, crossover_rate: f64) -> Self {
540            self.f_scale = f_scale;
541            self.crossover_rate = crossover_rate;
542            self
543        }
544
545        /// Optimize function using distributed differential evolution
546        pub fn optimize<F>(
547            &mut self,
548            function: F,
549            bounds: &[(f64, f64)],
550        ) -> ScirsResult<OptimizeResults<f64>>
551        where
552            F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
553        {
554            let dims = bounds.len();
555
556            // Initialize local population
557            let local_pop_size = self.population_size / self.context.size() as usize;
558            let mut local_population = self.initialize_local_population(local_pop_size, bounds)?;
559            let mut local_fitness = self.evaluate_local_population(&function, &local_population)?;
560
561            // Find global best across all processes
562            let mut global_best = self.find_global_best(&local_population, &local_fitness)?;
563            let mut global_best_fitness = global_best.1;
564
565            let mut total_evaluations = self.population_size;
566
567            for iteration in 0..self.max_nit {
568                // Generate trial population
569                let trial_population = self.generate_trial_population(&local_population)?;
570                let trial_fitness = self.evaluate_local_population(&function, &trial_population)?;
571
572                // Selection
573                self.selection(
574                    &mut local_population,
575                    &mut local_fitness,
576                    &trial_population,
577                    &trial_fitness,
578                );
579
580                total_evaluations += local_pop_size;
581
582                // Exchange information between processes
583                if iteration % 10 == 0 {
584                    let new_global_best =
585                        self.find_global_best(&local_population, &local_fitness)?;
586                    if new_global_best.1 < global_best_fitness {
587                        global_best = new_global_best;
588                        global_best_fitness = global_best.1;
589                    }
590
591                    // Migration between processes
592                    self.migrate_individuals(&mut local_population, &mut local_fitness)?;
593                }
594
595                // Convergence check (simplified)
596                if iteration % 50 == 0 {
597                    let convergence = self.check_convergence(&local_fitness)?;
598                    if convergence {
599                        break;
600                    }
601                }
602            }
603
604            // Final global best search
605            let final_best = self.find_global_best(&local_population, &local_fitness)?;
606            if final_best.1 < global_best_fitness {
607                global_best = final_best.clone();
608                global_best_fitness = final_best.1;
609            }
610
611            Ok(OptimizeResults::<f64> {
612                x: global_best.0,
613                fun: global_best_fitness,
614                success: true,
615                message: "Distributed differential evolution completed".to_string(),
616                nit: self.max_nit,
617                nfev: total_evaluations,
618                ..OptimizeResults::default()
619            })
620        }
621
622        fn initialize_local_population(
623            &self,
624            local_size: usize,
625            bounds: &[(f64, f64)],
626        ) -> ScirsResult<Array2<f64>> {
627            let mut rng = scirs2_core::random::rng();
628
629            let dims = bounds.len();
630            let mut population = Array2::zeros((local_size, dims));
631
632            for i in 0..local_size {
633                for j in 0..dims {
634                    let (low, high) = bounds[j];
635                    population[[i, j]] = rng.random_range(low..=high);
636                }
637            }
638
639            Ok(population)
640        }
641
642        fn evaluate_local_population<F>(
643            &self,
644            function: &F,
645            population: &Array2<f64>,
646        ) -> ScirsResult<Array1<f64>>
647        where
648            F: Fn(&ArrayView1<f64>) -> f64,
649        {
650            let mut fitness = Array1::zeros(population.nrows());
651
652            for i in 0..population.nrows() {
653                let individual = population.row(i);
654                fitness[i] = function(&individual);
655            }
656
657            Ok(fitness)
658        }
659
660        fn find_global_best(
661            &mut self,
662            local_population: &Array2<f64>,
663            local_fitness: &Array1<f64>,
664        ) -> ScirsResult<(Array1<f64>, f64)> {
665            // Find local best
666            let mut best_idx = 0;
667            let mut best_fitness = local_fitness[0];
668            for (i, &fitness) in local_fitness.iter().enumerate() {
669                if fitness < best_fitness {
670                    best_fitness = fitness;
671                    best_idx = i;
672                }
673            }
674
675            let local_best = local_population.row(best_idx).to_owned();
676
677            // Reduce to the true global best across all processes: the global
678            // minimum fitness and a broadcast of the individual that owns it.
679            self.context.select_global_best(&local_best, best_fitness)
680        }
681
682        fn generate_trial_population(&self, population: &Array2<f64>) -> ScirsResult<Array2<f64>> {
683            let mut rng = scirs2_core::random::rng();
684
685            let (pop_size, dims) = population.dim();
686            let mut trial_population = Array2::zeros((pop_size, dims));
687
688            for i in 0..pop_size {
689                // Select three random individuals
690                let mut indices = Vec::new();
691                while indices.len() < 3 {
692                    let idx = rng.random_range(0..pop_size);
693                    if idx != i && !indices.contains(&idx) {
694                        indices.push(idx);
695                    }
696                }
697
698                let a = indices[0];
699                let b = indices[1];
700                let c = indices[2];
701
702                // Mutation and crossover
703                let j_rand = rng.random_range(0..dims);
704                for j in 0..dims {
705                    if rng.random::<f64>() < self.crossover_rate || j == j_rand {
706                        trial_population[[i, j]] = population[[a, j]]
707                            + self.f_scale * (population[[b, j]] - population[[c, j]]);
708                    } else {
709                        trial_population[[i, j]] = population[[i, j]];
710                    }
711                }
712            }
713
714            Ok(trial_population)
715        }
716
717        fn selection(
718            &self,
719            population: &mut Array2<f64>,
720            fitness: &mut Array1<f64>,
721            trial_population: &Array2<f64>,
722            trial_fitness: &Array1<f64>,
723        ) {
724            for i in 0..population.nrows() {
725                if trial_fitness[i] <= fitness[i] {
726                    for j in 0..population.ncols() {
727                        population[[i, j]] = trial_population[[i, j]];
728                    }
729                    fitness[i] = trial_fitness[i];
730                }
731            }
732        }
733
734        fn migrate_individuals(
735            &mut self,
736            population: &mut Array2<f64>,
737            fitness: &mut Array1<f64>,
738        ) -> ScirsResult<()> {
739            // Island-model migration around a unidirectional process ring: send
740            // this island's best individual to the next process and adopt the
741            // migrant received from the previous process if it improves on the
742            // local worst individual.
743            if self.context.size() <= 1 {
744                return Ok(());
745            }
746
747            let best_idx = fitness
748                .iter()
749                .enumerate()
750                .min_by(|(_, a), (_, b)| a.partial_cmp(b).expect("Operation failed"))
751                .map(|(i, _)| i)
752                .unwrap_or(0);
753
754            // Pack the best individual together with its fitness so the migrant
755            // arrives with a valid objective value (the islands share the same
756            // objective, so the value transfers without re-evaluation).
757            let dims = population.ncols();
758            let mut payload = population.row(best_idx).to_vec();
759            payload.push(fitness[best_idx]);
760            let payload = Array1::from_vec(payload);
761
762            if let Some(received) = self.context.ring_exchange(&payload)? {
763                let migrant_fitness = received[dims];
764
765                // Replace the local worst individual when the migrant is better.
766                let worst_idx = fitness
767                    .iter()
768                    .enumerate()
769                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("Operation failed"))
770                    .map(|(i, _)| i)
771                    .unwrap_or(0);
772
773                if migrant_fitness < fitness[worst_idx] {
774                    for j in 0..dims {
775                        population[[worst_idx, j]] = received[j];
776                    }
777                    fitness[worst_idx] = migrant_fitness;
778                }
779            }
780
781            Ok(())
782        }
783
784        fn check_convergence(&mut self, local_fitness: &Array1<f64>) -> ScirsResult<bool> {
785            let mean = local_fitness.view().mean();
786            let variance = local_fitness
787                .iter()
788                .map(|&x| (x - mean).powi(2))
789                .sum::<f64>()
790                / local_fitness.len() as f64;
791
792            let std_dev = variance.sqrt();
793
794            // Simple convergence criterion
795            Ok(std_dev < 1e-12)
796        }
797    }
798
799    /// Distributed particle swarm optimization
800    pub struct DistributedParticleSwarm<M: MPIInterface> {
801        context: DistributedOptimizationContext<M>,
802        swarm_size: usize,
803        max_nit: usize,
804        w: f64,  // Inertia weight
805        c1: f64, // Cognitive parameter
806        c2: f64, // Social parameter
807    }
808
809    impl<M: MPIInterface> DistributedParticleSwarm<M> {
810        /// Create a new distributed particle swarm optimizer
811        pub fn new(
812            context: DistributedOptimizationContext<M>,
813            swarm_size: usize,
814            max_nit: usize,
815        ) -> Self {
816            Self {
817                context,
818                swarm_size,
819                max_nit,
820                w: 0.729,
821                c1: 1.49445,
822                c2: 1.49445,
823            }
824        }
825
826        /// Set PSO parameters
827        pub fn with_parameters(mut self, w: f64, c1: f64, c2: f64) -> Self {
828            self.w = w;
829            self.c1 = c1;
830            self.c2 = c2;
831            self
832        }
833
834        /// Optimize function using distributed particle swarm optimization
835        pub fn optimize<F>(
836            &mut self,
837            function: F,
838            bounds: &[(f64, f64)],
839        ) -> ScirsResult<OptimizeResults<f64>>
840        where
841            F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
842        {
843            let dims = bounds.len();
844            let local_swarm_size = self.swarm_size / self.context.size() as usize;
845
846            // Initialize local swarm
847            let mut positions = self.initialize_positions(local_swarm_size, bounds)?;
848            let mut velocities = Array2::zeros((local_swarm_size, dims));
849            let mut personal_best = positions.clone();
850            let mut personal_best_fitness = self.evaluate_swarm(&function, &positions)?;
851
852            // Find global best
853            let mut global_best = self.find_global_best(&personal_best, &personal_best_fitness)?;
854            let mut global_best_fitness = global_best.1;
855
856            let mut function_evaluations = local_swarm_size;
857
858            for iteration in 0..self.max_nit {
859                // Update swarm
860                self.update_swarm(
861                    &mut positions,
862                    &mut velocities,
863                    &personal_best,
864                    &global_best.0,
865                    bounds,
866                )?;
867
868                // Evaluate new positions
869                let fitness = self.evaluate_swarm(&function, &positions)?;
870                function_evaluations += local_swarm_size;
871
872                // Update personal bests
873                for i in 0..local_swarm_size {
874                    if fitness[i] < personal_best_fitness[i] {
875                        personal_best_fitness[i] = fitness[i];
876                        for j in 0..dims {
877                            personal_best[[i, j]] = positions[[i, j]];
878                        }
879                    }
880                }
881
882                // Update global best
883                if iteration % 10 == 0 {
884                    let new_global_best =
885                        self.find_global_best(&personal_best, &personal_best_fitness)?;
886                    if new_global_best.1 < global_best_fitness {
887                        global_best = new_global_best;
888                        global_best_fitness = global_best.1;
889                    }
890                }
891            }
892
893            Ok(OptimizeResults::<f64> {
894                x: global_best.0,
895                fun: global_best_fitness,
896                success: true,
897                message: "Distributed particle swarm optimization completed".to_string(),
898                nit: self.max_nit,
899                nfev: function_evaluations,
900                ..OptimizeResults::default()
901            })
902        }
903
904        fn initialize_positions(
905            &self,
906            local_size: usize,
907            bounds: &[(f64, f64)],
908        ) -> ScirsResult<Array2<f64>> {
909            let mut rng = scirs2_core::random::rng();
910
911            let dims = bounds.len();
912            let mut positions = Array2::zeros((local_size, dims));
913
914            for i in 0..local_size {
915                for j in 0..dims {
916                    let (low, high) = bounds[j];
917                    positions[[i, j]] = rng.random_range(low..=high);
918                }
919            }
920
921            Ok(positions)
922        }
923
924        fn evaluate_swarm<F>(
925            &self,
926            function: &F,
927            positions: &Array2<f64>,
928        ) -> ScirsResult<Array1<f64>>
929        where
930            F: Fn(&ArrayView1<f64>) -> f64,
931        {
932            let mut fitness = Array1::zeros(positions.nrows());
933
934            for i in 0..positions.nrows() {
935                let particle = positions.row(i);
936                fitness[i] = function(&particle);
937            }
938
939            Ok(fitness)
940        }
941
942        fn find_global_best(
943            &mut self,
944            positions: &Array2<f64>,
945            fitness: &Array1<f64>,
946        ) -> ScirsResult<(Array1<f64>, f64)> {
947            // Find local best
948            let mut best_idx = 0;
949            let mut best_fitness = fitness[0];
950            for (i, &f) in fitness.iter().enumerate() {
951                if f < best_fitness {
952                    best_fitness = f;
953                    best_idx = i;
954                }
955            }
956
957            let local_best = positions.row(best_idx).to_owned();
958
959            // Reduce to the true global best across all processes: the global
960            // minimum fitness and a broadcast of the particle that owns it.
961            self.context.select_global_best(&local_best, best_fitness)
962        }
963
964        fn update_swarm(
965            &self,
966            positions: &mut Array2<f64>,
967            velocities: &mut Array2<f64>,
968            personal_best: &Array2<f64>,
969            global_best: &Array1<f64>,
970            bounds: &[(f64, f64)],
971        ) -> ScirsResult<()> {
972            let mut rng = scirs2_core::random::rng();
973
974            let (swarm_size, dims) = positions.dim();
975
976            for i in 0..swarm_size {
977                for j in 0..dims {
978                    let r1: f64 = rng.random();
979                    let r2: f64 = rng.random();
980
981                    // Update velocity
982                    velocities[[i, j]] = self.w * velocities[[i, j]]
983                        + self.c1 * r1 * (personal_best[[i, j]] - positions[[i, j]])
984                        + self.c2 * r2 * (global_best[j] - positions[[i, j]]);
985
986                    // Update position
987                    positions[[i, j]] += velocities[[i, j]];
988
989                    // Apply bounds
990                    let (low, high) = bounds[j];
991                    if positions[[i, j]] < low {
992                        positions[[i, j]] = low;
993                        velocities[[i, j]] = 0.0;
994                    } else if positions[[i, j]] > high {
995                        positions[[i, j]] = high;
996                        velocities[[i, j]] = 0.0;
997                    }
998                }
999            }
1000
1001            Ok(())
1002        }
1003    }
1004}
1005
1006/// Performance statistics for distributed optimization
1007#[derive(Debug, Clone)]
1008pub struct DistributedStats {
1009    /// Communication time statistics
1010    pub communication_time: f64,
1011    /// Computation time statistics
1012    pub computation_time: f64,
1013    /// Load balancing statistics
1014    pub load_balance_ratio: f64,
1015    /// Number of synchronization points
1016    pub synchronizations: usize,
1017    /// Data transfer statistics (bytes)
1018    pub bytes_transferred: usize,
1019}
1020
1021impl DistributedStats {
1022    fn new() -> Self {
1023        Self {
1024            communication_time: 0.0,
1025            computation_time: 0.0,
1026            load_balance_ratio: 1.0,
1027            synchronizations: 0,
1028            bytes_transferred: 0,
1029        }
1030    }
1031
1032    /// Calculate parallel efficiency
1033    pub fn parallel_efficiency(&self) -> f64 {
1034        if self.communication_time + self.computation_time == 0.0 {
1035            1.0
1036        } else {
1037            self.computation_time / (self.communication_time + self.computation_time)
1038        }
1039    }
1040
1041    /// Generate performance report
1042    pub fn generate_report(&self) -> String {
1043        format!(
1044            "Distributed Optimization Performance Report\n\
1045             ==========================================\n\
1046             Computation Time: {:.3}s\n\
1047             Communication Time: {:.3}s\n\
1048             Parallel Efficiency: {:.1}%\n\
1049             Load Balance Ratio: {:.3}\n\
1050             Synchronizations: {}\n\
1051             Data Transferred: {} bytes\n",
1052            self.computation_time,
1053            self.communication_time,
1054            self.parallel_efficiency() * 100.0,
1055            self.load_balance_ratio,
1056            self.synchronizations,
1057            self.bytes_transferred
1058        )
1059    }
1060}
1061
1062/// Mock MPI implementation for testing
1063#[cfg(test)]
1064pub struct MockMPI {
1065    rank: i32,
1066    size: i32,
1067}
1068
1069#[cfg(test)]
1070impl MockMPI {
1071    pub fn new(rank: i32, size: i32) -> Self {
1072        Self { rank, size }
1073    }
1074}
1075
1076#[cfg(test)]
1077impl MPIInterface for MockMPI {
1078    fn rank(&self) -> i32 {
1079        self.rank
1080    }
1081    fn size(&self) -> i32 {
1082        self.size
1083    }
1084
1085    fn broadcast<T>(&self, data: &mut [T], root: i32) -> ScirsResult<()>
1086    where
1087        T: Clone + Send + Sync,
1088    {
1089        Ok(())
1090    }
1091
1092    fn gather<T>(
1093        &self,
1094        _send_data: &[T],
1095        _recv_data: Option<&mut [T]>,
1096        _root: i32,
1097    ) -> ScirsResult<()>
1098    where
1099        T: Clone + Send + Sync,
1100    {
1101        Ok(())
1102    }
1103
1104    fn allreduce<T>(
1105        &self,
1106        send_data: &[T],
1107        recv_data: &mut [T],
1108        _op: ReductionOp,
1109    ) -> ScirsResult<()>
1110    where
1111        T: Clone + Send + Sync + std::ops::Add<Output = T> + PartialOrd,
1112    {
1113        for (i, item) in send_data.iter().enumerate() {
1114            if i < recv_data.len() {
1115                recv_data[i] = item.clone();
1116            }
1117        }
1118        Ok(())
1119    }
1120
1121    fn barrier(&self) -> ScirsResult<()> {
1122        Ok(())
1123    }
1124    fn send<T>(&self, _data: &[T], _dest: i32, tag: i32) -> ScirsResult<()>
1125    where
1126        T: Clone + Send + Sync,
1127    {
1128        Ok(())
1129    }
1130    fn recv<T>(&self, _data: &mut [T], _source: i32, tag: i32) -> ScirsResult<()>
1131    where
1132        T: Clone + Send + Sync,
1133    {
1134        Ok(())
1135    }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141
1142    #[test]
1143    fn test_work_distribution() {
1144        let distribution = WorkDistribution::new(0, 4, DistributionStrategy::DataParallel);
1145        let assignment = distribution.assign_work(100);
1146
1147        assert_eq!(assignment.count, 25);
1148        assert_eq!(assignment.start_index, 0);
1149        assert_eq!(assignment.range(), 0..25);
1150    }
1151
1152    #[test]
1153    fn test_work_assignment_remainder() {
1154        let distribution = WorkDistribution::new(3, 4, DistributionStrategy::DataParallel);
1155        let assignment = distribution.assign_work(10);
1156
1157        // 10 items, 4 processes: 2, 3, 3, 2
1158        assert_eq!(assignment.count, 2);
1159        assert_eq!(assignment.start_index, 8);
1160    }
1161
1162    #[test]
1163    fn test_master_worker_distribution() {
1164        let master_distribution = WorkDistribution::new(0, 4, DistributionStrategy::MasterWorker);
1165        let master_assignment = master_distribution.assign_work(100);
1166
1167        assert_eq!(master_assignment.count, 0); // Master doesn't do computation
1168
1169        let worker_distribution = WorkDistribution::new(1, 4, DistributionStrategy::MasterWorker);
1170        let worker_assignment = worker_distribution.assign_work(100);
1171
1172        assert!(worker_assignment.count > 0); // Worker does computation
1173    }
1174
1175    #[test]
1176    fn test_distributed_context() {
1177        let mpi = MockMPI::new(0, 4);
1178        let config = DistributedConfig::default();
1179        let context = DistributedOptimizationContext::new(mpi, config);
1180
1181        assert_eq!(context.rank(), 0);
1182        assert_eq!(context.size(), 4);
1183        assert!(context.is_master());
1184    }
1185
1186    #[test]
1187    fn test_distributed_stats() {
1188        let mut stats = DistributedStats::new();
1189        stats.computation_time = 80.0;
1190        stats.communication_time = 20.0;
1191
1192        assert_eq!(stats.parallel_efficiency(), 0.8);
1193    }
1194}