Skip to main content

scirs2_core/array_protocol/
ml_ops.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//! Machine learning operations using the array protocol.
8//!
9//! This module provides implementations of various machine learning operations
10//! using the array protocol, such as activation functions, convolution, and
11//! pooling.
12
13use std::any::{Any, TypeId};
14use std::collections::HashMap;
15
16use ::ndarray::{Array, Axis, Ix1, Ix2, Ix3, Ix4, IxDyn};
17use rand::{Rng, RngExt, SeedableRng};
18
19use crate::array_protocol::operations::OperationError;
20use crate::array_protocol::{
21    array_function_dispatch, get_implementing_args, ArrayProtocol, NdarrayWrapper,
22};
23
24/// Activation function types.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum ActivationFunc {
27    /// Rectified Linear Unit: f(x) = max(0, x)
28    ReLU,
29
30    /// Sigmoid function: f(x) = 1 / (1 + exp(-x))
31    Sigmoid,
32
33    /// Hyperbolic tangent: f(x) = tanh(x)
34    Tanh,
35
36    /// Softmax function (applied along the last dimension)
37    Softmax,
38
39    /// Leaky ReLU: f(x) = max(alpha * x, x)
40    LeakyReLU(f64),
41}
42
43/// Apply an activation function to an array.
44#[allow(dead_code)]
45fn apply_activation(
46    x: &crate::ndarray::ArrayBase<crate::ndarray::ViewRepr<&f64>, IxDyn>,
47    func: ActivationFunc,
48) -> Array<f64, IxDyn> {
49    match func {
50        ActivationFunc::ReLU => x.mapv(|v| v.max(0.0)),
51        ActivationFunc::Sigmoid => x.mapv(|v| 1.0 / (1.0 + (-v).exp())),
52        ActivationFunc::Tanh => x.mapv(|v| v.tanh()),
53        ActivationFunc::LeakyReLU(alpha) => x.mapv(|v| if v > 0.0 { v } else { alpha * v }),
54        ActivationFunc::Softmax => {
55            // Apply softmax along the last dimension
56            let mut result = Array::zeros(x.raw_dim());
57
58            // Iterate over all but the last dimension
59            let last_dim = x.ndim() - 1;
60            let _last_dim_len = x.shape()[last_dim];
61
62            if x.ndim() == 1 {
63                // Simple 1D case
64                let max_val = x.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
65                let exp_x = x.mapv(|v| (v - max_val).exp());
66                let sum_exp = exp_x.sum();
67                result.assign(&(exp_x / sum_exp));
68            } else {
69                // Multi-dimensional case
70                for (i, mut slice) in result.lanes_mut(Axis(last_dim)).into_iter().enumerate() {
71                    // Use index_axis to get the ith slice along the last dimension
72                    let x_slice = x.index_axis(Axis(last_dim), i);
73                    let max_val = x_slice.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
74                    let exp_x = x_slice.mapv(|v| (v - max_val).exp());
75                    let sum_exp = exp_x.sum();
76                    slice.assign(&(exp_x / sum_exp));
77                }
78            }
79
80            result
81        }
82    }
83}
84
85// Define machine learning operations using the array protocol
86
87array_function_dispatch!(
88    fn activation(
89        x: &dyn ArrayProtocol,
90        func: ActivationFunc,
91    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
92        // Get implementing args
93        let boxed_x = Box::new(x.box_clone());
94        let boxed_args: Vec<Box<dyn Any>> = vec![boxed_x];
95        let implementing_args =
96            get_implementing_args("scirs2::array_protocol::ml_ops::activation", &boxed_args);
97        if implementing_args.is_empty() {
98            // Fallback implementation for ndarray types
99            if let Some(x_array) = x.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>() {
100                let x_array = x_array.as_array();
101                let result = apply_activation(&x_array.view(), func);
102                return Ok(Box::new(NdarrayWrapper::new(result)));
103            }
104            return Err(OperationError::NotImplemented(
105                "activation not implemented for this array type".to_string(),
106            ));
107        }
108
109        // Delegate to the implementation
110        let array_ref = implementing_args[0].1;
111
112        let result = array_ref.array_function(
113            &crate::array_protocol::ArrayFunction::new(
114                "scirs2::array_protocol::ml_ops::activation",
115            ),
116            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
117            &[Box::new(x.box_clone())],
118            &HashMap::new(),
119        )?;
120
121        // Try to downcast the result
122        match result.downcast::<Box<dyn ArrayProtocol>>() {
123            Ok(array) => Ok(*array),
124            Err(_) => Err(OperationError::Other(
125                "Failed to downcast array_function result".to_string(),
126            )),
127        }
128    },
129    "scirs2::array_protocol::ml, ops: activation"
130);
131
132array_function_dispatch!(
133    fn conv2d(
134        input: &dyn ArrayProtocol,
135        filters: &dyn ArrayProtocol,
136        stride: (usize, usize),
137        padding: (usize, usize),
138    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
139        // Get implementing args
140        let boxed_input = Box::new(input.box_clone());
141        let boxed_filters = Box::new(filters.box_clone());
142        let boxed_args: Vec<Box<dyn Any>> = vec![boxed_input, boxed_filters];
143        let implementing_args =
144            get_implementing_args("scirs2::array_protocol::ml_ops::conv2d", &boxed_args);
145        if implementing_args.is_empty() {
146            // Fallback implementation for ndarray types
147            // This is a simplified implementation - in practice, convolution is much more complex
148            if let (Some(inputarray), Some(filters_array)) = (
149                input.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
150                filters.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
151            ) {
152                let input = inputarray.as_array();
153                let filters = filters_array.as_array();
154
155                // Get dimensions
156                let batch_size = input.shape()[0];
157                let input_height = input.shape()[1];
158                let input_width = input.shape()[2];
159                let input_channels = input.shape()[3];
160
161                let filter_height = filters.shape()[0];
162                let filter_width = filters.shape()[1];
163                let filter_in_channels = filters.shape()[2];
164                let filter_out_channels = filters.shape()[3];
165
166                // Check dimensions
167                if input_channels != filter_in_channels {
168                    return Err(OperationError::ShapeMismatch(format!(
169                        "Input channels ({input_channels}) doesn't match filter input channels ({filter_in_channels})"
170                    )));
171                }
172
173                // Calculate output dimensions
174                let out_height = (input_height - filter_height + 2 * padding.0) / stride.0 + 1;
175                let out_width = (input_width - filter_width + 2 * padding.1) / stride.1 + 1;
176
177                // Create output array
178                let mut output: Array<f64, Ix4> =
179                    Array::zeros((batch_size, out_height, out_width, filter_out_channels));
180
181                // Perform convolution using basic sliding window approach
182                for b in 0..batch_size {
183                    for out_c in 0..filter_out_channels {
184                        for out_h in 0..out_height {
185                            for out_w in 0..out_width {
186                                let mut sum = 0.0;
187
188                                // Convolution over the filter window
189                                for f_h in 0..filter_height {
190                                    for f_w in 0..filter_width {
191                                        for in_c in 0..input_channels {
192                                            // Calculate input coordinates with padding
193                                            let in_h = (out_h * stride.0) as i32 + f_h as i32
194                                                - padding.0 as i32;
195                                            let in_w = (out_w * stride.1) as i32 + f_w as i32
196                                                - padding.1 as i32;
197
198                                            // Check bounds (zero padding)
199                                            if in_h >= 0
200                                                && in_h < input_height as i32
201                                                && in_w >= 0
202                                                && in_w < input_width as i32
203                                            {
204                                                let input_val =
205                                                    input[[b, in_h as usize, in_w as usize, in_c]];
206                                                let filter_val = filters[[f_h, f_w, in_c, out_c]];
207                                                sum += input_val * filter_val;
208                                            }
209                                        }
210                                    }
211                                }
212
213                                output[[b, out_h, out_w, out_c]] = sum;
214                            }
215                        }
216                    }
217                }
218
219                return Ok(Box::new(NdarrayWrapper::new(output)));
220            }
221            return Err(OperationError::NotImplemented(
222                "conv2d not implemented for these array types".to_string(),
223            ));
224        }
225
226        // Delegate to the implementation
227        let mut kwargs = HashMap::new();
228        kwargs.insert("stride".to_string(), Box::new(stride) as Box<dyn Any>);
229        kwargs.insert("padding".to_string(), Box::new(padding) as Box<dyn Any>);
230
231        let array_ref = implementing_args[0].1;
232
233        let result = array_ref.array_function(
234            &crate::array_protocol::ArrayFunction::new("scirs2::array_protocol::ml_ops::conv2d"),
235            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
236            &[Box::new(input.box_clone()), Box::new(filters.box_clone())],
237            &kwargs,
238        )?;
239
240        // Try to downcast the result
241        match result.downcast::<Box<dyn ArrayProtocol>>() {
242            Ok(array) => Ok(*array),
243            Err(_) => Err(OperationError::Other(
244                "Failed to downcast array_function result".to_string(),
245            )),
246        }
247    },
248    "scirs2::array_protocol::ml, ops: conv2d"
249);
250
251array_function_dispatch!(
252    fn max_pool2d(
253        input: &dyn ArrayProtocol,
254        kernel_size: (usize, usize),
255        stride: (usize, usize),
256        padding: (usize, usize),
257    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
258        // Get implementing args
259        let boxed_input = Box::new(input.box_clone());
260        let boxed_args: Vec<Box<dyn Any>> = vec![boxed_input];
261        let implementing_args =
262            get_implementing_args("scirs2::array_protocol::ml_ops::max_pool2d", &boxed_args);
263        if implementing_args.is_empty() {
264            // Fallback implementation for ndarray types
265            if let Some(inputarray) = input.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>() {
266                let input = inputarray.as_array();
267
268                // Get dimensions
269                let batch_size = input.shape()[0];
270                let input_height = input.shape()[1];
271                let input_width = input.shape()[2];
272                let channels = input.shape()[3];
273
274                // Calculate output dimensions
275                let out_height = (input_height - kernel_size.0 + 2 * padding.0) / stride.0 + 1;
276                let out_width = (input_width - kernel_size.1 + 2 * padding.1) / stride.1 + 1;
277
278                // Create output array
279                let mut output: Array<f64, Ix4> =
280                    Array::zeros((batch_size, out_height, out_width, channels));
281
282                // Perform max pooling
283                for b in 0..batch_size {
284                    for c in 0..channels {
285                        for out_h in 0..out_height {
286                            for out_w in 0..out_width {
287                                let mut max_val = f64::NEG_INFINITY;
288
289                                // Pool over the kernel window
290                                for k_h in 0..kernel_size.0 {
291                                    for k_w in 0..kernel_size.1 {
292                                        // Calculate input coordinates with padding
293                                        let in_h = (out_h * stride.0) as i32 + k_h as i32
294                                            - padding.0 as i32;
295                                        let in_w = (out_w * stride.1) as i32 + k_w as i32
296                                            - padding.1 as i32;
297
298                                        // Check bounds
299                                        if in_h >= 0
300                                            && in_h < input_height as i32
301                                            && in_w >= 0
302                                            && in_w < input_width as i32
303                                        {
304                                            let val = input[[b, in_h as usize, in_w as usize, c]];
305                                            if val > max_val {
306                                                max_val = val;
307                                            }
308                                        }
309                                    }
310                                }
311
312                                // Use 0.0 if no valid values found (due to padding)
313                                output[[b, out_h, out_w, c]] = if max_val == f64::NEG_INFINITY {
314                                    0.0
315                                } else {
316                                    max_val
317                                };
318                            }
319                        }
320                    }
321                }
322
323                return Ok(Box::new(NdarrayWrapper::new(output)));
324            }
325            return Err(OperationError::NotImplemented(
326                "max_pool2d not implemented for this array type".to_string(),
327            ));
328        }
329
330        // Delegate to the implementation
331        let mut kwargs = HashMap::new();
332        kwargs.insert(
333            "kernel_size".to_string(),
334            Box::new(kernel_size) as Box<dyn Any>,
335        );
336        kwargs.insert("stride".to_string(), Box::new(stride) as Box<dyn Any>);
337        kwargs.insert("padding".to_string(), Box::new(padding) as Box<dyn Any>);
338
339        let array_ref = implementing_args[0].1;
340
341        let result = array_ref.array_function(
342            &crate::array_protocol::ArrayFunction::new(
343                "scirs2::array_protocol::ml_ops::max_pool2d",
344            ),
345            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
346            &[Box::new(input.box_clone())],
347            &kwargs,
348        )?;
349
350        // Try to downcast the result
351        match result.downcast::<Box<dyn ArrayProtocol>>() {
352            Ok(array) => Ok(*array),
353            Err(_) => Err(OperationError::Other(
354                "Failed to downcast array_function result".to_string(),
355            )),
356        }
357    },
358    "scirs2::array_protocol::ml, ops: max_pool2d"
359);
360
361array_function_dispatch!(
362    fn batch_norm(
363        input: &dyn ArrayProtocol,
364        scale: &dyn ArrayProtocol,
365        offset: &dyn ArrayProtocol,
366        mean: &dyn ArrayProtocol,
367        variance: &dyn ArrayProtocol,
368        epsilon: f64,
369    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
370        // Get implementing args - convert to Box<dyn Any>
371        let boxed_args: Vec<Box<dyn Any>> = vec![
372            Box::new(input.box_clone()),
373            Box::new(scale.box_clone()),
374            Box::new(offset.box_clone()),
375            Box::new(mean.box_clone()),
376            Box::new(variance.box_clone()),
377        ];
378        let implementing_args =
379            get_implementing_args("scirs2::array_protocol::ml_ops::batch_norm", &boxed_args);
380        if implementing_args.is_empty() {
381            // Fallback implementation for ndarray types
382            if let (
383                Some(inputarray),
384                Some(scale_array),
385                Some(offset_array),
386                Some(mean_array),
387                Some(variance_array),
388            ) = (
389                input.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
390                scale.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>(),
391                offset.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>(),
392                mean.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>(),
393                variance
394                    .as_any()
395                    .downcast_ref::<NdarrayWrapper<f64, IxDyn>>(),
396            ) {
397                let input = inputarray.as_array();
398                let scale = scale_array.as_array();
399                let offset = offset_array.as_array();
400                let mean = mean_array.as_array();
401                let variance = variance_array.as_array();
402
403                // Get dimensions
404                let _batch_size = input.shape()[0];
405                let _height = input.shape()[1];
406                let _width = input.shape()[2];
407                let channels = input.shape()[3];
408
409                // Check dimensions
410                if scale.shape()[0] != channels
411                    || offset.shape()[0] != channels
412                    || mean.shape()[0] != channels
413                    || variance.shape()[0] != channels
414                {
415                    return Err(OperationError::ShapeMismatch(
416                        "Scale, offset, mean, and variance must match the number of channels"
417                            .to_string(),
418                    ));
419                }
420
421                // Create output array with same shape as input
422                let mut output: Array<f64, Ix4> = Array::zeros(input.raw_dim());
423
424                // Perform batch normalization
425                // For each channel, normalize using the formula:
426                // y = scale * (x - mean) / sqrt(variance + epsilon) + offset
427
428                let batch_size = input.shape()[0];
429                let _height = input.shape()[1];
430                let _width = input.shape()[2];
431
432                for b in 0..batch_size {
433                    for h in 0.._height {
434                        for w in 0.._width {
435                            for c in 0..channels {
436                                let x = input[[b, h, w, c]];
437                                let m = mean[[c]];
438                                let v = variance[[c]];
439                                let s = scale[[c]];
440                                let o = offset[[c]];
441
442                                // Normalize: (x - mean) / sqrt(variance + epsilon)
443                                let normalized = (x - m) / (v + epsilon).sqrt();
444
445                                // Scale and shift: scale * normalized + offset
446                                let result = s * normalized + o;
447
448                                output[[b, h, w, c]] = result;
449                            }
450                        }
451                    }
452                }
453
454                return Ok(Box::new(NdarrayWrapper::new(output)));
455            }
456            return Err(OperationError::NotImplemented(
457                "batch_norm not implemented for these array types".to_string(),
458            ));
459        }
460
461        // Delegate to the implementation
462        let mut kwargs = HashMap::new();
463        kwargs.insert("epsilon".to_string(), Box::new(epsilon) as Box<dyn Any>);
464
465        let array_ref = implementing_args[0].1;
466
467        let result = array_ref.array_function(
468            &crate::array_protocol::ArrayFunction::new(
469                "scirs2::array_protocol::ml_ops::batch_norm",
470            ),
471            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
472            &[
473                Box::new(input.box_clone()),
474                Box::new(scale.box_clone()),
475                Box::new(offset.box_clone()),
476                Box::new(mean.box_clone()),
477                Box::new(variance.box_clone()),
478            ],
479            &kwargs,
480        )?;
481
482        // Try to downcast the result
483        match result.downcast::<Box<dyn ArrayProtocol>>() {
484            Ok(array) => Ok(*array),
485            Err(_) => Err(OperationError::Other(
486                "Failed to downcast array_function result".to_string(),
487            )),
488        }
489    },
490    "scirs2::array_protocol::ml, ops: batch_norm"
491);
492
493array_function_dispatch!(
494    fn cross_entropy(
495        logits: &dyn ArrayProtocol,
496        labels: &dyn ArrayProtocol,
497        reduction: &str,
498    ) -> Result<Box<dyn Any>, OperationError> {
499        // Get implementing args
500        let boxed_args: Vec<Box<dyn Any>> =
501            vec![Box::new(logits.box_clone()), Box::new(labels.box_clone())];
502        let implementing_args =
503            get_implementing_args("scirs2::array_protocol::ml_ops::cross_entropy", &boxed_args);
504        if implementing_args.is_empty() {
505            // Fallback implementation for ndarray types
506            if let (Some(logits_array), Some(labels_array)) = (
507                logits.as_any().downcast_ref::<NdarrayWrapper<f64, Ix2>>(),
508                labels.as_any().downcast_ref::<NdarrayWrapper<f64, Ix2>>(),
509            ) {
510                let logits = logits_array.as_array();
511                let labels = labels_array.as_array();
512
513                // Check shapes
514                if logits.shape() != labels.shape() {
515                    return Err(OperationError::ShapeMismatch(format!(
516                        "Logits shape {logitsshape:?} doesn't match labels shape {labelsshape:?}",
517                        logitsshape = logits.shape(),
518                        labelsshape = labels.shape()
519                    )));
520                }
521
522                // Apply softmax to logits
523                let mut softmax = Array::zeros(logits.raw_dim());
524
525                // For each sample in the batch
526                for (i, sample) in logits.outer_iter().enumerate() {
527                    let max_val = sample.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
528                    let exp_x = sample.mapv(|v| (v - max_val).exp());
529                    let sum_exp = exp_x.sum();
530
531                    for (j, val) in exp_x.iter().enumerate() {
532                        softmax[[i, j]] = val / sum_exp;
533                    }
534                }
535
536                // Compute cross-entropy loss
537                // loss = -sum(labels * log(softmax))
538                let mut sample_losses = Array::zeros(logits.shape()[0]);
539
540                for (i, (s, l)) in softmax.outer_iter().zip(labels.outer_iter()).enumerate() {
541                    let mut loss = 0.0;
542                    for (s_val, l_val) in s.iter().zip(l.iter()) {
543                        // Add small epsilon to avoid log(0)
544                        loss -= l_val * (s_val + 1e-10).ln();
545                    }
546                    sample_losses[i] = loss;
547                }
548
549                // Apply reduction
550                let loss = match reduction {
551                    "none" => sample_losses,
552                    "mean" => {
553                        let mean = sample_losses.sum() / sample_losses.len() as f64;
554                        // Use Array1 instead of Array0 to make type consistent
555                        Array::from_elem(Ix1(1), mean)
556                    }
557                    "sum" => {
558                        let sum = sample_losses.sum();
559                        // Use Array1 instead of Array0 to make type consistent
560                        Array::from_elem(Ix1(1), sum)
561                    }
562                    _ => {
563                        return Err(OperationError::ShapeMismatch(format!(
564                            "Unknown reduction method: {reduction}"
565                        )))
566                    }
567                };
568
569                return Ok(Box::new(loss) as Box<dyn Any>);
570            }
571            return Err(OperationError::NotImplemented(
572                "cross_entropy not implemented for these array types".to_string(),
573            ));
574        }
575
576        // Delegate to the implementation
577        let mut kwargs = HashMap::new();
578        kwargs.insert(
579            "reduction".to_string(),
580            Box::new(reduction.to_string()) as Box<dyn Any>,
581        );
582
583        let array_ref = implementing_args[0].1;
584
585        let result = array_ref.array_function(
586            &crate::array_protocol::ArrayFunction::new(
587                "scirs2::array_protocol::ml_ops::cross_entropy",
588            ),
589            &[TypeId::of::<Box<dyn Any>>()],
590            &[Box::new(logits.box_clone()), Box::new(labels.box_clone())],
591            &kwargs,
592        )?;
593
594        Ok(result)
595    },
596    "scirs2::array_protocol::ml, ops: cross_entropy"
597);
598
599array_function_dispatch!(
600    fn dropout(
601        input: &dyn ArrayProtocol,
602        rate: f64,
603        training: bool,
604        seed: Option<u64>,
605    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
606        // Get implementing args
607        let boxed_args: Vec<Box<dyn Any>> = vec![Box::new(input.box_clone())];
608        let implementing_args =
609            get_implementing_args("scirs2::array_protocol::ml_ops::dropout", &boxed_args);
610        if implementing_args.is_empty() {
611            // Fallback implementation for ndarray types
612            if let Some(inputarray) = input.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>() {
613                let input = inputarray.as_array();
614
615                if !training {
616                    // During inference, just scale the input
617                    return Ok(Box::new(NdarrayWrapper::new(input.clone())));
618                }
619
620                // Create a binary mask with probabilities (1-rate)
621                let mut rng = match seed {
622                    Some(s) => rand::rngs::StdRng::seed_from_u64(s),
623                    None => {
624                        let mut rng = rand::rng();
625                        // Get a random seed from rng and create a new StdRng
626                        let random_seed: u64 = rng.random();
627                        rand::rngs::StdRng::seed_from_u64(random_seed)
628                    }
629                };
630
631                let mask = Array::from_shape_fn(input.raw_dim(), |_| {
632                    if rng.random::<f64>() >= rate {
633                        1.0
634                    } else {
635                        0.0
636                    }
637                });
638
639                // Scale by 1/(1-rate) to maintain expected value during training
640                let scale = 1.0 / (1.0 - rate);
641                let result = input.clone() * &mask * scale;
642
643                return Ok(Box::new(NdarrayWrapper::new(result)));
644            }
645            return Err(OperationError::NotImplemented(
646                "dropout not implemented for this array type".to_string(),
647            ));
648        }
649
650        // Delegate to the implementation
651        let mut kwargs = HashMap::new();
652        kwargs.insert("rate".to_string(), Box::new(rate) as Box<dyn Any>);
653        kwargs.insert("training".to_string(), Box::new(training) as Box<dyn Any>);
654        if let Some(s) = seed {
655            kwargs.insert("seed".to_string(), Box::new(s) as Box<dyn Any>);
656        }
657
658        let array_ref = implementing_args[0].1;
659
660        let result = array_ref.array_function(
661            &crate::array_protocol::ArrayFunction::new("scirs2::array_protocol::ml_ops::dropout"),
662            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
663            &[Box::new(input.box_clone())],
664            &kwargs,
665        )?;
666
667        // Try to downcast the result
668        match result.downcast::<Box<dyn ArrayProtocol>>() {
669            Ok(array) => Ok(*array),
670            Err(_) => Err(OperationError::Other(
671                "Failed to downcast array_function result".to_string(),
672            )),
673        }
674    },
675    "scirs2::array_protocol::ml, ops: dropout"
676);
677
678array_function_dispatch!(
679    fn self_attention(
680        queries: &dyn ArrayProtocol,
681        keys: &dyn ArrayProtocol,
682        values: &dyn ArrayProtocol,
683        mask: Option<&dyn ArrayProtocol>,
684        scale: Option<f64>,
685    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
686        // Get implementing args
687        let mut boxed_args: Vec<Box<dyn Any>> = vec![
688            Box::new(queries.box_clone()),
689            Box::new(keys.box_clone()),
690            Box::new(values.box_clone()),
691        ];
692        if let Some(m) = mask {
693            boxed_args.push(Box::new(m.box_clone()));
694        }
695
696        let implementing_args = get_implementing_args(
697            "scirs2::array_protocol::ml_ops::self_attention",
698            &boxed_args,
699        );
700        if implementing_args.is_empty() {
701            // Fallback implementation for ndarray types
702            if let (Some(q_array), Some(k_array), Some(v_array)) = (
703                queries.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
704                keys.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
705                values.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>(),
706            ) {
707                let q = q_array.as_array();
708                let k = k_array.as_array();
709                let v = v_array.as_array();
710
711                // Get dimensions
712                // q, k, v should have shape [batch_size, seq_len, num_heads, d_k]
713                let batch_size = q.shape()[0];
714                let q_len = q.shape()[1];
715                let num_heads = q.shape()[2];
716                let d_k = q.shape()[3];
717
718                let k_len = k.shape()[1];
719
720                // Check dimensions
721                if k.shape()[0] != batch_size
722                    || k.shape()[2] != num_heads
723                    || k.shape()[3] != d_k
724                    || v.shape()[0] != batch_size
725                    || v.shape()[1] != k_len
726                    || v.shape()[2] != num_heads
727                {
728                    return Err(OperationError::ShapeMismatch(
729                        "Incompatible shapes for self-attention".to_string(),
730                    ));
731                }
732
733                // Apply scaling
734                let _scale_factor = scale.unwrap_or_else(|| {
735                    // Default scale factor is 1/sqrt(d_k)
736                    let d_k_f64 = d_k as f64;
737                    if d_k_f64 > 0.0 {
738                        d_k_f64.sqrt()
739                    } else {
740                        1.0 // Fallback for edge case
741                    }
742                });
743
744                // Implement self-attention:
745                // 1. scores = matmul(q, k.transpose) / scale_factor
746                // 2. if mask: scores = scores.masked_fill(mask, -inf)
747                // 3. attention = softmax(scores)
748                // 4. output = matmul(attention, v)
749
750                let scale_factor = scale.unwrap_or(1.0 / (d_k as f64).sqrt());
751                let mut output: Array<f64, Ix3> = Array::zeros((batch_size, q_len, d_k));
752
753                for b in 0..batch_size {
754                    // Extract batch slices
755                    let q_batch = q.slice(crate::s![b, .., .., ..]);
756                    let k_batch = k.slice(crate::s![b, .., .., ..]);
757                    let v_batch = v.slice(crate::s![b, .., .., ..]);
758
759                    // Compute attention scores: Q * K^T for each head
760                    let mut head_outputs = Array::zeros((q_len, num_heads, d_k));
761
762                    for h in 0..num_heads {
763                        let mut scores = Array::zeros((q_len, k_len));
764                        for i in 0..q_len {
765                            for j in 0..k_len {
766                                let mut dot_product = 0.0;
767                                for k in 0..d_k {
768                                    dot_product += q_batch[[i, h, k]] * k_batch[[j, h, k]];
769                                }
770                                scores[[i, j]] = dot_product * scale_factor;
771                            }
772                        }
773
774                        // Apply mask if provided
775                        if let Some(mask_array) = mask {
776                            if let Some(mask_wrapper) = mask_array
777                                .as_any()
778                                .downcast_ref::<NdarrayWrapper<f64, Ix3>>()
779                            {
780                                let mask_batch =
781                                    mask_wrapper.as_array().slice(crate::s![b, .., ..]);
782                                for i in 0..q_len {
783                                    for j in 0..k_len {
784                                        if mask_batch[[i, j]] == 0.0 {
785                                            scores[[i, j]] = f64::NEG_INFINITY;
786                                        }
787                                    }
788                                }
789                            }
790                        }
791
792                        // Apply softmax to get attention weights
793                        let mut attention = Array::zeros((q_len, k_len));
794                        for i in 0..q_len {
795                            // Find max for numerical stability
796                            let mut max_score = f64::NEG_INFINITY;
797                            for j in 0..k_len {
798                                if scores[[i, j]] > max_score {
799                                    max_score = scores[[i, j]];
800                                }
801                            }
802
803                            // Compute exp and sum
804                            let mut exp_sum = 0.0;
805                            for j in 0..k_len {
806                                let exp_val = (scores[[i, j]] - max_score).exp();
807                                attention[[i, j]] = exp_val;
808                                exp_sum += exp_val;
809                            }
810
811                            // Normalize
812                            for j in 0..k_len {
813                                attention[[i, j]] /= exp_sum;
814                            }
815                        }
816
817                        // Compute output: attention * V
818                        for i in 0..q_len {
819                            for k in 0..d_k {
820                                let mut weighted_sum = 0.0;
821                                for j in 0..k_len {
822                                    weighted_sum += attention[[i, j]] * v_batch[[j, h, k]];
823                                }
824                                head_outputs[[i, h, k]] = weighted_sum;
825                            }
826                        }
827                    }
828
829                    // Aggregate outputs from all heads (simple average)
830                    for i in 0..q_len {
831                        for k in 0..d_k {
832                            let mut sum = 0.0;
833                            for h in 0..num_heads {
834                                sum += head_outputs[[i, h, k]];
835                            }
836                            output[[b, i, k]] = sum / num_heads as f64;
837                        }
838                    }
839                }
840
841                return Ok(Box::new(NdarrayWrapper::new(output)));
842            }
843            return Err(OperationError::NotImplemented(
844                "self_attention not implemented for these array types".to_string(),
845            ));
846        }
847
848        // Delegate to the implementation
849        let mut kwargs = HashMap::new();
850        if let Some(s) = scale {
851            kwargs.insert("scale".to_string(), Box::new(s) as Box<dyn Any>);
852        }
853        if let Some(m) = mask {
854            kwargs.insert("mask".to_string(), Box::new(m.box_clone()) as Box<dyn Any>);
855        }
856
857        let array_ref = implementing_args[0].1;
858
859        let result = array_ref.array_function(
860            &crate::array_protocol::ArrayFunction::new(
861                "scirs2::array_protocol::ml_ops::self_attention",
862            ),
863            &[TypeId::of::<Box<dyn ArrayProtocol>>()],
864            &[
865                Box::new(queries.box_clone()),
866                Box::new(keys.box_clone()),
867                Box::new(values.box_clone()),
868            ],
869            &kwargs,
870        )?;
871
872        // Try to downcast the result
873        match result.downcast::<Box<dyn ArrayProtocol>>() {
874            Ok(array) => Ok(*array),
875            Err(_) => Err(OperationError::Other(
876                "Failed to downcast array_function result".to_string(),
877            )),
878        }
879    },
880    "scirs2::array_protocol::ml, ops: self_attention"
881);