Skip to main content

torsh_sparse/nn/
convolution.rs

1//! Sparse convolution layers
2//!
3//! This module provides sparse convolution implementations optimized for both sparse inputs
4//! and sparse kernels. These layers can significantly reduce computational complexity when
5//! dealing with sparse data, which is common in computer vision tasks after activation
6//! functions like ReLU, or in specialized applications like medical imaging and satellite data.
7
8use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
9
10use torsh_core::{Shape, TorshError};
11use torsh_tensor::{creation::zeros, Tensor};
12
13/// Sparse 2D Convolution layer
14///
15/// Implements 2D convolution operations optimized for sparse inputs and/or sparse kernels.
16/// This can significantly reduce computation when either the input or kernel has many zeros.
17///
18/// # Mathematical Formulation
19/// For standard convolution: `y[i,j] = Σ_m Σ_n w[m,n] * x[i+m, j+n]`
20/// For sparse convolution: Only compute terms where `w[m,n] ≠ 0`
21///
22/// # Use Cases
23/// - Computer vision with sparse activations (post-ReLU)
24/// - Medical imaging with sparse features
25/// - Pruned neural networks
26/// - Specialized domains with naturally sparse data
27#[derive(Debug, Clone)]
28pub struct SparseConv2d {
29    /// Convolution kernel in sparse format (out_channels, in_channels, kernel_height, kernel_width)
30    kernel: CsrTensor,
31    /// Optional bias vector (out_channels,)
32    bias: Option<Tensor>,
33    /// Number of input channels
34    in_channels: usize,
35    /// Number of output channels
36    out_channels: usize,
37    /// Kernel size (height, width)
38    kernel_size: (usize, usize),
39    /// Stride (height, width)
40    stride: (usize, usize),
41    /// Padding (height, width)
42    padding: (usize, usize),
43    /// Dilation (height, width)
44    dilation: (usize, usize),
45}
46
47impl SparseConv2d {
48    /// Create a new sparse 2D convolution layer
49    ///
50    /// # Arguments
51    /// * `in_channels` - Number of input channels
52    /// * `out_channels` - Number of output channels
53    /// * `kernel_size` - Convolution kernel size (height, width)
54    /// * `stride` - Stride for convolution (default: (1, 1))
55    /// * `padding` - Padding for convolution (default: (0, 0))
56    /// * `dilation` - Dilation for convolution (default: (1, 1))
57    /// * `sparsity` - Kernel sparsity level (0.0 = dense, 1.0 = fully sparse)
58    /// * `use_bias` - Whether to include learnable bias
59    ///
60    /// # Returns
61    /// * `TorshResult<Self>` - New sparse 2D convolution layer or error
62    ///
63    /// # Example
64    /// ```rust
65    /// use torsh_sparse::nn::convolution::SparseConv2d;
66    ///
67    /// // Create sparse conv: 3->64 channels, 3x3 kernel, 90% sparse
68    /// let conv = SparseConv2d::new(
69    ///     3, 64, (3, 3),
70    ///     Some((1, 1)), Some((1, 1)), None,
71    ///     0.9, true
72    /// ).expect("valid conv2d config");
73    /// ```
74    #[allow(clippy::too_many_arguments)]
75    pub fn new(
76        in_channels: usize,
77        out_channels: usize,
78        kernel_size: (usize, usize),
79        stride: Option<(usize, usize)>,
80        padding: Option<(usize, usize)>,
81        dilation: Option<(usize, usize)>,
82        sparsity: f32,
83        use_bias: bool,
84    ) -> TorshResult<Self> {
85        if !(0.0..=1.0).contains(&sparsity) {
86            return Err(TorshError::InvalidArgument(
87                "Sparsity must be between 0.0 and 1.0".to_string(),
88            ));
89        }
90
91        if kernel_size.0 == 0 || kernel_size.1 == 0 {
92            return Err(TorshError::InvalidArgument(
93                "Kernel size must be greater than 0".to_string(),
94            ));
95        }
96
97        let stride = stride.unwrap_or((1, 1));
98        let padding = padding.unwrap_or((0, 0));
99        let dilation = dilation.unwrap_or((1, 1));
100
101        if stride.0 == 0 || stride.1 == 0 {
102            return Err(TorshError::InvalidArgument(
103                "Stride must be greater than 0".to_string(),
104            ));
105        }
106
107        if dilation.0 == 0 || dilation.1 == 0 {
108            return Err(TorshError::InvalidArgument(
109                "Dilation must be greater than 0".to_string(),
110            ));
111        }
112
113        // Generate sparse kernel
114        let kernel =
115            Self::generate_sparse_kernel(out_channels, in_channels, kernel_size, sparsity)?;
116
117        // Generate bias if requested
118        let bias = if use_bias {
119            Some(zeros::<f32>(&[out_channels])?)
120        } else {
121            None
122        };
123
124        Ok(Self {
125            kernel,
126            bias,
127            in_channels,
128            out_channels,
129            kernel_size,
130            stride,
131            padding,
132            dilation,
133        })
134    }
135
136    /// Forward pass for sparse convolution
137    ///
138    /// # Arguments
139    /// * `input` - Input tensor (batch_size, in_channels, height, width)
140    ///
141    /// # Returns
142    /// * `TorshResult<Tensor>` - Output tensor (batch_size, out_channels, out_height, out_width)
143    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
144        // Validate input shape: (batch_size, in_channels, height, width)
145        let input_shape = input.shape();
146        if input_shape.ndim() != 4 {
147            return Err(TorshError::InvalidArgument(
148                "Input must be 4D tensor (batch_size, in_channels, height, width)".to_string(),
149            ));
150        }
151
152        let batch_size = input_shape.dims()[0];
153        let input_channels = input_shape.dims()[1];
154        let input_height = input_shape.dims()[2];
155        let input_width = input_shape.dims()[3];
156
157        if input_channels != self.in_channels {
158            return Err(TorshError::InvalidArgument(format!(
159                "Input channels {} don't match layer input channels {}",
160                input_channels, self.in_channels
161            )));
162        }
163
164        // Calculate output dimensions
165        let output_height =
166            (input_height + 2 * self.padding.0 - self.dilation.0 * (self.kernel_size.0 - 1) - 1)
167                / self.stride.0
168                + 1;
169        let output_width =
170            (input_width + 2 * self.padding.1 - self.dilation.1 * (self.kernel_size.1 - 1) - 1)
171                / self.stride.1
172                + 1;
173
174        let mut output =
175            zeros::<f32>(&[batch_size, self.out_channels, output_height, output_width])?;
176
177        // Perform sparse convolution for each batch item
178        for b in 0..batch_size {
179            self.conv2d_single(
180                input,
181                &mut output,
182                b,
183                input_height,
184                input_width,
185                output_height,
186                output_width,
187            )?;
188        }
189
190        // Add bias if present
191        if let Some(ref bias) = self.bias {
192            for b in 0..batch_size {
193                for c in 0..self.out_channels {
194                    let bias_val = bias.get(&[c])?;
195                    for h in 0..output_height {
196                        for w in 0..output_width {
197                            let current = output.get(&[b, c, h, w])?;
198                            output.set(&[b, c, h, w], current + bias_val)?;
199                        }
200                    }
201                }
202            }
203        }
204
205        Ok(output)
206    }
207
208    /// Perform convolution for a single batch item
209    #[allow(clippy::too_many_arguments)]
210    fn conv2d_single(
211        &self,
212        input: &Tensor,
213        output: &mut Tensor,
214        batch_idx: usize,
215        input_height: usize,
216        input_width: usize,
217        output_height: usize,
218        output_width: usize,
219    ) -> TorshResult<()> {
220        // Get sparse kernel triplets (we need to interpret the flat CSR as 4D)
221        let kernel_coo = self.kernel.to_coo()?;
222        let kernel_triplets = kernel_coo.triplets();
223
224        // Perform sparse convolution by iterating over non-zero kernel elements
225        for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
226            // Convert flat index back to 4D coordinates
227            let (out_c, in_c, kh, kw) = self.flat_to_4d_kernel(kernel_flat_idx);
228
229            // For each output position
230            for out_h in 0..output_height {
231                for out_w in 0..output_width {
232                    // Calculate input position
233                    let in_h = out_h * self.stride.0 + kh * self.dilation.0;
234                    let in_w = out_w * self.stride.1 + kw * self.dilation.1;
235
236                    // Apply padding offset
237                    if in_h >= self.padding.0 && in_w >= self.padding.1 {
238                        let padded_in_h = in_h - self.padding.0;
239                        let padded_in_w = in_w - self.padding.1;
240
241                        // Check bounds
242                        if padded_in_h < input_height && padded_in_w < input_width {
243                            let input_val =
244                                input.get(&[batch_idx, in_c, padded_in_h, padded_in_w])?;
245                            let current_out = output.get(&[batch_idx, out_c, out_h, out_w])?;
246                            output.set(
247                                &[batch_idx, out_c, out_h, out_w],
248                                current_out + kernel_val * input_val,
249                            )?;
250                        }
251                    }
252                }
253            }
254        }
255
256        Ok(())
257    }
258
259    /// Convert flat kernel index to 4D coordinates (out_c, in_c, kh, kw)
260    fn flat_to_4d_kernel(&self, flat_idx: usize) -> (usize, usize, usize, usize) {
261        let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
262        let channel_size = self.in_channels * kernel_size_total;
263
264        let out_c = flat_idx / channel_size;
265        let remaining = flat_idx % channel_size;
266        let in_c = remaining / kernel_size_total;
267        let remaining = remaining % kernel_size_total;
268        let kh = remaining / self.kernel_size.1;
269        let kw = remaining % self.kernel_size.1;
270
271        (out_c, in_c, kh, kw)
272    }
273
274    /// Convert 4D kernel coordinates to flat index
275    #[allow(dead_code)]
276    fn kernel_4d_to_flat(&self, out_c: usize, in_c: usize, kh: usize, kw: usize) -> usize {
277        let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
278        let channel_size = self.in_channels * kernel_size_total;
279
280        out_c * channel_size + in_c * kernel_size_total + kh * self.kernel_size.1 + kw
281    }
282
283    /// Generate sparse convolution kernel
284    fn generate_sparse_kernel(
285        out_channels: usize,
286        in_channels: usize,
287        kernel_size: (usize, usize),
288        sparsity: f32,
289    ) -> TorshResult<CsrTensor> {
290        let total_elements = out_channels * in_channels * kernel_size.0 * kernel_size.1;
291        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
292
293        let mut row_indices = Vec::with_capacity(nnz);
294        let mut col_indices = Vec::with_capacity(nnz);
295        let mut values = Vec::with_capacity(nnz);
296
297        // Generate random sparse pattern
298        let mut positions = std::collections::HashSet::new();
299        while positions.len() < nnz {
300            let mut rng = scirs2_core::random::thread_rng();
301            let flat_idx = rng.gen_range(0..total_elements);
302            positions.insert(flat_idx);
303        }
304
305        // For CSR representation, we'll use a 2D matrix where:
306        // - rows represent output channels
307        // - columns represent (in_channels * kernel_height * kernel_width)
308        let kernel_size_total = kernel_size.0 * kernel_size.1;
309        let channel_size = in_channels * kernel_size_total;
310
311        for flat_idx in positions {
312            let out_c = flat_idx / channel_size;
313            let col_idx = flat_idx % channel_size; // This represents (in_c, kh, kw) flattened
314
315            row_indices.push(out_c);
316            col_indices.push(col_idx);
317
318            // He initialization for convolution layers
319            let fan_in = in_channels * kernel_size.0 * kernel_size.1;
320            let std_dev = (2.0 / fan_in as f32).sqrt();
321            let mut rng = scirs2_core::random::thread_rng();
322            values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
323        }
324
325        let shape = Shape::new(vec![out_channels, channel_size]);
326        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
327        CsrTensor::from_coo(&coo)
328    }
329
330    /// Get the number of parameters
331    pub fn num_parameters(&self) -> usize {
332        let kernel_params = self.kernel.nnz();
333        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
334        kernel_params + bias_params
335    }
336
337    /// Get kernel sparsity
338    pub fn kernel_sparsity(&self) -> f32 {
339        let total_elements =
340            self.out_channels * self.in_channels * self.kernel_size.0 * self.kernel_size.1;
341        let nnz = self.kernel.nnz();
342        1.0 - (nnz as f32 / total_elements as f32)
343    }
344
345    /// Get input channels
346    pub fn in_channels(&self) -> usize {
347        self.in_channels
348    }
349
350    /// Get output channels
351    pub fn out_channels(&self) -> usize {
352        self.out_channels
353    }
354
355    /// Get kernel size
356    pub fn kernel_size(&self) -> (usize, usize) {
357        self.kernel_size
358    }
359
360    /// Get stride
361    pub fn stride(&self) -> (usize, usize) {
362        self.stride
363    }
364
365    /// Get padding
366    pub fn padding(&self) -> (usize, usize) {
367        self.padding
368    }
369
370    /// Get dilation
371    pub fn dilation(&self) -> (usize, usize) {
372        self.dilation
373    }
374}
375
376/// Sparse 1D Convolution layer
377///
378/// Implements 1D convolution operations optimized for sparse inputs and/or sparse kernels.
379/// This is particularly useful for time series data, audio processing, and NLP applications
380/// where sparsity can naturally occur or be induced through pruning.
381#[derive(Debug, Clone)]
382pub struct SparseConv1d {
383    /// Convolution kernel in sparse format (out_channels, in_channels, kernel_length)
384    kernel: CsrTensor,
385    /// Optional bias vector (out_channels,)
386    bias: Option<Tensor>,
387    /// Number of input channels
388    in_channels: usize,
389    /// Number of output channels
390    out_channels: usize,
391    /// Kernel size
392    kernel_size: usize,
393    /// Stride
394    stride: usize,
395    /// Padding
396    padding: usize,
397    /// Dilation
398    dilation: usize,
399}
400
401impl SparseConv1d {
402    /// Create a new sparse 1D convolution layer
403    ///
404    /// # Arguments
405    /// * `in_channels` - Number of input channels
406    /// * `out_channels` - Number of output channels
407    /// * `kernel_size` - Convolution kernel size
408    /// * `stride` - Stride for convolution (default: 1)
409    /// * `padding` - Padding for convolution (default: 0)
410    /// * `dilation` - Dilation for convolution (default: 1)
411    /// * `sparsity` - Kernel sparsity level (0.0 = dense, 1.0 = fully sparse)
412    /// * `use_bias` - Whether to include learnable bias
413    ///
414    /// # Returns
415    /// * `TorshResult<Self>` - New sparse 1D convolution layer or error
416    ///
417    /// # Example
418    /// ```rust
419    /// use torsh_sparse::nn::convolution::SparseConv1d;
420    ///
421    /// // Create sparse 1D conv: 16->32 channels, kernel size 3, 80% sparse
422    /// let conv = SparseConv1d::new(16, 32, 3, None, None, None, 0.8, true).expect("valid conv1d config");
423    /// ```
424    #[allow(clippy::too_many_arguments)]
425    pub fn new(
426        in_channels: usize,
427        out_channels: usize,
428        kernel_size: usize,
429        stride: Option<usize>,
430        padding: Option<usize>,
431        dilation: Option<usize>,
432        sparsity: f32,
433        use_bias: bool,
434    ) -> TorshResult<Self> {
435        if !(0.0..=1.0).contains(&sparsity) {
436            return Err(TorshError::InvalidArgument(
437                "Sparsity must be between 0.0 and 1.0".to_string(),
438            ));
439        }
440
441        if kernel_size == 0 {
442            return Err(TorshError::InvalidArgument(
443                "Kernel size must be greater than 0".to_string(),
444            ));
445        }
446
447        let stride = stride.unwrap_or(1);
448        let padding = padding.unwrap_or(0);
449        let dilation = dilation.unwrap_or(1);
450
451        if stride == 0 {
452            return Err(TorshError::InvalidArgument(
453                "Stride must be greater than 0".to_string(),
454            ));
455        }
456
457        if dilation == 0 {
458            return Err(TorshError::InvalidArgument(
459                "Dilation must be greater than 0".to_string(),
460            ));
461        }
462
463        // Generate sparse kernel
464        let kernel =
465            Self::generate_sparse_kernel_1d(out_channels, in_channels, kernel_size, sparsity)?;
466
467        // Generate bias if requested
468        let bias = if use_bias {
469            Some(zeros::<f32>(&[out_channels])?)
470        } else {
471            None
472        };
473
474        Ok(Self {
475            kernel,
476            bias,
477            in_channels,
478            out_channels,
479            kernel_size,
480            stride,
481            padding,
482            dilation,
483        })
484    }
485
486    /// Forward pass for sparse 1D convolution
487    ///
488    /// # Arguments
489    /// * `input` - Input tensor (batch_size, in_channels, length)
490    ///
491    /// # Returns
492    /// * `TorshResult<Tensor>` - Output tensor (batch_size, out_channels, out_length)
493    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
494        // Validate input shape: (batch_size, in_channels, length)
495        let input_shape = input.shape();
496        if input_shape.ndim() != 3 {
497            return Err(TorshError::InvalidArgument(
498                "Input must be 3D tensor (batch_size, in_channels, length)".to_string(),
499            ));
500        }
501
502        let batch_size = input_shape.dims()[0];
503        let input_channels = input_shape.dims()[1];
504        let input_length = input_shape.dims()[2];
505
506        if input_channels != self.in_channels {
507            return Err(TorshError::InvalidArgument(format!(
508                "Input channels {} don't match layer input channels {}",
509                input_channels, self.in_channels
510            )));
511        }
512
513        // Calculate output length
514        let output_length =
515            (input_length + 2 * self.padding - self.dilation * (self.kernel_size - 1) - 1)
516                / self.stride
517                + 1;
518
519        let mut output = zeros::<f32>(&[batch_size, self.out_channels, output_length])?;
520
521        // Perform sparse convolution for each batch item
522        for b in 0..batch_size {
523            self.conv1d_single(input, &mut output, b, input_length, output_length)?;
524        }
525
526        // Add bias if present
527        if let Some(ref bias) = self.bias {
528            for b in 0..batch_size {
529                for c in 0..self.out_channels {
530                    let bias_val = bias.get(&[c])?;
531                    for l in 0..output_length {
532                        let current = output.get(&[b, c, l])?;
533                        output.set(&[b, c, l], current + bias_val)?;
534                    }
535                }
536            }
537        }
538
539        Ok(output)
540    }
541
542    /// Perform 1D convolution for a single batch item
543    fn conv1d_single(
544        &self,
545        input: &Tensor,
546        output: &mut Tensor,
547        batch_idx: usize,
548        input_length: usize,
549        output_length: usize,
550    ) -> TorshResult<()> {
551        // Get sparse kernel triplets
552        let kernel_coo = self.kernel.to_coo()?;
553        let kernel_triplets = kernel_coo.triplets();
554
555        // Perform sparse convolution by iterating over non-zero kernel elements
556        for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
557            // Convert flat index back to 3D coordinates
558            let (out_c, in_c, k_pos) = self.flat_to_3d_kernel(kernel_flat_idx);
559
560            // For each output position
561            for out_pos in 0..output_length {
562                // Calculate input position
563                let in_pos = out_pos * self.stride + k_pos * self.dilation;
564
565                // Apply padding offset
566                if in_pos >= self.padding {
567                    let padded_in_pos = in_pos - self.padding;
568
569                    // Check bounds
570                    if padded_in_pos < input_length {
571                        let input_val = input.get(&[batch_idx, in_c, padded_in_pos])?;
572                        let current_out = output.get(&[batch_idx, out_c, out_pos])?;
573                        output.set(
574                            &[batch_idx, out_c, out_pos],
575                            current_out + kernel_val * input_val,
576                        )?;
577                    }
578                }
579            }
580        }
581
582        Ok(())
583    }
584
585    /// Convert flat kernel index to 3D coordinates (out_c, in_c, k_pos)
586    fn flat_to_3d_kernel(&self, flat_idx: usize) -> (usize, usize, usize) {
587        let channel_size = self.in_channels * self.kernel_size;
588
589        let out_c = flat_idx / channel_size;
590        let remaining = flat_idx % channel_size;
591        let in_c = remaining / self.kernel_size;
592        let k_pos = remaining % self.kernel_size;
593
594        (out_c, in_c, k_pos)
595    }
596
597    /// Generate sparse 1D convolution kernel
598    fn generate_sparse_kernel_1d(
599        out_channels: usize,
600        in_channels: usize,
601        kernel_size: usize,
602        sparsity: f32,
603    ) -> TorshResult<CsrTensor> {
604        let total_elements = out_channels * in_channels * kernel_size;
605        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
606
607        let mut row_indices = Vec::with_capacity(nnz);
608        let mut col_indices = Vec::with_capacity(nnz);
609        let mut values = Vec::with_capacity(nnz);
610
611        // Generate random sparse pattern
612        let mut positions = std::collections::HashSet::new();
613        while positions.len() < nnz {
614            let mut rng = scirs2_core::random::thread_rng();
615            let flat_idx = rng.gen_range(0..total_elements);
616            positions.insert(flat_idx);
617        }
618
619        // For CSR representation, we'll use a 2D matrix where:
620        // - rows represent output channels
621        // - columns represent (in_channels * kernel_size)
622        let channel_size = in_channels * kernel_size;
623
624        for flat_idx in positions {
625            let out_c = flat_idx / channel_size;
626            let col_idx = flat_idx % channel_size; // This represents (in_c, k_pos) flattened
627
628            row_indices.push(out_c);
629            col_indices.push(col_idx);
630
631            // He initialization for convolution layers
632            let fan_in = in_channels * kernel_size;
633            let std_dev = (2.0 / fan_in as f32).sqrt();
634            let mut rng = scirs2_core::random::thread_rng();
635            values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
636        }
637
638        let shape = Shape::new(vec![out_channels, channel_size]);
639        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
640        CsrTensor::from_coo(&coo)
641    }
642
643    /// Get the number of parameters
644    pub fn num_parameters(&self) -> usize {
645        let kernel_params = self.kernel.nnz();
646        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
647        kernel_params + bias_params
648    }
649
650    /// Get kernel sparsity
651    pub fn kernel_sparsity(&self) -> f32 {
652        let total_elements = self.out_channels * self.in_channels * self.kernel_size;
653        let nnz = self.kernel.nnz();
654        1.0 - (nnz as f32 / total_elements as f32)
655    }
656
657    /// Get input channels
658    pub fn in_channels(&self) -> usize {
659        self.in_channels
660    }
661
662    /// Get output channels
663    pub fn out_channels(&self) -> usize {
664        self.out_channels
665    }
666
667    /// Get kernel size
668    pub fn kernel_size(&self) -> usize {
669        self.kernel_size
670    }
671
672    /// Get stride
673    pub fn stride(&self) -> usize {
674        self.stride
675    }
676
677    /// Get padding
678    pub fn padding(&self) -> usize {
679        self.padding
680    }
681
682    /// Get dilation
683    pub fn dilation(&self) -> usize {
684        self.dilation
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use torsh_tensor::creation::ones;
692
693    #[test]
694    fn test_sparse_conv2d_creation() {
695        let conv = SparseConv2d::new(3, 16, (3, 3), Some((1, 1)), Some((1, 1)), None, 0.5, true)
696            .expect("operation should succeed");
697        assert_eq!(conv.in_channels(), 3);
698        assert_eq!(conv.out_channels(), 16);
699        assert_eq!(conv.kernel_size(), (3, 3));
700        assert!(conv.num_parameters() > 0);
701    }
702
703    #[test]
704    fn test_sparse_conv2d_forward() {
705        let conv = SparseConv2d::new(2, 4, (3, 3), None, Some((1, 1)), None, 0.3, false)
706            .expect("operation should succeed");
707        let input = ones::<f32>(&[1, 2, 5, 5]).expect("operation should succeed");
708        let output = conv.forward(&input).expect("forward pass should succeed");
709        assert_eq!(output.shape().dims(), &[1, 4, 5, 5]); // Same size due to padding
710    }
711
712    #[test]
713    fn test_sparse_conv1d_creation() {
714        let conv = SparseConv1d::new(8, 16, 5, None, None, None, 0.7, true)
715            .expect("Sparse Conv1d should succeed");
716        assert_eq!(conv.in_channels(), 8);
717        assert_eq!(conv.out_channels(), 16);
718        assert_eq!(conv.kernel_size(), 5);
719        assert!(conv.num_parameters() > 0);
720    }
721
722    #[test]
723    fn test_sparse_conv1d_forward() {
724        let conv = SparseConv1d::new(4, 8, 3, None, Some(1), None, 0.4, false)
725            .expect("operation should succeed");
726        let input = ones::<f32>(&[2, 4, 10]).expect("operation should succeed");
727        let output = conv.forward(&input).expect("forward pass should succeed");
728        assert_eq!(output.shape().dims(), &[2, 8, 10]); // Same size due to padding
729    }
730
731    #[test]
732    fn test_output_size_calculation() {
733        // Test 2D convolution output size
734        let conv = SparseConv2d::new(1, 1, (3, 3), Some((2, 2)), None, None, 0.0, false)
735            .expect("operation should succeed");
736        let input = ones::<f32>(&[1, 1, 8, 8]).expect("operation should succeed");
737        let output = conv.forward(&input).expect("forward pass should succeed");
738        assert_eq!(output.shape().dims(), &[1, 1, 3, 3]); // (8 - 3 + 1) / 2 = 3
739
740        // Test 1D convolution output size
741        let conv1d = SparseConv1d::new(1, 1, 3, Some(2), None, None, 0.0, false)
742            .expect("operation should succeed");
743        let input1d = ones::<f32>(&[1, 1, 10]).expect("operation should succeed");
744        let output1d = conv1d
745            .forward(&input1d)
746            .expect("forward pass should succeed");
747        assert_eq!(output1d.shape().dims(), &[1, 1, 4]); // (10 - 3 + 1) / 2 = 4
748    }
749
750    #[test]
751    fn test_invalid_parameters() {
752        assert!(SparseConv2d::new(1, 1, (0, 3), None, None, None, 0.5, false).is_err());
753        assert!(SparseConv2d::new(1, 1, (3, 3), Some((0, 1)), None, None, 0.5, false).is_err());
754        assert!(SparseConv2d::new(1, 1, (3, 3), None, None, None, 1.5, false).is_err());
755
756        assert!(SparseConv1d::new(1, 1, 0, None, None, None, 0.5, false).is_err());
757        assert!(SparseConv1d::new(1, 1, 3, Some(0), None, None, 0.5, false).is_err());
758        assert!(SparseConv1d::new(1, 1, 3, None, None, None, -0.1, false).is_err());
759    }
760
761    #[test]
762    fn test_dimension_validation() {
763        let conv = SparseConv2d::new(3, 16, (3, 3), None, None, None, 0.5, false)
764            .expect("operation should succeed");
765        let wrong_input = ones::<f32>(&[1, 2, 5, 5]).expect("operation should succeed"); // Wrong channels
766        assert!(conv.forward(&wrong_input).is_err());
767
768        let conv1d = SparseConv1d::new(4, 8, 3, None, None, None, 0.5, false)
769            .expect("Sparse Conv1d should succeed");
770        let wrong_input1d = ones::<f32>(&[1, 3, 10]).expect("operation should succeed"); // Wrong channels
771        assert!(conv1d.forward(&wrong_input1d).is_err());
772    }
773
774    #[test]
775    fn test_sparsity_measurement() {
776        let conv = SparseConv2d::new(2, 4, (3, 3), None, None, None, 0.8, false)
777            .expect("operation should succeed");
778        let sparsity = conv.kernel_sparsity();
779        assert!(sparsity >= 0.7 && sparsity <= 0.9); // Should be around 0.8
780
781        let conv1d = SparseConv1d::new(2, 4, 5, None, None, None, 0.6, false)
782            .expect("Sparse Conv1d should succeed");
783        let sparsity1d = conv1d.kernel_sparsity();
784        assert!(sparsity1d >= 0.5 && sparsity1d <= 0.7); // Should be around 0.6
785    }
786
787    #[test]
788    fn test_bias_addition() {
789        let conv = SparseConv2d::new(1, 2, (1, 1), None, None, None, 0.0, true)
790            .expect("operation should succeed");
791        let input = ones::<f32>(&[1, 1, 3, 3]).expect("operation should succeed");
792        let output = conv.forward(&input).expect("forward pass should succeed");
793        assert_eq!(output.shape().dims(), &[1, 2, 3, 3]);
794
795        let conv1d = SparseConv1d::new(1, 2, 1, None, None, None, 0.0, true)
796            .expect("Sparse Conv1d should succeed");
797        let input1d = ones::<f32>(&[1, 1, 5]).expect("operation should succeed");
798        let output1d = conv1d
799            .forward(&input1d)
800            .expect("forward pass should succeed");
801        assert_eq!(output1d.shape().dims(), &[1, 2, 5]);
802    }
803}