1use 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
16pub trait MPIInterface {
24 fn rank(&self) -> i32;
26
27 fn size(&self) -> i32;
29
30 fn broadcast<T>(&self, data: &mut [T], root: i32) -> ScirsResult<()>
32 where
33 T: Clone + Send + Sync + 'static;
34
35 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 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 fn barrier(&self) -> ScirsResult<()>;
52
53 fn send<T>(&self, data: &[T], dest: i32, tag: i32) -> ScirsResult<()>
55 where
56 T: Clone + Send + Sync + 'static;
57
58 fn recv<T>(&self, data: &mut [T], source: i32, tag: i32) -> ScirsResult<()>
60 where
61 T: Clone + Send + Sync + 'static;
62}
63
64#[derive(Debug, Clone, Copy)]
66pub enum ReductionOp {
67 Sum,
68 Min,
69 Max,
70 Prod,
71}
72
73#[derive(Debug, Clone)]
75pub struct DistributedConfig {
76 pub distribution_strategy: DistributionStrategy,
78 pub load_balancing: LoadBalancingConfig,
80 pub communication: CommunicationConfig,
82 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#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum DistributionStrategy {
100 DataParallel,
102 ModelParallel,
104 Hybrid,
106 MasterWorker,
108}
109
110#[derive(Debug, Clone)]
112pub struct LoadBalancingConfig {
113 pub dynamic: bool,
115 pub imbalance_threshold: f64,
117 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#[derive(Debug, Clone)]
133pub struct CommunicationConfig {
134 pub async_communication: bool,
136 pub buffer_size: usize,
138 pub use_compression: bool,
140 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, use_compression: false,
150 overlap_computation: true,
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct FaultToleranceConfig {
158 pub checkpointing: bool,
160 pub checkpoint_interval: usize,
162 pub max_retries: usize,
164 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
179pub 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 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 pub fn rank(&self) -> i32 {
208 self.rank
209 }
210
211 pub fn size(&self) -> i32 {
213 self.size
214 }
215
216 pub fn is_master(&self) -> bool {
218 self.rank == 0
219 }
220
221 pub fn distribute_work(&mut self, total_work: usize) -> WorkAssignment {
223 self.work_distribution.assign_work(total_work)
224 }
225
226 pub fn synchronize(&self) -> ScirsResult<()> {
228 self.mpi.barrier()
229 }
230
231 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 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 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 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 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 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 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 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 let value_buf = Array1::from_elem(1, local_value);
322 let global_value = self.allreduce_min(&value_buf)?[0];
323
324 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 let mut winner = local_best.to_owned();
337 self.broadcast_from(&mut winner, owner_rank)?;
338
339 Ok((winner, global_value))
340 }
341
342 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 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 pub fn stats(&self) -> &DistributedStats {
374 &self.performance_stats
375 }
376}
377
378struct 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 WorkAssignment {
425 start_index: 0,
426 count: total_work, strategy: DistributionStrategy::ModelParallel,
428 }
429 }
430
431 fn hybrid_assignment(&self, total_work: usize) -> WorkAssignment {
432 let size = (self.size.max(1)) as usize;
441
442 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 let rank = (self.rank.max(0)) as usize;
456 let data_coord = (rank / model_groups).min(data_groups.saturating_sub(1));
457
458 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 WorkAssignment {
476 start_index: 0,
477 count: 0,
478 strategy: DistributionStrategy::MasterWorker,
479 }
480 } else {
481 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#[derive(Debug, Clone)]
507pub struct WorkAssignment {
508 pub start_index: usize,
510 pub count: usize,
512 pub strategy: DistributionStrategy,
514}
515
516impl WorkAssignment {
517 pub fn range(&self) -> std::ops::Range<usize> {
519 self.start_index..(self.start_index + self.count)
520 }
521
522 pub fn is_empty(&self) -> bool {
524 self.count == 0
525 }
526}
527
528pub mod algorithms {
530 use super::*;
531 use crate::result::OptimizeResults;
532
533 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 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 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 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 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 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 let trial_population = self.generate_trial_population(&local_population)?;
590 let trial_fitness = self.evaluate_local_population(&function, &trial_population)?;
591
592 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 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 self.migrate_individuals(&mut local_population, &mut local_fitness)?;
613 }
614
615 if iteration % 50 == 0 {
617 let convergence = self.check_convergence(&local_fitness)?;
618 if convergence {
619 break;
620 }
621 }
622 }
623
624 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 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 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 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 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 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 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 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 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 pub struct DistributedParticleSwarm<M: MPIInterface> {
832 context: DistributedOptimizationContext<M>,
833 swarm_size: usize,
834 max_nit: usize,
835 w: f64, c1: f64, c2: f64, }
839
840 impl<M: MPIInterface> DistributedParticleSwarm<M> {
841 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 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 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 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 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 self.update_swarm(
892 &mut positions,
893 &mut velocities,
894 &personal_best,
895 &global_best.0,
896 bounds,
897 )?;
898
899 let fitness = self.evaluate_swarm(&function, &positions)?;
901 function_evaluations += local_swarm_size;
902
903 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 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 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 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 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 positions[[i, j]] += velocities[[i, j]];
1019
1020 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#[derive(Debug, Clone)]
1039pub struct DistributedStats {
1040 pub communication_time: f64,
1042 pub computation_time: f64,
1044 pub load_balance_ratio: f64,
1046 pub synchronizations: usize,
1048 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 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 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
1093pub 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 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
1180struct 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
1198pub struct ThreadRankMpi {
1209 rank: i32,
1210 shared: Arc<ThreadMpiShared>,
1211}
1212
1213impl ThreadRankMpi {
1214 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
1226pub 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 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 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 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); 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); }
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 #[test]
1508 fn test_thread_rank_mpi_broadcast_allreduce_sendrecv() {
1509 let results = spawn_ranks(4, |mpi| {
1510 let rank = mpi.rank();
1511
1512 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 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 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 let next = (rank + 1) % 4;
1539 let prev = (rank + 3) % 4; 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 #[test]
1580 fn test_distributed_differential_evolution_thread_ranks() {
1581 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 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 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}