Skip to main content

torsh_sparse/layers/
pooling.rs

1//! Sparse pooling layers
2//!
3//! This module provides pooling operations optimized for sparse tensors,
4//! including max pooling, average pooling, and adaptive pooling variants.
5
6use crate::TorshResult;
7use torsh_core::TorshError;
8use torsh_tensor::{creation::zeros, Tensor};
9
10/// Sparse 2D Max Pooling layer
11///
12/// Performs max pooling on sparse tensor input, preserving sparsity patterns where possible.
13/// Only processes non-zero regions to maintain computational efficiency.
14pub struct SparseMaxPool2d {
15    /// Kernel size (height, width)
16    kernel_size: (usize, usize),
17    /// Stride (height, width)
18    stride: (usize, usize),
19    /// Padding (height, width)
20    padding: (usize, usize),
21    /// Dilation (height, width)
22    dilation: (usize, usize),
23}
24
25impl SparseMaxPool2d {
26    /// Create a new sparse 2D max pooling layer
27    pub fn new(
28        kernel_size: (usize, usize),
29        stride: Option<(usize, usize)>,
30        padding: Option<(usize, usize)>,
31        dilation: Option<(usize, usize)>,
32    ) -> Self {
33        let stride = stride.unwrap_or(kernel_size);
34        let padding = padding.unwrap_or((0, 0));
35        let dilation = dilation.unwrap_or((1, 1));
36
37        Self {
38            kernel_size,
39            stride,
40            padding,
41            dilation,
42        }
43    }
44
45    /// Forward pass for sparse max pooling
46    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
47        // Validate input shape: (batch_size, channels, height, width)
48        let input_shape = input.shape();
49        if input_shape.ndim() != 4 {
50            return Err(TorshError::InvalidArgument(
51                "Input must be 4D tensor (batch_size, channels, height, width)".to_string(),
52            ));
53        }
54
55        let batch_size = input_shape.dims()[0];
56        let channels = input_shape.dims()[1];
57        let input_height = input_shape.dims()[2];
58        let input_width = input_shape.dims()[3];
59
60        // Calculate output dimensions
61        let output_height =
62            (input_height + 2 * self.padding.0 - self.dilation.0 * (self.kernel_size.0 - 1) - 1)
63                / self.stride.0
64                + 1;
65        let output_width =
66            (input_width + 2 * self.padding.1 - self.dilation.1 * (self.kernel_size.1 - 1) - 1)
67                / self.stride.1
68                + 1;
69
70        let mut output = zeros::<f32>(&[batch_size, channels, output_height, output_width])?;
71
72        // Perform max pooling for each batch and channel
73        for b in 0..batch_size {
74            for c in 0..channels {
75                self.max_pool_channel(
76                    input,
77                    &mut output,
78                    b,
79                    c,
80                    input_height,
81                    input_width,
82                    output_height,
83                    output_width,
84                )?;
85            }
86        }
87
88        Ok(output)
89    }
90
91    /// Perform max pooling for a single channel
92    #[allow(clippy::too_many_arguments)]
93    fn max_pool_channel(
94        &self,
95        input: &Tensor,
96        output: &mut Tensor,
97        batch_idx: usize,
98        channel_idx: usize,
99        input_height: usize,
100        input_width: usize,
101        output_height: usize,
102        output_width: usize,
103    ) -> TorshResult<()> {
104        for out_h in 0..output_height {
105            for out_w in 0..output_width {
106                let mut max_val = f32::NEG_INFINITY;
107                let mut found_value = false;
108
109                // Iterate over the pooling window
110                for kh in 0..self.kernel_size.0 {
111                    for kw in 0..self.kernel_size.1 {
112                        let in_h = out_h * self.stride.0 + kh * self.dilation.0;
113                        let in_w = out_w * self.stride.1 + kw * self.dilation.1;
114
115                        // Apply padding offset
116                        if in_h >= self.padding.0 && in_w >= self.padding.1 {
117                            let padded_in_h = in_h - self.padding.0;
118                            let padded_in_w = in_w - self.padding.1;
119
120                            // Check bounds
121                            if padded_in_h < input_height && padded_in_w < input_width {
122                                let val = input.get(&[
123                                    batch_idx,
124                                    channel_idx,
125                                    padded_in_h,
126                                    padded_in_w,
127                                ])?;
128                                if val > max_val || !found_value {
129                                    max_val = val;
130                                    found_value = true;
131                                }
132                            }
133                        }
134                    }
135                }
136
137                // Set output value (0.0 if no values found)
138                output.set(
139                    &[batch_idx, channel_idx, out_h, out_w],
140                    if found_value { max_val } else { 0.0 },
141                )?;
142            }
143        }
144
145        Ok(())
146    }
147}
148
149/// Sparse 2D Average Pooling layer
150///
151/// Performs average pooling on sparse tensor input, computing averages only over non-zero regions
152/// to maintain meaningful sparse representations.
153pub struct SparseAvgPool2d {
154    /// Kernel size (height, width)
155    kernel_size: (usize, usize),
156    /// Stride (height, width)
157    stride: (usize, usize),
158    /// Padding (height, width)
159    padding: (usize, usize),
160    /// Whether to count padding zeros in the average
161    count_include_pad: bool,
162}
163
164impl SparseAvgPool2d {
165    /// Create a new sparse 2D average pooling layer
166    pub fn new(
167        kernel_size: (usize, usize),
168        stride: Option<(usize, usize)>,
169        padding: Option<(usize, usize)>,
170        count_include_pad: bool,
171    ) -> Self {
172        let stride = stride.unwrap_or(kernel_size);
173        let padding = padding.unwrap_or((0, 0));
174
175        Self {
176            kernel_size,
177            stride,
178            padding,
179            count_include_pad,
180        }
181    }
182
183    /// Forward pass for sparse average pooling
184    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
185        // Validate input shape: (batch_size, channels, height, width)
186        let input_shape = input.shape();
187        if input_shape.ndim() != 4 {
188            return Err(TorshError::InvalidArgument(
189                "Input must be 4D tensor (batch_size, channels, height, width)".to_string(),
190            ));
191        }
192
193        let batch_size = input_shape.dims()[0];
194        let channels = input_shape.dims()[1];
195        let input_height = input_shape.dims()[2];
196        let input_width = input_shape.dims()[3];
197
198        // Calculate output dimensions
199        let output_height =
200            (input_height + 2 * self.padding.0 - self.kernel_size.0) / self.stride.0 + 1;
201        let output_width =
202            (input_width + 2 * self.padding.1 - self.kernel_size.1) / self.stride.1 + 1;
203
204        let mut output = zeros::<f32>(&[batch_size, channels, output_height, output_width])?;
205
206        // Perform average pooling for each batch and channel
207        for b in 0..batch_size {
208            for c in 0..channels {
209                self.avg_pool_channel(
210                    input,
211                    &mut output,
212                    b,
213                    c,
214                    input_height,
215                    input_width,
216                    output_height,
217                    output_width,
218                )?;
219            }
220        }
221
222        Ok(output)
223    }
224
225    /// Perform average pooling for a single channel
226    #[allow(clippy::too_many_arguments)]
227    fn avg_pool_channel(
228        &self,
229        input: &Tensor,
230        output: &mut Tensor,
231        batch_idx: usize,
232        channel_idx: usize,
233        input_height: usize,
234        input_width: usize,
235        output_height: usize,
236        output_width: usize,
237    ) -> TorshResult<()> {
238        for out_h in 0..output_height {
239            for out_w in 0..output_width {
240                let mut sum = 0.0;
241                let mut count = 0;
242
243                // Iterate over the pooling window
244                for kh in 0..self.kernel_size.0 {
245                    for kw in 0..self.kernel_size.1 {
246                        let in_h = out_h * self.stride.0 + kh;
247                        let in_w = out_w * self.stride.1 + kw;
248
249                        // Apply padding offset
250                        if in_h >= self.padding.0 && in_w >= self.padding.1 {
251                            let padded_in_h = in_h - self.padding.0;
252                            let padded_in_w = in_w - self.padding.1;
253
254                            // Check bounds
255                            if padded_in_h < input_height && padded_in_w < input_width {
256                                let val = input.get(&[
257                                    batch_idx,
258                                    channel_idx,
259                                    padded_in_h,
260                                    padded_in_w,
261                                ])?;
262                                sum += val;
263                                count += 1;
264                            } else if self.count_include_pad {
265                                count += 1; // Count padding zeros
266                            }
267                        } else if self.count_include_pad {
268                            count += 1; // Count padding zeros
269                        }
270                    }
271                }
272
273                // Calculate average
274                let avg = if count > 0 { sum / count as f32 } else { 0.0 };
275                output.set(&[batch_idx, channel_idx, out_h, out_w], avg)?;
276            }
277        }
278
279        Ok(())
280    }
281}
282
283/// Sparse 1D Max Pooling layer
284pub struct SparseMaxPool1d {
285    /// Kernel size
286    kernel_size: usize,
287    /// Stride
288    stride: usize,
289    /// Padding
290    padding: usize,
291    /// Dilation
292    dilation: usize,
293}
294
295impl SparseMaxPool1d {
296    /// Create a new sparse 1D max pooling layer
297    pub fn new(
298        kernel_size: usize,
299        stride: Option<usize>,
300        padding: Option<usize>,
301        dilation: Option<usize>,
302    ) -> Self {
303        let stride = stride.unwrap_or(kernel_size);
304        let padding = padding.unwrap_or(0);
305        let dilation = dilation.unwrap_or(1);
306
307        Self {
308            kernel_size,
309            stride,
310            padding,
311            dilation,
312        }
313    }
314
315    /// Forward pass for sparse 1D max pooling
316    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
317        // Validate input shape: (batch_size, channels, length)
318        let input_shape = input.shape();
319        if input_shape.ndim() != 3 {
320            return Err(TorshError::InvalidArgument(
321                "Input must be 3D tensor (batch_size, channels, length)".to_string(),
322            ));
323        }
324
325        let batch_size = input_shape.dims()[0];
326        let channels = input_shape.dims()[1];
327        let input_length = input_shape.dims()[2];
328
329        // Calculate output length
330        let output_length =
331            (input_length + 2 * self.padding - self.dilation * (self.kernel_size - 1) - 1)
332                / self.stride
333                + 1;
334
335        let output = zeros::<f32>(&[batch_size, channels, output_length])?;
336
337        // Perform max pooling
338        for b in 0..batch_size {
339            for c in 0..channels {
340                for out_pos in 0..output_length {
341                    let mut max_val = f32::NEG_INFINITY;
342                    let mut found_value = false;
343
344                    // Iterate over the pooling window
345                    for k in 0..self.kernel_size {
346                        let in_pos = out_pos * self.stride + k * self.dilation;
347
348                        // Apply padding offset
349                        if in_pos >= self.padding {
350                            let padded_in_pos = in_pos - self.padding;
351
352                            // Check bounds
353                            if padded_in_pos < input_length {
354                                let val = input.get(&[b, c, padded_in_pos])?;
355                                if val > max_val || !found_value {
356                                    max_val = val;
357                                    found_value = true;
358                                }
359                            }
360                        }
361                    }
362
363                    // Set output value
364                    output.set(&[b, c, out_pos], if found_value { max_val } else { 0.0 })?;
365                }
366            }
367        }
368
369        Ok(output)
370    }
371}
372
373/// Sparse 1D Average Pooling layer
374pub struct SparseAvgPool1d {
375    /// Kernel size
376    kernel_size: usize,
377    /// Stride
378    stride: usize,
379    /// Padding
380    padding: usize,
381    /// Whether to count padding zeros in the average
382    count_include_pad: bool,
383}
384
385impl SparseAvgPool1d {
386    /// Create a new sparse 1D average pooling layer
387    pub fn new(
388        kernel_size: usize,
389        stride: Option<usize>,
390        padding: Option<usize>,
391        count_include_pad: bool,
392    ) -> Self {
393        let stride = stride.unwrap_or(kernel_size);
394        let padding = padding.unwrap_or(0);
395
396        Self {
397            kernel_size,
398            stride,
399            padding,
400            count_include_pad,
401        }
402    }
403
404    /// Forward pass for sparse 1D average pooling
405    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
406        // Validate input shape: (batch_size, channels, length)
407        let input_shape = input.shape();
408        if input_shape.ndim() != 3 {
409            return Err(TorshError::InvalidArgument(
410                "Input must be 3D tensor (batch_size, channels, length)".to_string(),
411            ));
412        }
413
414        let batch_size = input_shape.dims()[0];
415        let channels = input_shape.dims()[1];
416        let input_length = input_shape.dims()[2];
417
418        // Calculate output length
419        let output_length = (input_length + 2 * self.padding - self.kernel_size) / self.stride + 1;
420
421        let output = zeros::<f32>(&[batch_size, channels, output_length])?;
422
423        // Perform average pooling
424        for b in 0..batch_size {
425            for c in 0..channels {
426                for out_pos in 0..output_length {
427                    let mut sum = 0.0;
428                    let mut count = 0;
429
430                    // Iterate over the pooling window
431                    for k in 0..self.kernel_size {
432                        let in_pos = out_pos * self.stride + k;
433
434                        // Apply padding offset
435                        if in_pos >= self.padding {
436                            let padded_in_pos = in_pos - self.padding;
437
438                            // Check bounds
439                            if padded_in_pos < input_length {
440                                let val = input.get(&[b, c, padded_in_pos])?;
441                                sum += val;
442                                count += 1;
443                            } else if self.count_include_pad {
444                                count += 1; // Count padding zeros
445                            }
446                        } else if self.count_include_pad {
447                            count += 1; // Count padding zeros
448                        }
449                    }
450
451                    // Calculate average
452                    let avg = if count > 0 { sum / count as f32 } else { 0.0 };
453                    output.set(&[b, c, out_pos], avg)?;
454                }
455            }
456        }
457
458        Ok(output)
459    }
460}
461
462/// Sparse Adaptive Max Pooling 2D
463///
464/// Performs adaptive max pooling to produce a fixed output size regardless of input dimensions.
465/// Useful for global pooling operations in sparse neural networks.
466pub struct SparseAdaptiveMaxPool2d {
467    /// Target output size (height, width)
468    output_size: (usize, usize),
469}
470
471impl SparseAdaptiveMaxPool2d {
472    /// Create a new sparse adaptive max pooling layer
473    pub fn new(output_size: (usize, usize)) -> Self {
474        Self { output_size }
475    }
476
477    /// Forward pass for sparse adaptive max pooling
478    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
479        // Validate input shape: (batch_size, channels, height, width)
480        let input_shape = input.shape();
481        if input_shape.ndim() != 4 {
482            return Err(TorshError::InvalidArgument(
483                "Input must be 4D tensor (batch_size, channels, height, width)".to_string(),
484            ));
485        }
486
487        let batch_size = input_shape.dims()[0];
488        let channels = input_shape.dims()[1];
489        let input_height = input_shape.dims()[2];
490        let input_width = input_shape.dims()[3];
491
492        let output_height = self.output_size.0;
493        let output_width = self.output_size.1;
494
495        let output = zeros::<f32>(&[batch_size, channels, output_height, output_width])?;
496
497        // Perform adaptive max pooling
498        for b in 0..batch_size {
499            for c in 0..channels {
500                for out_h in 0..output_height {
501                    for out_w in 0..output_width {
502                        // Calculate adaptive pooling window
503                        let start_h = (out_h * input_height) / output_height;
504                        let end_h = ((out_h + 1) * input_height).div_ceil(output_height);
505                        let start_w = (out_w * input_width) / output_width;
506                        let end_w = ((out_w + 1) * input_width).div_ceil(output_width);
507
508                        let mut max_val = f32::NEG_INFINITY;
509                        let mut found_value = false;
510
511                        // Find max in adaptive window
512                        for h in start_h..end_h {
513                            for w in start_w..end_w {
514                                if h < input_height && w < input_width {
515                                    let val = input.get(&[b, c, h, w])?;
516                                    if val > max_val || !found_value {
517                                        max_val = val;
518                                        found_value = true;
519                                    }
520                                }
521                            }
522                        }
523
524                        output.set(
525                            &[b, c, out_h, out_w],
526                            if found_value { max_val } else { 0.0 },
527                        )?;
528                    }
529                }
530            }
531        }
532
533        Ok(output)
534    }
535}
536
537/// Sparse Adaptive Average Pooling 2D
538///
539/// Performs adaptive average pooling to produce a fixed output size regardless of input dimensions.
540/// Computes averages over adaptive window sizes.
541pub struct SparseAdaptiveAvgPool2d {
542    /// Target output size (height, width)
543    output_size: (usize, usize),
544}
545
546impl SparseAdaptiveAvgPool2d {
547    /// Create a new sparse adaptive average pooling layer
548    pub fn new(output_size: (usize, usize)) -> Self {
549        Self { output_size }
550    }
551
552    /// Forward pass for sparse adaptive average pooling
553    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
554        // Validate input shape: (batch_size, channels, height, width)
555        let input_shape = input.shape();
556        if input_shape.ndim() != 4 {
557            return Err(TorshError::InvalidArgument(
558                "Input must be 4D tensor (batch_size, channels, height, width)".to_string(),
559            ));
560        }
561
562        let batch_size = input_shape.dims()[0];
563        let channels = input_shape.dims()[1];
564        let input_height = input_shape.dims()[2];
565        let input_width = input_shape.dims()[3];
566
567        let output_height = self.output_size.0;
568        let output_width = self.output_size.1;
569
570        let output = zeros::<f32>(&[batch_size, channels, output_height, output_width])?;
571
572        // Perform adaptive average pooling
573        for b in 0..batch_size {
574            for c in 0..channels {
575                for out_h in 0..output_height {
576                    for out_w in 0..output_width {
577                        // Calculate adaptive pooling window
578                        let start_h = (out_h * input_height) / output_height;
579                        let end_h = ((out_h + 1) * input_height).div_ceil(output_height);
580                        let start_w = (out_w * input_width) / output_width;
581                        let end_w = ((out_w + 1) * input_width).div_ceil(output_width);
582
583                        let mut sum = 0.0;
584                        let mut count = 0;
585
586                        // Calculate average in adaptive window
587                        for h in start_h..end_h {
588                            for w in start_w..end_w {
589                                if h < input_height && w < input_width {
590                                    let val = input.get(&[b, c, h, w])?;
591                                    sum += val;
592                                    count += 1;
593                                }
594                            }
595                        }
596
597                        let avg = if count > 0 { sum / count as f32 } else { 0.0 };
598                        output.set(&[b, c, out_h, out_w], avg)?;
599                    }
600                }
601            }
602        }
603
604        Ok(output)
605    }
606}