Skip to main content

scirs2_core/array_protocol/
distributed_impl.rs

1// Copyright (c) 2025, `SciRS2` Team
2//
3// Licensed under the Apache License, Version 2.0
4// (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
5//
6
7//! Distributed array implementation using the array protocol.
8//!
9//! This module provides a more complete implementation of distributed arrays
10//! than the mock version in the main `array_protocol` module.
11
12use std::any::{Any, TypeId};
13use std::collections::HashMap;
14use std::fmt::Debug;
15
16use crate::array_protocol::{ArrayFunction, ArrayProtocol, DistributedArray, NotImplemented};
17use crate::error::CoreResult;
18use ::ndarray::{Array, Axis, Dimension, Slice};
19
20/// A configuration for distributed array operations
21#[derive(Debug, Clone, Default)]
22pub struct DistributedConfig {
23    /// Number of chunks to split the array into
24    pub chunks: usize,
25
26    /// Whether to balance the chunks across devices/nodes
27    pub balance: bool,
28
29    /// Strategy for distributing the array
30    pub strategy: DistributionStrategy,
31
32    /// Communication backend to use
33    pub backend: DistributedBackend,
34}
35
36/// Strategies for distributing an array
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum DistributionStrategy {
39    /// Split along the first axis
40    RowWise,
41
42    /// Split along the second axis
43    ColumnWise,
44
45    /// Split along all axes
46    Blocks,
47
48    /// Automatically determine the best strategy
49    Auto,
50}
51
52impl Default for DistributionStrategy {
53    fn default() -> Self {
54        Self::Auto
55    }
56}
57
58/// Communication backends for distributed arrays
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum DistributedBackend {
61    /// Local multi-threading only
62    Threaded,
63
64    /// MPI-based distributed computing
65    MPI,
66
67    /// Custom TCP/IP based communication
68    TCP,
69}
70
71impl Default for DistributedBackend {
72    fn default() -> Self {
73        Self::Threaded
74    }
75}
76
77/// A chunk of a distributed array
78#[derive(Debug, Clone)]
79pub struct ArrayChunk<T, D>
80where
81    T: Clone + 'static,
82    D: Dimension + 'static,
83{
84    /// The data in this chunk
85    pub data: Array<T, D>,
86
87    /// The global index of this chunk
88    pub global_index: Vec<usize>,
89
90    /// The node ID that holds this chunk
91    pub nodeid: usize,
92}
93
94/// A distributed array implementation
95pub struct DistributedNdarray<T, D>
96where
97    T: Clone + 'static,
98    D: Dimension + 'static,
99{
100    /// Configuration for this distributed array
101    pub config: DistributedConfig,
102
103    /// The chunks that make up this array
104    chunks: Vec<ArrayChunk<T, D>>,
105
106    /// The global shape of the array
107    shape: Vec<usize>,
108
109    /// The unique ID of this distributed array
110    id: String,
111}
112
113impl<T, D> Debug for DistributedNdarray<T, D>
114where
115    T: Clone + Debug + 'static,
116    D: Dimension + Debug + 'static,
117{
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("DistributedNdarray")
120            .field("config", &self.config)
121            .field("chunks", &self.chunks.len())
122            .field("shape", &self.shape)
123            .field("id", &self.id)
124            .finish()
125    }
126}
127
128impl<T, D> DistributedNdarray<T, D>
129where
130    T: Clone + Send + Sync + 'static + num_traits::Zero + std::ops::Div<f64, Output = T> + Default,
131    D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
132{
133    /// Create a new distributed array from chunks.
134    #[must_use]
135    pub fn new(
136        chunks: Vec<ArrayChunk<T, D>>,
137        shape: Vec<usize>,
138        config: DistributedConfig,
139    ) -> Self {
140        let uuid = uuid::Uuid::new_v4();
141        let id = format!("uuid_{uuid}");
142        Self {
143            config,
144            chunks,
145            shape,
146            id,
147        }
148    }
149
150    /// Create a distributed array by splitting an existing array.
151    ///
152    /// Every chunk gets a genuinely disjoint slice of the original data,
153    /// split along axis 0 and recorded with its real starting offset in
154    /// `global_index`, so [`Self::to_array`] can reconstruct the original
155    /// array exactly. The communication backend (`config.backend`) is still
156    /// simulated (`nodeid` is a simple round-robin label, not a real network
157    /// destination) — only the actual data partitioning needed for correct
158    /// round-tripping is implemented here.
159    #[must_use]
160    pub fn from_array(array: &Array<T, D>, config: DistributedConfig) -> Self
161    where
162        T: Clone,
163    {
164        let shape = array.shape().to_vec();
165        let num_chunks = config.chunks.max(1);
166        let axis0_len = shape.first().copied().unwrap_or(1).max(1);
167        let rows_per_chunk = axis0_len.div_ceil(num_chunks).max(1);
168
169        let mut chunks = Vec::new();
170        let mut start = 0usize;
171        let mut nodeid = 0usize;
172        while start < axis0_len {
173            let end = (start + rows_per_chunk).min(axis0_len);
174            let chunk_data = array
175                .slice_axis(Axis(0), Slice::from(start..end))
176                .to_owned();
177
178            chunks.push(ArrayChunk {
179                data: chunk_data,
180                global_index: vec![start],
181                nodeid: nodeid % 3, // Simulate distribution across 3 nodes
182            });
183
184            start = end;
185            nodeid += 1;
186        }
187
188        Self::new(chunks, shape, config)
189    }
190
191    /// Get the number of chunks in this distributed array.
192    #[must_use]
193    pub fn num_chunks(&self) -> usize {
194        self.chunks.len()
195    }
196
197    /// Get the shape of this distributed array.
198    #[must_use]
199    pub fn shape(&self) -> &[usize] {
200        &self.shape
201    }
202
203    /// Get a reference to the chunks in this distributed array.
204    #[must_use]
205    pub fn chunks(&self) -> &[ArrayChunk<T, D>] {
206        &self.chunks
207    }
208
209    /// Convert this distributed array back to a regular array.
210    ///
211    /// Reconstructs the array by copying each chunk's real data back to the
212    /// axis-0 offset recorded in its `global_index` (set by
213    /// [`Self::from_array`]) — this only correctly inverts chunks produced by
214    /// `from_array`'s own axis-0 splitting; arbitrary externally-constructed
215    /// chunks (via [`Self::new`]) with different placement semantics are not
216    /// supported.
217    ///
218    /// # Errors
219    /// Returns `CoreError` if array conversion fails.
220    pub fn to_array(&self) -> CoreResult<Array<T, crate::ndarray::IxDyn>> {
221        // `T: Zero` is already guaranteed by this impl block's own bounds.
222        let mut result =
223            Array::<T, crate::ndarray::IxDyn>::zeros(crate::ndarray::IxDyn(&self.shape));
224
225        for chunk in &self.chunks {
226            let start = chunk.global_index.first().copied().unwrap_or(0);
227            let chunk_view = chunk.data.view().into_dyn();
228            let len = chunk_view.shape().first().copied().unwrap_or(0);
229            if len == 0 {
230                continue;
231            }
232            let mut dest = result.slice_axis_mut(Axis(0), Slice::from(start..start + len));
233            dest.assign(&chunk_view);
234        }
235
236        Ok(result)
237    }
238
239    /// Execute a function on each chunk in parallel.
240    #[must_use]
241    pub fn map<F, R>(&self, f: F) -> Vec<R>
242    where
243        F: Fn(&ArrayChunk<T, D>) -> R + Send + Sync,
244        R: Send + 'static,
245    {
246        // In a real distributed system, this would execute functions on different nodes
247        // For now, use a simple iterator instead of parallel execution
248        self.chunks.iter().map(f).collect()
249    }
250
251    /// Reduce the results of mapping a function across all chunks.
252    ///
253    /// # Panics
254    ///
255    /// Panics if the chunks collection is empty and no initial value can be reduced.
256    #[must_use]
257    pub fn map_reduce<F, R, G>(&self, map_fn: F, reducefn: G) -> R
258    where
259        F: Fn(&ArrayChunk<T, D>) -> R + Send + Sync,
260        G: Fn(R, R) -> R + Send + Sync,
261        R: Send + Clone + 'static,
262    {
263        // Map phase
264        let results = self.map(map_fn);
265
266        // Reduce phase
267        // In a real distributed system, this might happen on a single node
268        results
269            .into_iter()
270            .reduce(reducefn)
271            .expect("Operation failed")
272    }
273}
274
275impl<T, D> ArrayProtocol for DistributedNdarray<T, D>
276where
277    T: Clone
278        + Send
279        + Sync
280        + 'static
281        + num_traits::Zero
282        + std::ops::Div<f64, Output = T>
283        + Default
284        + std::ops::Add<Output = T>
285        + std::ops::Mul<Output = T>,
286    D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
287{
288    fn array_function(
289        &self,
290        func: &ArrayFunction,
291        _types: &[TypeId],
292        args: &[Box<dyn Any>],
293        kwargs: &HashMap<String, Box<dyn Any>>,
294    ) -> Result<Box<dyn Any>, NotImplemented> {
295        match func.name {
296            "scirs2::array_protocol::operations::sum" => {
297                // Distributed implementation of sum
298                let axis = kwargs.get("axis").and_then(|a| a.downcast_ref::<usize>());
299
300                if let Some(&ax) = axis {
301                    // Sum along a specific axis - use map-reduce across chunks
302                    // In a simplified implementation, we'll use a dummy array
303                    let dummy_array = self.chunks[0].data.clone();
304                    let sum_array = dummy_array.sum_axis(crate::ndarray::Axis(ax));
305
306                    // Create a new distributed array with the result
307                    Ok(Box::new(super::NdarrayWrapper::new(sum_array)))
308                } else {
309                    // Sum all elements using map-reduce
310                    let sum = self.map_reduce(|chunk| chunk.data.sum(), |a, b| a + b);
311                    Ok(Box::new(sum))
312                }
313            }
314            "scirs2::array_protocol::operations::mean" => {
315                // Distributed implementation of mean
316                // Get total sum across chunks
317                let sum = self.map_reduce(|chunk| chunk.data.sum(), |a, b| a + b);
318
319                // Calculate the total number of elements across all chunks
320                #[allow(clippy::cast_precision_loss)]
321                let count = self.shape.iter().product::<usize>() as f64;
322
323                // Calculate mean
324                let mean = sum / count;
325
326                Ok(Box::new(mean))
327            }
328            "scirs2::array_protocol::operations::add" => {
329                // Element-wise addition
330                if args.len() < 2 {
331                    return Err(NotImplemented);
332                }
333
334                // Try to get the second argument as a distributed array
335                if let Some(other) = args[1].downcast_ref::<Self>() {
336                    // Check shapes match
337                    if self.shape() != other.shape() {
338                        return Err(NotImplemented);
339                    }
340
341                    // Create a new distributed array with chunks that represent addition
342                    let mut new_chunks = Vec::with_capacity(self.chunks.len());
343
344                    // For simplicity, assume number of chunks matches
345                    // In a real implementation, we would handle different chunk distributions
346                    for (self_chunk, other_chunk) in self.chunks.iter().zip(other.chunks.iter()) {
347                        let result_data = &self_chunk.data + &other_chunk.data;
348                        new_chunks.push(ArrayChunk {
349                            data: result_data,
350                            global_index: self_chunk.global_index.clone(),
351                            nodeid: self_chunk.nodeid,
352                        });
353                    }
354
355                    let result = Self::new(new_chunks, self.shape.clone(), self.config.clone());
356
357                    return Ok(Box::new(result));
358                }
359
360                Err(NotImplemented)
361            }
362            "scirs2::array_protocol::operations::multiply" => {
363                // Element-wise multiplication
364                if args.len() < 2 {
365                    return Err(NotImplemented);
366                }
367
368                // Try to get the second argument as a distributed array
369                if let Some(other) = args[1].downcast_ref::<Self>() {
370                    // Check shapes match
371                    if self.shape() != other.shape() {
372                        return Err(NotImplemented);
373                    }
374
375                    // Create a new distributed array with chunks that represent multiplication
376                    let mut new_chunks = Vec::with_capacity(self.chunks.len());
377
378                    // For simplicity, assume number of chunks matches
379                    // In a real implementation, we would handle different chunk distributions
380                    for (self_chunk, other_chunk) in self.chunks.iter().zip(other.chunks.iter()) {
381                        let result_data = &self_chunk.data * &other_chunk.data;
382                        new_chunks.push(ArrayChunk {
383                            data: result_data,
384                            global_index: self_chunk.global_index.clone(),
385                            nodeid: self_chunk.nodeid,
386                        });
387                    }
388
389                    let result = Self::new(new_chunks, self.shape.clone(), self.config.clone());
390
391                    return Ok(Box::new(result));
392                }
393
394                Err(NotImplemented)
395            }
396            "scirs2::array_protocol::operations::matmul" => {
397                // Matrix multiplication
398                if args.len() < 2 {
399                    return Err(NotImplemented);
400                }
401
402                // We can only handle matrix multiplication for 2D arrays
403                if self.shape.len() != 2 {
404                    return Err(NotImplemented);
405                }
406
407                // Try to get the second argument as a distributed array
408                if let Some(other) = args[1].downcast_ref::<Self>() {
409                    // Check that shapes are compatible
410                    if self.shape.len() != 2
411                        || other.shape.len() != 2
412                        || self.shape[1] != other.shape[0]
413                    {
414                        return Err(NotImplemented);
415                    }
416
417                    // In a real implementation, we would perform a distributed matrix multiplication
418                    // For this simplified version, we'll return a dummy result with the correct shape
419
420                    let resultshape = vec![self.shape[0], other.shape[1]];
421
422                    // Create a dummy result array
423                    // Using a simpler approach with IxDyn directly
424                    let dummyshape = crate::ndarray::IxDyn(&resultshape);
425                    let dummy_array = Array::<T, crate::ndarray::IxDyn>::zeros(dummyshape);
426
427                    // Create a new distributed array with the dummy result
428                    let chunk = ArrayChunk {
429                        data: dummy_array,
430                        global_index: vec![0],
431                        nodeid: 0,
432                    };
433
434                    let result =
435                        DistributedNdarray::new(vec![chunk], resultshape, self.config.clone());
436
437                    return Ok(Box::new(result));
438                }
439
440                Err(NotImplemented)
441            }
442            "scirs2::array_protocol::operations::transpose" => {
443                // Transpose operation
444                if self.shape.len() != 2 {
445                    return Err(NotImplemented);
446                }
447
448                // Create a new shape for the transposed array
449                let transposedshape = vec![self.shape[1], self.shape[0]];
450
451                // In a real implementation, we would transpose each chunk and reconstruct
452                // the distributed array with the correct chunk distribution
453                // For this simplified version, we'll just create a single dummy chunk
454
455                // Create a dummy result array
456                // Using a simpler approach with IxDyn directly
457                let dummyshape = crate::ndarray::IxDyn(&transposedshape);
458                let dummy_array = Array::<T, crate::ndarray::IxDyn>::zeros(dummyshape);
459
460                // Create a new distributed array with the dummy result
461                let chunk = ArrayChunk {
462                    data: dummy_array,
463                    global_index: vec![0],
464                    nodeid: 0,
465                };
466
467                let result =
468                    DistributedNdarray::new(vec![chunk], transposedshape, self.config.clone());
469
470                Ok(Box::new(result))
471            }
472            "scirs2::array_protocol::operations::reshape" => {
473                // Reshape operation
474                if let Some(shape) = kwargs
475                    .get("shape")
476                    .and_then(|s| s.downcast_ref::<Vec<usize>>())
477                {
478                    // Check that total size matches
479                    let old_size: usize = self.shape.iter().product();
480                    let new_size: usize = shape.iter().product();
481
482                    if old_size != new_size {
483                        return Err(NotImplemented);
484                    }
485
486                    // In a real implementation, we would need to redistribute the chunks
487                    // For this simplified version, we'll just create a single dummy chunk
488
489                    // Create a dummy result array
490                    // Using a simpler approach with IxDyn directly
491                    let dummyshape = crate::ndarray::IxDyn(shape);
492                    let dummy_array = Array::<T, crate::ndarray::IxDyn>::zeros(dummyshape);
493
494                    // Create a new distributed array with the dummy result
495                    let chunk = ArrayChunk {
496                        data: dummy_array,
497                        global_index: vec![0],
498                        nodeid: 0,
499                    };
500
501                    let result =
502                        DistributedNdarray::new(vec![chunk], shape.clone(), self.config.clone());
503
504                    return Ok(Box::new(result));
505                }
506
507                Err(NotImplemented)
508            }
509            _ => Err(NotImplemented),
510        }
511    }
512
513    fn as_any(&self) -> &dyn Any {
514        self
515    }
516
517    fn shape(&self) -> &[usize] {
518        &self.shape
519    }
520
521    fn box_clone(&self) -> Box<dyn ArrayProtocol> {
522        Box::new(Self {
523            config: self.config.clone(),
524            chunks: self.chunks.clone(),
525            shape: self.shape.clone(),
526            id: self.id.clone(),
527        })
528    }
529}
530
531impl<T, D> DistributedArray for DistributedNdarray<T, D>
532where
533    T: Clone
534        + Send
535        + Sync
536        + 'static
537        + num_traits::Zero
538        + std::ops::Div<f64, Output = T>
539        + Default
540        + num_traits::One,
541    D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
542{
543    fn distribution_info(&self) -> HashMap<String, String> {
544        let mut info = HashMap::new();
545        info.insert("type".to_string(), "distributed_ndarray".to_string());
546        info.insert("chunks".to_string(), self.chunks.len().to_string());
547        info.insert("shape".to_string(), format!("{:?}", self.shape));
548        info.insert("id".to_string(), self.id.clone());
549        info.insert(
550            "strategy".to_string(),
551            format!("{:?}", self.config.strategy),
552        );
553        info.insert("backend".to_string(), format!("{:?}", self.config.backend));
554        info
555    }
556
557    /// # Errors
558    /// Returns `CoreError` if gathering fails.
559    fn gather(&self) -> CoreResult<Box<dyn ArrayProtocol>>
560    where
561        D: crate::ndarray::RemoveAxis,
562        T: Default + Clone + num_traits::One,
563    {
564        // In a real implementation, this would gather data from all nodes
565        // Get a properly shaped array with the right dimensions
566        let array_dyn = self.to_array()?;
567
568        // Wrap it in NdarrayWrapper
569        Ok(Box::new(super::NdarrayWrapper::new(array_dyn)))
570    }
571
572    /// # Errors
573    /// Returns `CoreError` if scattering fails.
574    fn scatter(&self, chunks: usize) -> CoreResult<Box<dyn DistributedArray>> {
575        // Create a new distributed array with a different number of chunks, but since
576        // to_array requires complex trait bounds, we'll do a simplified version
577        // that just creates a new array directly
578
579        let mut config = self.config.clone();
580        config.chunks = chunks;
581
582        // Create a new distributed array with the specified number of chunks
583        // For simplicity, we'll just create a copy of the existing chunks
584        let new_dist_array = Self {
585            config,
586            chunks: self.chunks.clone(),
587            shape: self.shape.clone(),
588            id: {
589                let uuid = uuid::Uuid::new_v4();
590                format!("uuid_{uuid}")
591            },
592        };
593
594        Ok(Box::new(new_dist_array))
595    }
596
597    fn is_distributed(&self) -> bool {
598        true
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use ::ndarray::Array2;
606
607    #[test]
608    fn test_distributed_ndarray_creation() {
609        let array = Array2::<f64>::ones((10, 5));
610        let config = DistributedConfig {
611            chunks: 3,
612            ..Default::default()
613        };
614
615        let dist_array = DistributedNdarray::from_array(&array, config);
616
617        // Check that the array was split correctly
618        assert_eq!(dist_array.num_chunks(), 3);
619        assert_eq!(dist_array.shape(), &[10, 5]);
620
621        // Chunks are genuinely disjoint row-wise slices of the original
622        // array (see `from_array`), so their total element count must equal
623        // the original array's size exactly — not a multiple of it.
624        let total_elements: usize = dist_array
625            .chunks()
626            .iter()
627            .map(|chunk| chunk.data.len())
628            .sum();
629        assert_eq!(total_elements, array.len());
630    }
631
632    #[test]
633    fn test_distributed_ndarray_to_array() {
634        // Non-constant data (not all-ones): a stub that fabricates a
635        // plausible-looking placeholder (e.g. all-ones, matching a former
636        // version of this test) would fail this check, whereas an all-ones
637        // input would not have caught it.
638        let array = Array2::from_shape_fn((10, 5), |(i, j)| (i * 5 + j) as f64);
639        let config = DistributedConfig {
640            chunks: 3,
641            ..Default::default()
642        };
643
644        let dist_array = DistributedNdarray::from_array(&array, config);
645
646        // Convert back to a regular array
647        let result = dist_array.to_array().expect("Operation failed");
648
649        // Check that the result matches the original array's shape and content.
650        assert_eq!(result.shape(), array.shape());
651        assert_eq!(result, array.into_dyn());
652    }
653
654    #[test]
655    fn test_distributed_ndarray_map_reduce() {
656        let array = Array2::<f64>::ones((10, 5));
657        let config = DistributedConfig {
658            chunks: 3,
659            ..Default::default()
660        };
661
662        let dist_array = DistributedNdarray::from_array(&array, config);
663
664        // Chunks are disjoint slices, so summing across all of them via
665        // map_reduce should reconstruct the original array's total sum
666        // exactly once — not a multiple of it.
667        let expected_sum = array.sum();
668
669        // Calculate the sum using map_reduce
670        let sum = dist_array.map_reduce(|chunk| chunk.data.sum(), |a, b| a + b);
671
672        assert_eq!(sum, expected_sum);
673    }
674}