Skip to main content

scirs2_core/ufuncs/
core.rs

1//! Core Universal Function implementation
2//!
3//! This module provides the foundational infrastructure for the universal function
4//! (ufunc) system, including trait definitions, registration, and dispatching.
5
6use ::ndarray::{
7    Array, ArrayBase, ArrayView, ArrayViewMut, Data, DataMut, Dimension, Ix1, IxDyn, RawData,
8};
9use once_cell::sync::Lazy;
10use std::collections::HashMap;
11use std::sync::RwLock;
12
13/// Enum defining the different kinds of universal functions
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum UFuncKind {
16    /// Unary function (takes one input array)
17    Unary,
18    /// Binary function (takes two input arrays)
19    Binary,
20    /// Reduction function (reduces array along an axis)
21    Reduction,
22}
23
24/// Trait for universal function implementation
25pub trait UFunc: Send + Sync {
26    /// Get the name of the ufunc
27    fn name(&self) -> &str;
28
29    /// Get the kind of ufunc (unary, binary, reduction)
30    fn kind(&self) -> UFuncKind;
31
32    /// Apply the ufunc to array(s) and store the result in the output array
33    fn apply(
34        &self,
35        inputs: &[ArrayView<f64, IxDyn>],
36        output: &mut ArrayViewMut<f64, IxDyn>,
37    ) -> Result<(), &'static str>;
38
39    /// Use SIMD acceleration if available
40    fn use_simd(&self) -> bool {
41        #[cfg(feature = "simd")]
42        return true;
43
44        #[cfg(not(feature = "simd"))]
45        return false;
46    }
47
48    /// Use parallel execution if available
49    fn use_parallel(&self) -> bool {
50        #[cfg(feature = "parallel")]
51        return true;
52
53        #[cfg(not(feature = "parallel"))]
54        return false;
55    }
56}
57
58/// Global registry for universal functions
59static UFUNC_REGISTRY: Lazy<RwLock<HashMap<String, Box<dyn UFunc>>>> =
60    Lazy::new(|| RwLock::new(HashMap::new()));
61
62/// Register a universal function in the global registry
63#[allow(dead_code)]
64pub fn register_ufunc(ufunc: Box<dyn UFunc>) -> Result<(), &'static str> {
65    let name = ufunc.name().to_string();
66
67    let mut registry = UFUNC_REGISTRY.write().expect("Operation failed");
68
69    if registry.contains_key(&name) {
70        return Err("UFunc with this name already exists");
71    }
72
73    registry.insert(name, ufunc);
74    Ok(())
75}
76
77/// Get a universal function from the registry by name
78#[allow(dead_code)]
79pub fn get_ufunc(name: &str) -> Option<Box<dyn UFunc>> {
80    let registry = UFUNC_REGISTRY.read().expect("Operation failed");
81
82    registry.get(name).map(|ufunc| {
83        // Clone the UFunc implementation
84        let ufunc_clone: Box<dyn UFunc> = Box::new(UFuncWrapper {
85            name: ufunc.name().to_string(),
86            kind: ufunc.kind(),
87        });
88
89        ufunc_clone
90    })
91}
92
93/// A wrapper for UFunc implementations to allow cloning
94struct UFuncWrapper {
95    name: String,
96    kind: UFuncKind,
97}
98
99impl UFunc for UFuncWrapper {
100    fn name(&self) -> &str {
101        &self.name
102    }
103
104    fn kind(&self) -> UFuncKind {
105        self.kind
106    }
107
108    fn apply(
109        &self,
110        inputs: &[ArrayView<f64, IxDyn>],
111        output: &mut ArrayViewMut<f64, IxDyn>,
112    ) -> Result<(), &'static str> {
113        // This is a wrapper that delegates to the actual implementation
114        // Get the real UFunc from the registry
115        let registry = UFUNC_REGISTRY.read().expect("Operation failed");
116
117        if let Some(real_ufunc) = registry.get(&self.name) {
118            real_ufunc.apply(inputs, output)
119        } else {
120            Err("UFunc not found in registry")
121        }
122    }
123}
124
125/// Helper function to apply a unary operation element-wise
126#[allow(dead_code)]
127pub fn apply_unary<T, F, O, S1, S2, D>(
128    input: &ArrayBase<S1, D>,
129    output: &mut ArrayBase<S2, D>,
130    op: F,
131) -> Result<(), &'static str>
132where
133    S1: Data<Elem = T>,
134    S2: Data<Elem = O> + DataMut,
135    T: Clone + Send + Sync,
136    O: Clone + Send + Sync,
137    F: Fn(&T) -> O + Send + Sync,
138    D: Dimension,
139{
140    // Check that the output shape matches the input shape
141    if input.shape() != output.shape() {
142        return Err("Output shape must match input shape for unary operations");
143    }
144
145    // Apply the operation element-wise
146    #[cfg(feature = "parallel")]
147    {
148        use crate::parallel_ops::*;
149        // For simplicity, we convert to vectors, process in parallel, then convert back
150        // A more efficient implementation would operate directly on array iterators
151        let input_slice = input.as_slice().expect("Operation failed");
152        let output_slice = output.as_slice_mut().expect("Operation failed");
153
154        output_slice
155            .par_iter_mut()
156            .enumerate()
157            .for_each(|(i, out)| {
158                let in_val = unsafe { input_slice.get_unchecked(i) };
159                *out = op(in_val);
160            });
161    }
162
163    #[cfg(not(feature = "parallel"))]
164    {
165        output.iter_mut().zip(input.iter()).for_each(|(out, inp)| {
166            *out = op(inp);
167        });
168    }
169
170    Ok(())
171}
172
173/// Helper function to apply a binary operation element-wise with broadcasting
174#[allow(dead_code)]
175pub fn apply_binary<T, F, O, S1, S2, S3, D>(
176    input1: &ArrayBase<S1, D>,
177    input2: &ArrayBase<S2, D>,
178    output: &mut ArrayBase<S3, D>,
179    op: F,
180) -> Result<(), &'static str>
181where
182    S1: Data<Elem = T>,
183    S2: Data<Elem = T>,
184    S3: Data<Elem = O> + DataMut,
185    T: Clone + Send + Sync,
186    O: Clone + Send + Sync,
187    F: Fn(&T, &T) -> O + Send + Sync,
188    D: Dimension,
189{
190    // This is a simplified implementation without full broadcasting support
191    // For a complete implementation, we would need to use the broadcasting module
192
193    // For now, just check that all arrays have the same shape
194    if input1.shape() != output.shape() || input2.shape() != output.shape() {
195        return Err("All arrays must have the same shape for binary operations");
196    }
197
198    // Apply the operation element-wise
199    #[cfg(feature = "parallel")]
200    {
201        use crate::parallel_ops::*;
202
203        let input1_slice = input1.as_slice().expect("Operation failed");
204        let input2_slice = input2.as_slice().expect("Operation failed");
205        let output_slice = output.as_slice_mut().expect("Operation failed");
206
207        output_slice
208            .par_iter_mut()
209            .enumerate()
210            .for_each(|(i, out)| {
211                let in1 = unsafe { input1_slice.get_unchecked(i) };
212                let in2 = unsafe { input2_slice.get_unchecked(i) };
213                *out = op(in1, in2);
214            });
215    }
216
217    #[cfg(not(feature = "parallel"))]
218    {
219        output
220            .iter_mut()
221            .zip(input1.iter().zip(input2.iter()))
222            .for_each(|(out, (in1, in2))| {
223                *out = op(in1, in2);
224            });
225    }
226
227    Ok(())
228}
229
230/// Helper function to apply a reduction operation along an axis
231#[allow(dead_code)]
232pub fn apply_reduction<T, F, S1, S2, D>(
233    input: &ArrayBase<S1, D>,
234    output: &mut ArrayBase<S2, Ix1>,
235    axis: Option<usize>,
236    initial: Option<T>,
237    op: F,
238) -> Result<(), &'static str>
239where
240    S1: Data<Elem = T>,
241    S2: Data<Elem = T> + DataMut,
242    T: Clone + Send + Sync,
243    F: Fn(T, &T) -> T + Send + Sync,
244    D: Dimension,
245{
246    // This is a simplified implementation for reduction along an axis
247    // In a complete implementation, we would handle all reduction patterns
248
249    match axis {
250        Some(ax) => {
251            // Reduction along a specific axis
252            if ax >= input.ndim() {
253                return Err("Axis index out of bounds");
254            }
255
256            let axis_size = input.len_of(crate::ndarray::Axis(ax));
257            let othershape = input
258                .shape()
259                .iter()
260                .enumerate()
261                .filter_map(|(i, &s)| if i != ax { Some(s) } else { None })
262                .collect::<Vec<_>>();
263
264            // Check that the output shape matches the expected shape
265            if output.shape() != othershape.as_slice() {
266                return Err("Output shape does not match the expected shape for reduction");
267            }
268
269            // For simplicity, this implementation only handles 2D arrays and axis 0 or 1
270            // A complete implementation would handle arbitrary dimensions
271            if input.ndim() != 2 {
272                return Err("This simplified implementation only supports 2D arrays");
273            }
274
275            let (rows, cols) = (input.shape()[0], input.shape()[1]);
276
277            // Convert to slice for linear indexing
278            if let Some(input_slice) = input.as_slice() {
279                if ax == 0 {
280                    // Reduce along rows
281                    for j in 0..cols {
282                        let mut acc = initial.clone().unwrap_or_else(|| {
283                            // Get first element in this column
284                            input_slice[j].clone()
285                        });
286                        let start_i = if initial.is_some() { 0 } else { 1 };
287                        for i in start_i..rows {
288                            // Use linear indexing for 2D array in row-major order
289                            let val = &input_slice[i * cols + j];
290                            acc = op(acc, val);
291                        }
292                        output[j] = acc;
293                    }
294                } else {
295                    // Reduce along columns
296                    for i in 0..rows {
297                        let mut acc = initial.clone().unwrap_or_else(|| {
298                            // Get first element in this row
299                            input_slice[i * cols].clone()
300                        });
301                        let start_j = if initial.is_some() { 0 } else { 1 };
302                        for j in start_j..cols {
303                            // Use linear indexing for 2D array in row-major order
304                            let val = &input_slice[i * cols + j];
305                            acc = op(acc, val);
306                        }
307                        output[i] = acc;
308                    }
309                }
310            } else {
311                return Err("Input array is not contiguous");
312            }
313        }
314        None => {
315            // Reduction over the entire array
316            if output.len() != 1 {
317                return Err("Output array must have length 1 for full reduction");
318            }
319
320            let mut iter = input.iter();
321            let mut acc = initial
322                .clone()
323                .unwrap_or_else(|| iter.next().expect("Operation failed").clone());
324
325            for val in iter {
326                acc = op(acc, val);
327            }
328
329            output[0] = acc;
330        }
331    }
332
333    Ok(())
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use ndarray::{array, Array1, Array2};
340
341    // Create a simple unary ufunc for testing
342    struct TestUnaryUFunc;
343
344    impl UFunc for TestUnaryUFunc {
345        fn name(&self) -> &str {
346            "test_unary"
347        }
348
349        fn kind(&self) -> UFuncKind {
350            UFuncKind::Unary
351        }
352
353        fn apply(
354            &self,
355            inputs: &[ArrayView<f64, IxDyn>],
356            output: &mut ArrayViewMut<f64, IxDyn>,
357        ) -> Result<(), &'static str> {
358            if inputs.len() != 1 {
359                return Err("Unary ufunc requires exactly one input array");
360            }
361
362            // Square each element
363            let input = &inputs[0];
364            for (inp, out) in input.iter().zip(output.iter_mut()) {
365                *out = inp * inp;
366            }
367            Ok(())
368        }
369    }
370
371    // Create a simple binary ufunc for testing
372    struct TestBinaryUFunc;
373
374    impl UFunc for TestBinaryUFunc {
375        fn name(&self) -> &str {
376            "test_binary"
377        }
378
379        fn kind(&self) -> UFuncKind {
380            UFuncKind::Binary
381        }
382
383        fn apply(
384            &self,
385            inputs: &[ArrayView<f64, IxDyn>],
386            output: &mut ArrayViewMut<f64, IxDyn>,
387        ) -> Result<(), &'static str> {
388            if inputs.len() != 2 {
389                return Err("Binary ufunc requires exactly two input arrays");
390            }
391
392            // Add the elements
393            let input1 = &inputs[0];
394            let input2 = &inputs[1];
395            for ((a, b), out) in input1.iter().zip(input2.iter()).zip(output.iter_mut()) {
396                *out = a + b;
397            }
398            Ok(())
399        }
400    }
401
402    #[test]
403    fn test_ufunc_registry() {
404        // Register a test ufunc
405        let ufunc = Box::new(TestUnaryUFunc);
406        register_ufunc(ufunc).expect("Operation failed");
407
408        // Get the ufunc from the registry
409        let ufunc = get_ufunc("test_unary").expect("Operation failed");
410        assert_eq!(ufunc.name(), "test_unary");
411        assert_eq!(ufunc.kind(), UFuncKind::Unary);
412    }
413
414    #[test]
415    fn test_apply_unary() {
416        let input = array![1.0, 2.0, 3.0, 4.0];
417        let mut output = Array1::<f64>::zeros(4);
418
419        apply_unary(&input, &mut output, |&x: &f64| x * x).expect("Operation failed");
420
421        assert_eq!(output, array![1.0, 4.0, 9.0, 16.0]);
422    }
423
424    #[test]
425    fn test_apply_binary() {
426        let input1 = array![1.0, 2.0, 3.0, 4.0];
427        let input2 = array![5.0, 6.0, 7.0, 8.0];
428        let mut output = Array1::<f64>::zeros(4);
429
430        apply_binary(&input1, &input2, &mut output, |&x: &f64, &y: &f64| x + y)
431            .expect("Operation failed");
432
433        assert_eq!(output, array![6.0, 8.0, 10.0, 12.0]);
434    }
435
436    #[test]
437    fn test_apply_reduction() {
438        let input = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
439
440        // Reduction along axis 0 (sum of columns)
441        let mut output = Array1::<f64>::zeros(3);
442        apply_reduction(&input, &mut output, Some(0), Some(0.0), |acc, &x| acc + x)
443            .expect("Operation failed");
444        assert_eq!(output, array![5.0, 7.0, 9.0]);
445
446        // Reduction along axis 1 (sum of rows)
447        let mut output = Array1::<f64>::zeros(2);
448        apply_reduction(&input, &mut output, Some(1), Some(0.0), |acc, &x| acc + x)
449            .expect("Operation failed");
450        assert_eq!(output, array![6.0, 15.0]);
451
452        // Full reduction (sum of all elements)
453        let mut output = Array1::<f64>::zeros(1);
454        apply_reduction(&input, &mut output, None, Some(0.0), |acc, &x| acc + x)
455            .expect("Operation failed");
456        assert_eq!(output, array![21.0]);
457    }
458}