Skip to main content

torsh_tensor/
conv.rs

1//! Convolution and signal processing operations for tensors
2
3use crate::{FloatElement, Tensor};
4use torsh_core::error::{Result, TorshError};
5use torsh_core::TensorElement;
6
7impl<T: FloatElement> Tensor<T> {
8    /// Broadcast a per-output-channel bias across a contiguous convolution output.
9    ///
10    /// `output_data` is laid out row-major as `[batch, out_channels, spatial...]`,
11    /// so each `(batch, channel)` pair owns a contiguous run of `spatial_size`
12    /// elements. Every element of channel `c`'s run receives `bias_data[c]` added
13    /// to it, identically for every batch element. This is the standard channel-wise
14    /// broadcast of a `[C_out]` bias vector across an `[N, C_out, *spatial]` tensor.
15    ///
16    /// Iterating contiguous channel-sized chunks (instead of recomputing a flat
17    /// index per element) keeps the access pattern cache-friendly and lets the
18    /// compiler auto-vectorize the inner scalar add.
19    fn add_channel_bias(
20        output_data: &mut [T],
21        bias_data: &[T],
22        out_channels: usize,
23        spatial_size: usize,
24    ) {
25        // `chunks_mut(0)` panics, and an empty buffer has nothing to broadcast.
26        if spatial_size == 0 || out_channels == 0 {
27            return;
28        }
29
30        // Each chunk is one (batch, channel) block; chunks advance in row-major
31        // order, so the channel is the chunk index modulo the channel count.
32        for (block_index, block) in output_data.chunks_mut(spatial_size).enumerate() {
33            let channel = block_index % out_channels;
34            let bias = bias_data[channel];
35            for value in block.iter_mut() {
36                *value = *value + bias;
37            }
38        }
39    }
40
41    /// 1D convolution operation
42    pub fn conv1d(
43        &self,
44        weight: &Self,
45        bias: Option<&Self>,
46        stride: usize,
47        padding: usize,
48        dilation: usize,
49        groups: usize,
50    ) -> Result<Self> {
51        // Input shape: (N, C_in, L)
52        // Weight shape: (C_out, C_in/groups, kernel_size)
53        // Output shape: (N, C_out, L_out)
54
55        let input_shape_obj = self.shape();
56        let input_shape = input_shape_obj.dims();
57        let weight_shape_obj = weight.shape();
58        let weight_shape = weight_shape_obj.dims();
59
60        if input_shape.len() != 3 {
61            return Err(TorshError::InvalidArgument(format!(
62                "Expected 3D input tensor for conv1d, got {}D",
63                input_shape.len()
64            )));
65        }
66
67        if weight_shape.len() != 3 {
68            return Err(TorshError::InvalidArgument(format!(
69                "Expected 3D weight tensor for conv1d, got {}D",
70                weight_shape.len()
71            )));
72        }
73
74        let batch_size = input_shape[0];
75        let in_channels = input_shape[1];
76        let input_length = input_shape[2];
77
78        let out_channels = weight_shape[0];
79        let kernel_size = weight_shape[2];
80
81        // Check groups
82        if in_channels % groups != 0 || out_channels % groups != 0 {
83            return Err(TorshError::InvalidArgument(
84                "in_channels and out_channels must be divisible by groups".to_string(),
85            ));
86        }
87
88        if weight_shape[1] != in_channels / groups {
89            return Err(TorshError::InvalidArgument(format!(
90                "Weight tensor has wrong number of input channels: expected {}, got {}",
91                in_channels / groups,
92                weight_shape[1]
93            )));
94        }
95
96        // Calculate output length
97        let effective_kernel = (kernel_size - 1) * dilation + 1;
98        let padded_length = input_length + 2 * padding;
99        let output_length = (padded_length - effective_kernel) / stride + 1;
100
101        // Initialize output
102        let mut output_data =
103            vec![<T as TensorElement>::zero(); batch_size * out_channels * output_length];
104
105        // Perform convolution
106        for n in 0..batch_size {
107            for g in 0..groups {
108                let out_ch_start = g * (out_channels / groups);
109                let out_ch_end = (g + 1) * (out_channels / groups);
110                let in_ch_start = g * (in_channels / groups);
111                let in_ch_end = (g + 1) * (in_channels / groups);
112
113                for oc in out_ch_start..out_ch_end {
114                    for ol in 0..output_length {
115                        let mut sum = <T as TensorElement>::zero();
116
117                        for ic in in_ch_start..in_ch_end {
118                            let ic_rel = ic - in_ch_start;
119                            for k in 0..kernel_size {
120                                let il = (ol * stride + k * dilation) as i32 - padding as i32;
121
122                                if il >= 0 && (il as usize) < input_length {
123                                    let input_idx = n * in_channels * input_length
124                                        + ic * input_length
125                                        + il as usize;
126                                    let weight_idx = oc * (in_channels / groups) * kernel_size
127                                        + ic_rel * kernel_size
128                                        + k;
129
130                                    let input_val = self.storage.get(input_idx)?;
131                                    let weight_val = weight.storage.get(weight_idx)?;
132                                    sum = sum + input_val * weight_val;
133                                }
134                            }
135                        }
136
137                        let output_idx = n * out_channels * output_length + oc * output_length + ol;
138                        output_data[output_idx] = sum;
139                    }
140                }
141            }
142        }
143
144        // Create output tensor
145        let mut output = Tensor::from_data(
146            output_data,
147            vec![batch_size, out_channels, output_length],
148            self.device(),
149        )?;
150
151        // Add bias if provided
152        if let Some(b) = bias {
153            if b.shape().dims() != [out_channels] {
154                return Err(TorshError::InvalidArgument(format!(
155                    "Bias must have shape [{}], got {:?}",
156                    out_channels,
157                    b.shape().dims()
158                )));
159            }
160
161            // Broadcast the per-output-channel bias across every spatial position
162            // of each channel, for every batch element.
163            let bias_data = b.to_vec()?;
164            let mut output_data = output.to_vec()?;
165            Self::add_channel_bias(&mut output_data, &bias_data, out_channels, output_length);
166
167            // Recreate tensor with modified data
168            output = Tensor::from_data(
169                output_data,
170                vec![batch_size, out_channels, output_length],
171                self.device(),
172            )?;
173        }
174
175        // Track operation for autograd
176        if self.requires_grad
177            || weight.requires_grad
178            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
179        {
180            use std::sync::Arc;
181            output.requires_grad = true;
182            output.operation = crate::Operation::Custom(
183                "conv1d".to_string(),
184                vec![
185                    Arc::downgrade(&Arc::new(self.clone())),
186                    Arc::downgrade(&Arc::new(weight.clone())),
187                ],
188            );
189        }
190
191        Ok(output)
192    }
193
194    /// 2D convolution operation
195    pub fn conv2d(
196        &self,
197        weight: &Self,
198        bias: Option<&Self>,
199        stride: (usize, usize),
200        padding: (usize, usize),
201        dilation: (usize, usize),
202        groups: usize,
203    ) -> Result<Self> {
204        // Input shape: (N, C_in, H, W)
205        // Weight shape: (C_out, C_in/groups, kernel_h, kernel_w)
206        // Output shape: (N, C_out, H_out, W_out)
207
208        let input_shape_obj = self.shape();
209        let input_shape = input_shape_obj.dims();
210        let weight_shape_obj = weight.shape();
211        let weight_shape = weight_shape_obj.dims();
212
213        if input_shape.len() != 4 {
214            return Err(TorshError::InvalidArgument(format!(
215                "Expected 4D input tensor for conv2d, got {}D",
216                input_shape.len()
217            )));
218        }
219
220        if weight_shape.len() != 4 {
221            return Err(TorshError::InvalidArgument(format!(
222                "Expected 4D weight tensor for conv2d, got {}D",
223                weight_shape.len()
224            )));
225        }
226
227        let batch_size = input_shape[0];
228        let in_channels = input_shape[1];
229        let input_height = input_shape[2];
230        let input_width = input_shape[3];
231
232        let out_channels = weight_shape[0];
233        let kernel_height = weight_shape[2];
234        let kernel_width = weight_shape[3];
235
236        // Check groups
237        if in_channels % groups != 0 || out_channels % groups != 0 {
238            return Err(TorshError::InvalidArgument(
239                "in_channels and out_channels must be divisible by groups".to_string(),
240            ));
241        }
242
243        if weight_shape[1] != in_channels / groups {
244            return Err(TorshError::InvalidArgument(format!(
245                "Weight tensor has wrong number of input channels: expected {}, got {}",
246                in_channels / groups,
247                weight_shape[1]
248            )));
249        }
250
251        // Calculate output dimensions
252        let effective_kernel_h = (kernel_height - 1) * dilation.0 + 1;
253        let effective_kernel_w = (kernel_width - 1) * dilation.1 + 1;
254        let padded_height = input_height + 2 * padding.0;
255        let padded_width = input_width + 2 * padding.1;
256        let output_height = (padded_height - effective_kernel_h) / stride.0 + 1;
257        let output_width = (padded_width - effective_kernel_w) / stride.1 + 1;
258
259        // Initialize output
260        let mut output_data = vec![
261            <T as TensorElement>::zero();
262            batch_size * out_channels * output_height * output_width
263        ];
264
265        let self_data = self.to_vec()?;
266        let weight_data = weight.to_vec()?;
267
268        // Perform convolution
269        for n in 0..batch_size {
270            for g in 0..groups {
271                let out_ch_start = g * (out_channels / groups);
272                let out_ch_end = (g + 1) * (out_channels / groups);
273                let in_ch_start = g * (in_channels / groups);
274                let in_ch_end = (g + 1) * (in_channels / groups);
275
276                for oc in out_ch_start..out_ch_end {
277                    for oh in 0..output_height {
278                        for ow in 0..output_width {
279                            let mut sum = <T as TensorElement>::zero();
280
281                            for ic in in_ch_start..in_ch_end {
282                                let ic_rel = ic - in_ch_start;
283                                for kh in 0..kernel_height {
284                                    for kw in 0..kernel_width {
285                                        let ih = (oh * stride.0 + kh * dilation.0) as i32
286                                            - padding.0 as i32;
287                                        let iw = (ow * stride.1 + kw * dilation.1) as i32
288                                            - padding.1 as i32;
289
290                                        if ih >= 0
291                                            && (ih as usize) < input_height
292                                            && iw >= 0
293                                            && (iw as usize) < input_width
294                                        {
295                                            let input_idx =
296                                                n * in_channels * input_height * input_width
297                                                    + ic * input_height * input_width
298                                                    + ih as usize * input_width
299                                                    + iw as usize;
300                                            let weight_idx = oc
301                                                * (in_channels / groups)
302                                                * kernel_height
303                                                * kernel_width
304                                                + ic_rel * kernel_height * kernel_width
305                                                + kh * kernel_width
306                                                + kw;
307
308                                            sum = sum
309                                                + self_data[input_idx] * weight_data[weight_idx];
310                                        }
311                                    }
312                                }
313                            }
314
315                            let output_idx = n * out_channels * output_height * output_width
316                                + oc * output_height * output_width
317                                + oh * output_width
318                                + ow;
319                            output_data[output_idx] = sum;
320                        }
321                    }
322                }
323            }
324        }
325
326        // Create output tensor
327        let mut output = Tensor::from_data(
328            output_data,
329            vec![batch_size, out_channels, output_height, output_width],
330            self.device(),
331        )?;
332
333        // Add bias if provided
334        if let Some(b) = bias {
335            if b.shape().dims() != [out_channels] {
336                return Err(TorshError::InvalidArgument(format!(
337                    "Bias must have shape [{}], got {:?}",
338                    out_channels,
339                    b.shape().dims()
340                )));
341            }
342
343            // Broadcast the per-output-channel bias across the (H, W) spatial map
344            // of each channel, for every batch element.
345            let bias_data = b.to_vec()?;
346            let mut output_data = output.to_vec()?;
347            Self::add_channel_bias(
348                &mut output_data,
349                &bias_data,
350                out_channels,
351                output_height * output_width,
352            );
353
354            // Create new output tensor with bias added
355            output = Tensor::from_data(
356                output_data,
357                vec![batch_size, out_channels, output_height, output_width],
358                self.device(),
359            )?;
360        }
361
362        // Track operation for autograd
363        if self.requires_grad
364            || weight.requires_grad
365            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
366        {
367            use std::sync::Arc;
368            output.requires_grad = true;
369            output.operation = crate::Operation::Custom(
370                "conv2d".to_string(),
371                vec![
372                    Arc::downgrade(&Arc::new(self.clone())),
373                    Arc::downgrade(&Arc::new(weight.clone())),
374                ],
375            );
376        }
377
378        Ok(output)
379    }
380
381    /// 3D convolution operation
382    pub fn conv3d(
383        &self,
384        weight: &Self,
385        bias: Option<&Self>,
386        stride: (usize, usize, usize),
387        padding: (usize, usize, usize),
388        dilation: (usize, usize, usize),
389        groups: usize,
390    ) -> Result<Self> {
391        // Input shape: (N, C_in, D, H, W)
392        // Weight shape: (C_out, C_in/groups, kernel_d, kernel_h, kernel_w)
393        // Output shape: (N, C_out, D_out, H_out, W_out)
394
395        let input_shape_obj = self.shape();
396        let input_shape = input_shape_obj.dims();
397        let weight_shape_obj = weight.shape();
398        let weight_shape = weight_shape_obj.dims();
399
400        if input_shape.len() != 5 {
401            return Err(TorshError::InvalidArgument(format!(
402                "Expected 5D input tensor for conv3d, got {}D",
403                input_shape.len()
404            )));
405        }
406
407        if weight_shape.len() != 5 {
408            return Err(TorshError::InvalidArgument(format!(
409                "Expected 5D weight tensor for conv3d, got {}D",
410                weight_shape.len()
411            )));
412        }
413
414        let batch_size = input_shape[0];
415        let in_channels = input_shape[1];
416        let input_depth = input_shape[2];
417        let input_height = input_shape[3];
418        let input_width = input_shape[4];
419
420        let out_channels = weight_shape[0];
421        let kernel_depth = weight_shape[2];
422        let kernel_height = weight_shape[3];
423        let kernel_width = weight_shape[4];
424
425        // Check groups
426        if in_channels % groups != 0 || out_channels % groups != 0 {
427            return Err(TorshError::InvalidArgument(
428                "in_channels and out_channels must be divisible by groups".to_string(),
429            ));
430        }
431
432        if weight_shape[1] != in_channels / groups {
433            return Err(TorshError::InvalidArgument(format!(
434                "Weight tensor has wrong number of input channels: expected {}, got {}",
435                in_channels / groups,
436                weight_shape[1]
437            )));
438        }
439
440        // Calculate output dimensions
441        let effective_kernel_d = (kernel_depth - 1) * dilation.0 + 1;
442        let effective_kernel_h = (kernel_height - 1) * dilation.1 + 1;
443        let effective_kernel_w = (kernel_width - 1) * dilation.2 + 1;
444        let padded_depth = input_depth + 2 * padding.0;
445        let padded_height = input_height + 2 * padding.1;
446        let padded_width = input_width + 2 * padding.2;
447        let output_depth = (padded_depth - effective_kernel_d) / stride.0 + 1;
448        let output_height = (padded_height - effective_kernel_h) / stride.1 + 1;
449        let output_width = (padded_width - effective_kernel_w) / stride.2 + 1;
450
451        // Initialize output
452        let output_size = batch_size * out_channels * output_depth * output_height * output_width;
453        let mut output_data = vec![<T as TensorElement>::zero(); output_size];
454
455        let self_data = self.to_vec()?;
456        let weight_data = weight.to_vec()?;
457
458        // Perform convolution
459        for n in 0..batch_size {
460            for g in 0..groups {
461                let out_ch_start = g * (out_channels / groups);
462                let out_ch_end = (g + 1) * (out_channels / groups);
463                let in_ch_start = g * (in_channels / groups);
464                let in_ch_end = (g + 1) * (in_channels / groups);
465
466                for oc in out_ch_start..out_ch_end {
467                    for od in 0..output_depth {
468                        for oh in 0..output_height {
469                            for ow in 0..output_width {
470                                let mut sum = <T as TensorElement>::zero();
471
472                                for ic in in_ch_start..in_ch_end {
473                                    let ic_rel = ic - in_ch_start;
474                                    for kd in 0..kernel_depth {
475                                        for kh in 0..kernel_height {
476                                            for kw in 0..kernel_width {
477                                                let id = (od * stride.0 + kd * dilation.0) as i32
478                                                    - padding.0 as i32;
479                                                let ih = (oh * stride.1 + kh * dilation.1) as i32
480                                                    - padding.1 as i32;
481                                                let iw = (ow * stride.2 + kw * dilation.2) as i32
482                                                    - padding.2 as i32;
483
484                                                if id >= 0
485                                                    && (id as usize) < input_depth
486                                                    && ih >= 0
487                                                    && (ih as usize) < input_height
488                                                    && iw >= 0
489                                                    && (iw as usize) < input_width
490                                                {
491                                                    let input_idx = n
492                                                        * in_channels
493                                                        * input_depth
494                                                        * input_height
495                                                        * input_width
496                                                        + ic * input_depth
497                                                            * input_height
498                                                            * input_width
499                                                        + id as usize * input_height * input_width
500                                                        + ih as usize * input_width
501                                                        + iw as usize;
502                                                    let weight_idx = oc
503                                                        * (in_channels / groups)
504                                                        * kernel_depth
505                                                        * kernel_height
506                                                        * kernel_width
507                                                        + ic_rel
508                                                            * kernel_depth
509                                                            * kernel_height
510                                                            * kernel_width
511                                                        + kd * kernel_height * kernel_width
512                                                        + kh * kernel_width
513                                                        + kw;
514
515                                                    sum = sum
516                                                        + self_data[input_idx]
517                                                            * weight_data[weight_idx];
518                                                }
519                                            }
520                                        }
521                                    }
522                                }
523
524                                let output_idx =
525                                    n * out_channels * output_depth * output_height * output_width
526                                        + oc * output_depth * output_height * output_width
527                                        + od * output_height * output_width
528                                        + oh * output_width
529                                        + ow;
530                                output_data[output_idx] = sum;
531                            }
532                        }
533                    }
534                }
535            }
536        }
537
538        // Create output tensor
539        let mut output = Tensor::from_data(
540            output_data,
541            vec![
542                batch_size,
543                out_channels,
544                output_depth,
545                output_height,
546                output_width,
547            ],
548            self.device(),
549        )?;
550
551        // Add bias if provided
552        if let Some(b) = bias {
553            if b.shape().dims() != [out_channels] {
554                return Err(TorshError::InvalidArgument(format!(
555                    "Bias must have shape [{}], got {:?}",
556                    out_channels,
557                    b.shape().dims()
558                )));
559            }
560
561            // Broadcast the per-output-channel bias across the (D, H, W) spatial
562            // volume of each channel, for every batch element.
563            let bias_data = b.to_vec()?;
564            let mut output_data = output.to_vec()?;
565            Self::add_channel_bias(
566                &mut output_data,
567                &bias_data,
568                out_channels,
569                output_depth * output_height * output_width,
570            );
571
572            // Create new output tensor with bias added
573            output = Tensor::from_data(
574                output_data,
575                vec![
576                    batch_size,
577                    out_channels,
578                    output_depth,
579                    output_height,
580                    output_width,
581                ],
582                self.device(),
583            )?;
584        }
585
586        // Track operation for autograd
587        if self.requires_grad
588            || weight.requires_grad
589            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
590        {
591            use std::sync::Arc;
592            output.requires_grad = true;
593            output.operation = crate::Operation::Custom(
594                "conv3d".to_string(),
595                vec![
596                    Arc::downgrade(&Arc::new(self.clone())),
597                    Arc::downgrade(&Arc::new(weight.clone())),
598                ],
599            );
600        }
601
602        Ok(output)
603    }
604
605    /// Depthwise 2D convolution operation
606    /// Each input channel is convolved with its own kernel independently
607    pub fn depthwise_conv2d(
608        &self,
609        weight: &Self,
610        bias: Option<&Self>,
611        stride: (usize, usize),
612        padding: (usize, usize),
613        dilation: (usize, usize),
614    ) -> Result<Self> {
615        // Input shape: (N, C_in, H, W)
616        // Weight shape: (C_in, 1, kernel_h, kernel_w) - each channel has its own kernel
617        // Output shape: (N, C_in, H_out, W_out)
618
619        let input_shape_obj = self.shape();
620        let input_shape = input_shape_obj.dims();
621        let weight_shape_obj = weight.shape();
622        let weight_shape = weight_shape_obj.dims();
623
624        if input_shape.len() != 4 {
625            return Err(TorshError::InvalidArgument(format!(
626                "Expected 4D input tensor for depthwise_conv2d, got {}D",
627                input_shape.len()
628            )));
629        }
630
631        if weight_shape.len() != 4 {
632            return Err(TorshError::InvalidArgument(format!(
633                "Expected 4D weight tensor for depthwise_conv2d, got {}D",
634                weight_shape.len()
635            )));
636        }
637
638        let batch_size = input_shape[0];
639        let in_channels = input_shape[1];
640        let input_height = input_shape[2];
641        let input_width = input_shape[3];
642
643        let kernel_height = weight_shape[2];
644        let kernel_width = weight_shape[3];
645
646        // For depthwise conv, weight should have shape (C_in, 1, kernel_h, kernel_w)
647        if weight_shape[0] != in_channels || weight_shape[1] != 1 {
648            return Err(TorshError::InvalidArgument(format!(
649                "Weight tensor must have shape ({}, 1, kernel_h, kernel_w), got ({}, {}, {}, {})",
650                in_channels, weight_shape[0], weight_shape[1], weight_shape[2], weight_shape[3]
651            )));
652        }
653
654        // Calculate output dimensions
655        let effective_kernel_h = (kernel_height - 1) * dilation.0 + 1;
656        let effective_kernel_w = (kernel_width - 1) * dilation.1 + 1;
657        let padded_height = input_height + 2 * padding.0;
658        let padded_width = input_width + 2 * padding.1;
659        let output_height = (padded_height - effective_kernel_h) / stride.0 + 1;
660        let output_width = (padded_width - effective_kernel_w) / stride.1 + 1;
661
662        // Initialize output
663        let mut output_data = vec![
664            <T as TensorElement>::zero();
665            batch_size * in_channels * output_height * output_width
666        ];
667
668        let _self_data = self.to_vec()?;
669        let _weight_data = weight.to_vec()?;
670
671        // Perform depthwise convolution
672        for n in 0..batch_size {
673            for c in 0..in_channels {
674                for oh in 0..output_height {
675                    for ow in 0..output_width {
676                        let mut sum = <T as TensorElement>::zero();
677
678                        for kh in 0..kernel_height {
679                            for kw in 0..kernel_width {
680                                let ih =
681                                    (oh * stride.0 + kh * dilation.0) as i32 - padding.0 as i32;
682                                let iw =
683                                    (ow * stride.1 + kw * dilation.1) as i32 - padding.1 as i32;
684
685                                if ih >= 0
686                                    && (ih as usize) < input_height
687                                    && iw >= 0
688                                    && (iw as usize) < input_width
689                                {
690                                    let input_idx = n * in_channels * input_height * input_width
691                                        + c * input_height * input_width
692                                        + ih as usize * input_width
693                                        + iw as usize;
694                                    let weight_idx =
695                                        c * kernel_height * kernel_width + kh * kernel_width + kw;
696
697                                    let input_val = self.storage.get(input_idx)?;
698                                    let weight_val = weight.storage.get(weight_idx)?;
699                                    sum = sum + input_val * weight_val;
700                                }
701                            }
702                        }
703
704                        let output_idx = n * in_channels * output_height * output_width
705                            + c * output_height * output_width
706                            + oh * output_width
707                            + ow;
708                        output_data[output_idx] = sum;
709                    }
710                }
711            }
712        }
713
714        // Create output tensor
715        let mut output = Tensor::from_data(
716            output_data,
717            vec![batch_size, in_channels, output_height, output_width],
718            self.device(),
719        )?;
720
721        // Add bias if provided
722        if let Some(b) = bias {
723            if b.shape().dims() != [in_channels] {
724                return Err(TorshError::InvalidArgument(format!(
725                    "Bias must have shape [{}], got {:?}",
726                    in_channels,
727                    b.shape().dims()
728                )));
729            }
730
731            // Broadcast the per-channel bias across the (H, W) spatial map of each
732            // channel, for every batch element. Depthwise conv keeps one output
733            // channel per input channel, so the channel count is `in_channels`.
734            let bias_data = b.to_vec()?;
735            let mut output_data = output.to_vec()?;
736            Self::add_channel_bias(
737                &mut output_data,
738                &bias_data,
739                in_channels,
740                output_height * output_width,
741            );
742
743            // Create new output tensor with bias added
744            output = Tensor::from_data(
745                output_data,
746                vec![batch_size, in_channels, output_height, output_width],
747                self.device(),
748            )?;
749        }
750
751        // Track operation for autograd
752        if self.requires_grad
753            || weight.requires_grad
754            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
755        {
756            use std::sync::Arc;
757            output.requires_grad = true;
758            output.operation = crate::Operation::Custom(
759                "depthwise_conv2d".to_string(),
760                vec![
761                    Arc::downgrade(&Arc::new(self.clone())),
762                    Arc::downgrade(&Arc::new(weight.clone())),
763                ],
764            );
765        }
766
767        Ok(output)
768    }
769
770    /// Separable 2D convolution operation
771    /// Factorized into depthwise convolution followed by pointwise (1x1) convolution
772    pub fn separable_conv2d(
773        &self,
774        depthwise_weight: &Self,
775        pointwise_weight: &Self,
776        bias: Option<&Self>,
777        stride: (usize, usize),
778        padding: (usize, usize),
779        dilation: (usize, usize),
780    ) -> Result<Self> {
781        // Step 1: Depthwise convolution
782        let depthwise_output = self.depthwise_conv2d(
783            depthwise_weight,
784            None, // No bias in depthwise step
785            stride,
786            padding,
787            dilation,
788        )?;
789
790        // Step 2: Pointwise (1x1) convolution
791        let output = depthwise_output.conv2d(
792            pointwise_weight,
793            bias,
794            (1, 1), // stride = 1 for pointwise
795            (0, 0), // padding = 0 for pointwise
796            (1, 1), // dilation = 1 for pointwise
797            1,      // groups = 1 for pointwise
798        )?;
799
800        // Track operation for autograd
801        if self.requires_grad
802            || depthwise_weight.requires_grad
803            || pointwise_weight.requires_grad
804            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
805        {
806            use std::sync::Arc;
807            let mut tracked_output = output;
808            tracked_output.requires_grad = true;
809            tracked_output.operation = crate::Operation::Custom(
810                "separable_conv2d".to_string(),
811                vec![
812                    Arc::downgrade(&Arc::new(self.clone())),
813                    Arc::downgrade(&Arc::new(depthwise_weight.clone())),
814                    Arc::downgrade(&Arc::new(pointwise_weight.clone())),
815                ],
816            );
817            Ok(tracked_output)
818        } else {
819            Ok(output)
820        }
821    }
822
823    /// Transposed (deconvolution) 2D convolution operation
824    #[allow(clippy::too_many_arguments)]
825    pub fn conv_transpose2d(
826        &self,
827        weight: &Self,
828        bias: Option<&Self>,
829        stride: (usize, usize),
830        padding: (usize, usize),
831        output_padding: (usize, usize),
832        dilation: (usize, usize),
833        groups: usize,
834    ) -> Result<Self> {
835        // Input shape: (N, C_in, H, W)
836        // Weight shape: (C_in, C_out/groups, kernel_h, kernel_w)
837        // Output shape: (N, C_out, H_out, W_out)
838
839        let input_shape_obj = self.shape();
840        let input_shape = input_shape_obj.dims();
841        let weight_shape_obj = weight.shape();
842        let weight_shape = weight_shape_obj.dims();
843
844        if input_shape.len() != 4 {
845            return Err(TorshError::InvalidArgument(format!(
846                "Expected 4D input tensor for conv_transpose2d, got {}D",
847                input_shape.len()
848            )));
849        }
850
851        if weight_shape.len() != 4 {
852            return Err(TorshError::InvalidArgument(format!(
853                "Expected 4D weight tensor for conv_transpose2d, got {}D",
854                weight_shape.len()
855            )));
856        }
857
858        let batch_size = input_shape[0];
859        let in_channels = input_shape[1];
860        let input_height = input_shape[2];
861        let input_width = input_shape[3];
862
863        let out_channels = weight_shape[1] * groups;
864        let kernel_height = weight_shape[2];
865        let kernel_width = weight_shape[3];
866
867        // Check groups
868        if in_channels % groups != 0 || out_channels % groups != 0 {
869            return Err(TorshError::InvalidArgument(
870                "in_channels and out_channels must be divisible by groups".to_string(),
871            ));
872        }
873
874        if weight_shape[0] != in_channels {
875            return Err(TorshError::InvalidArgument(format!(
876                "Weight tensor has wrong number of input channels: expected {}, got {}",
877                in_channels, weight_shape[0]
878            )));
879        }
880
881        // Calculate output dimensions
882        let effective_kernel_h = (kernel_height - 1) * dilation.0 + 1;
883        let effective_kernel_w = (kernel_width - 1) * dilation.1 + 1;
884        let output_height =
885            (input_height - 1) * stride.0 - 2 * padding.0 + effective_kernel_h + output_padding.0;
886        let output_width =
887            (input_width - 1) * stride.1 - 2 * padding.1 + effective_kernel_w + output_padding.1;
888
889        // Initialize output
890        let mut output_data = vec![
891            <T as TensorElement>::zero();
892            batch_size * out_channels * output_height * output_width
893        ];
894
895        let self_data = self.to_vec()?;
896        let weight_data = weight.to_vec()?;
897
898        // Perform transposed convolution
899        for n in 0..batch_size {
900            for g in 0..groups {
901                let in_ch_start = g * (in_channels / groups);
902                let in_ch_end = (g + 1) * (in_channels / groups);
903                let out_ch_start = g * (out_channels / groups);
904                let out_ch_end = (g + 1) * (out_channels / groups);
905
906                for ic in in_ch_start..in_ch_end {
907                    for ih in 0..input_height {
908                        for iw in 0..input_width {
909                            let input_val = self_data[n * in_channels * input_height * input_width
910                                + ic * input_height * input_width
911                                + ih * input_width
912                                + iw];
913
914                            for oc in out_ch_start..out_ch_end {
915                                let oc_rel = oc - out_ch_start;
916                                for kh in 0..kernel_height {
917                                    for kw in 0..kernel_width {
918                                        let oh = ih * stride.0 + kh * dilation.0;
919                                        let ow = iw * stride.1 + kw * dilation.1;
920
921                                        if oh >= padding.0 && ow >= padding.1 {
922                                            let oh_final = oh - padding.0;
923                                            let ow_final = ow - padding.1;
924
925                                            if oh_final < output_height && ow_final < output_width {
926                                                let weight_idx = ic
927                                                    * (out_channels / groups)
928                                                    * kernel_height
929                                                    * kernel_width
930                                                    + oc_rel * kernel_height * kernel_width
931                                                    + kh * kernel_width
932                                                    + kw;
933                                                let output_idx =
934                                                    n * out_channels * output_height * output_width
935                                                        + oc * output_height * output_width
936                                                        + oh_final * output_width
937                                                        + ow_final;
938
939                                                output_data[output_idx] = output_data[output_idx]
940                                                    + input_val * weight_data[weight_idx];
941                                            }
942                                        }
943                                    }
944                                }
945                            }
946                        }
947                    }
948                }
949            }
950        }
951
952        // Create output tensor
953        let mut output = Tensor::from_data(
954            output_data,
955            vec![batch_size, out_channels, output_height, output_width],
956            self.device(),
957        )?;
958
959        // Add bias if provided
960        if let Some(b) = bias {
961            if b.shape().dims() != [out_channels] {
962                return Err(TorshError::InvalidArgument(format!(
963                    "Bias must have shape [{}], got {:?}",
964                    out_channels,
965                    b.shape().dims()
966                )));
967            }
968
969            // Broadcast the per-output-channel bias across the (H, W) spatial map
970            // of each channel, for every batch element.
971            let bias_data = b.to_vec()?;
972            let mut output_data = output.to_vec()?;
973            Self::add_channel_bias(
974                &mut output_data,
975                &bias_data,
976                out_channels,
977                output_height * output_width,
978            );
979
980            // Create new output tensor with bias added
981            output = Tensor::from_data(
982                output_data,
983                vec![batch_size, out_channels, output_height, output_width],
984                self.device(),
985            )?;
986        }
987
988        // Track operation for autograd
989        if self.requires_grad
990            || weight.requires_grad
991            || (bias.is_some() && bias.expect("bias checked with is_some").requires_grad)
992        {
993            use std::sync::Arc;
994            output.requires_grad = true;
995            output.operation = crate::Operation::Custom(
996                "conv_transpose2d".to_string(),
997                vec![
998                    Arc::downgrade(&Arc::new(self.clone())),
999                    Arc::downgrade(&Arc::new(weight.clone())),
1000                ],
1001            );
1002        }
1003
1004        Ok(output)
1005    }
1006
1007    /// 1D cross-correlation operation
1008    /// Computes the cross-correlation between two 1D signals
1009    #[allow(clippy::needless_range_loop)]
1010    pub fn xcorr1d(&self, other: &Self, mode: CorrelationMode) -> Result<Self> {
1011        let self_shape_ref = self.shape();
1012        let other_shape_ref = other.shape();
1013        let self_shape = self_shape_ref.dims();
1014        let other_shape = other_shape_ref.dims();
1015
1016        if self_shape.len() != 1 || other_shape.len() != 1 {
1017            return Err(TorshError::InvalidArgument(
1018                "xcorr1d requires 1D tensors".to_string(),
1019            ));
1020        }
1021
1022        let n = self_shape[0];
1023        let m = other_shape[0];
1024
1025        let (output_size, lag_start) = match mode {
1026            CorrelationMode::Full => (n + m - 1, -(m as i32 - 1)),
1027            CorrelationMode::Valid => {
1028                if n < m || m < n {
1029                    return Err(TorshError::InvalidArgument(
1030                        "Valid mode requires both tensors to have the same size or one to be smaller".to_string(),
1031                    ));
1032                }
1033                (std::cmp::max(n, m) - std::cmp::min(n, m) + 1, 0)
1034            }
1035            CorrelationMode::Same => (n, -((m as i32 - 1) / 2)),
1036        };
1037
1038        let mut output_data = vec![<T as TensorElement>::zero(); output_size];
1039        let self_data = self.to_vec()?;
1040        let other_data = other.to_vec()?;
1041
1042        // Compute cross-correlation: (f ★ g)[lag] = Σ_i f[i] * g[i - lag]
1043        for i in 0..output_size {
1044            let mut sum = <T as TensorElement>::zero();
1045            let lag = lag_start + i as i32;
1046
1047            for j in 0..n {
1048                let other_idx = j as i32 - lag;
1049                if other_idx >= 0 && (other_idx as usize) < m {
1050                    sum = sum + self_data[j] * other_data[other_idx as usize];
1051                }
1052            }
1053            output_data[i] = sum;
1054        }
1055
1056        let output = Tensor::from_data(output_data, vec![output_size], self.device())?;
1057
1058        Ok(output)
1059    }
1060
1061    /// 1D auto-correlation operation
1062    /// Computes the auto-correlation of a 1D signal
1063    pub fn autocorr1d(&self, max_lag: Option<usize>) -> Result<Self> {
1064        let shape_ref = self.shape();
1065        let shape = shape_ref.dims();
1066        if shape.len() != 1 {
1067            return Err(TorshError::InvalidArgument(
1068                "autocorr1d requires 1D tensor".to_string(),
1069            ));
1070        }
1071
1072        let n = shape[0];
1073        let max_lag = max_lag.unwrap_or(n - 1).min(n - 1);
1074
1075        let self_data = self.to_vec()?;
1076        let mut output_data = Vec::with_capacity(max_lag + 1);
1077
1078        // Directly compute auto-correlation: R[k] = Σ_n x[n] * x[n-k]
1079        for lag in 0..=max_lag {
1080            let mut sum = <T as TensorElement>::zero();
1081
1082            for i in lag..n {
1083                sum = sum + self_data[i] * self_data[i - lag];
1084            }
1085
1086            output_data.push(sum);
1087        }
1088
1089        let output = Tensor::from_data(output_data, vec![max_lag + 1], self.device())?;
1090        Ok(output)
1091    }
1092
1093    /// 2D cross-correlation operation
1094    /// Computes the 2D cross-correlation between two signals
1095    pub fn xcorr2d(&self, other: &Self, mode: CorrelationMode) -> Result<Self> {
1096        let self_shape_ref = self.shape();
1097        let other_shape_ref = other.shape();
1098        let self_shape = self_shape_ref.dims();
1099        let other_shape = other_shape_ref.dims();
1100
1101        if self_shape.len() != 2 || other_shape.len() != 2 {
1102            return Err(TorshError::InvalidArgument(
1103                "xcorr2d requires 2D tensors".to_string(),
1104            ));
1105        }
1106
1107        let (h1, w1) = (self_shape[0], self_shape[1]);
1108        let (h2, w2) = (other_shape[0], other_shape[1]);
1109
1110        let (out_h, out_w, start_h, start_w) = match mode {
1111            CorrelationMode::Full => (h1 + h2 - 1, w1 + w2 - 1, 0, 0),
1112            CorrelationMode::Valid => {
1113                if h1 < h2 || w1 < w2 {
1114                    return Err(TorshError::InvalidArgument(
1115                        "Valid mode requires first tensor to be larger than or equal to second"
1116                            .to_string(),
1117                    ));
1118                }
1119                (h1 - h2 + 1, w1 - w2 + 1, h2 - 1, w2 - 1)
1120            }
1121            CorrelationMode::Same => (h1, w1, (h2 - 1) / 2, (w2 - 1) / 2),
1122        };
1123
1124        let mut output_data = vec![<T as TensorElement>::zero(); out_h * out_w];
1125        let self_data = self.to_vec()?;
1126        let other_data = other.to_vec()?;
1127
1128        // Compute 2D cross-correlation
1129        for i in 0..out_h {
1130            for j in 0..out_w {
1131                let mut sum = <T as TensorElement>::zero();
1132                let actual_i = i + start_h;
1133                let actual_j = j + start_w;
1134
1135                for ki in 0..h2 {
1136                    for kj in 0..w2 {
1137                        let src_i = actual_i as i32 - ki as i32;
1138                        let src_j = actual_j as i32 - kj as i32;
1139
1140                        if src_i >= 0
1141                            && (src_i as usize) < h1
1142                            && src_j >= 0
1143                            && (src_j as usize) < w1
1144                        {
1145                            let self_idx = src_i as usize * w1 + src_j as usize;
1146                            let other_idx = ki * w2 + kj;
1147                            sum = sum + self_data[self_idx] * other_data[other_idx];
1148                        }
1149                    }
1150                }
1151                output_data[i * out_w + j] = sum;
1152            }
1153        }
1154
1155        let output = Tensor::from_data(output_data, vec![out_h, out_w], self.device())?;
1156        Ok(output)
1157    }
1158
1159    /// 1D median filter
1160    /// Applies a median filter with the specified window size
1161    pub fn median_filter1d(&self, window_size: usize) -> Result<Self> {
1162        let shape_ref = self.shape();
1163        let shape = shape_ref.dims();
1164        if shape.len() != 1 {
1165            return Err(TorshError::InvalidArgument(
1166                "median_filter1d requires 1D tensor".to_string(),
1167            ));
1168        }
1169
1170        if window_size == 0 || window_size % 2 == 0 {
1171            return Err(TorshError::InvalidArgument(
1172                "Window size must be odd and greater than 0".to_string(),
1173            ));
1174        }
1175
1176        let n = shape[0];
1177        let half_window = window_size / 2;
1178        let mut output_data = Vec::with_capacity(n);
1179        let self_data = self.to_vec()?;
1180
1181        for i in 0..n {
1182            let mut window_values = Vec::new();
1183
1184            // Collect values in the window (with padding by repeating edge values)
1185            for j in 0..window_size {
1186                let idx = i as i32 + j as i32 - half_window as i32;
1187                let actual_idx = if idx < 0 {
1188                    0
1189                } else if idx >= n as i32 {
1190                    n - 1
1191                } else {
1192                    idx as usize
1193                };
1194                window_values.push(self_data[actual_idx]);
1195            }
1196
1197            // Sort to find median
1198            window_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1199            output_data.push(window_values[half_window]);
1200        }
1201
1202        let output = Tensor::from_data(output_data, vec![n], self.device())?;
1203        Ok(output)
1204    }
1205
1206    /// 2D median filter
1207    /// Applies a 2D median filter with the specified window size
1208    pub fn median_filter2d(&self, window_size: (usize, usize)) -> Result<Self> {
1209        let shape_ref = self.shape();
1210        let shape = shape_ref.dims();
1211        if shape.len() != 2 {
1212            return Err(TorshError::InvalidArgument(
1213                "median_filter2d requires 2D tensor".to_string(),
1214            ));
1215        }
1216
1217        let (window_h, window_w) = window_size;
1218        if window_h == 0 || window_w == 0 || window_h % 2 == 0 || window_w % 2 == 0 {
1219            return Err(TorshError::InvalidArgument(
1220                "Window dimensions must be odd and greater than 0".to_string(),
1221            ));
1222        }
1223
1224        let (h, w) = (shape[0], shape[1]);
1225        let half_h = window_h / 2;
1226        let half_w = window_w / 2;
1227        let mut output_data = Vec::with_capacity(h * w);
1228        let self_data = self.to_vec()?;
1229
1230        for i in 0..h {
1231            for j in 0..w {
1232                let mut window_values = Vec::new();
1233
1234                // Collect values in the 2D window
1235                for di in 0..window_h {
1236                    for dj in 0..window_w {
1237                        let row = i as i32 + di as i32 - half_h as i32;
1238                        let col = j as i32 + dj as i32 - half_w as i32;
1239
1240                        // Handle boundaries by clamping
1241                        let actual_row = row.max(0).min(h as i32 - 1) as usize;
1242                        let actual_col = col.max(0).min(w as i32 - 1) as usize;
1243
1244                        window_values.push(self_data[actual_row * w + actual_col]);
1245                    }
1246                }
1247
1248                // Sort to find median
1249                window_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1250                output_data.push(window_values[window_values.len() / 2]);
1251            }
1252        }
1253
1254        let output = Tensor::from_data(output_data, vec![h, w], self.device())?;
1255        Ok(output)
1256    }
1257
1258    /// 1D Gaussian filter
1259    /// Applies a Gaussian filter with specified sigma (standard deviation)
1260    pub fn gaussian_filter1d(&self, sigma: f32, kernel_size: Option<usize>) -> Result<Self> {
1261        let tensor_shape = self.shape();
1262        let shape = tensor_shape.dims();
1263        if shape.len() != 1 {
1264            return Err(TorshError::InvalidArgument(
1265                "gaussian_filter1d requires 1D tensor".to_string(),
1266            ));
1267        }
1268
1269        if sigma <= 0.0 {
1270            return Err(TorshError::InvalidArgument(
1271                "Sigma must be positive".to_string(),
1272            ));
1273        }
1274
1275        // Calculate kernel size if not provided (6 sigma rule)
1276        let kernel_size = kernel_size.unwrap_or(((6.0 * sigma) as usize).max(3));
1277        let kernel_size = if kernel_size % 2 == 0 {
1278            kernel_size + 1
1279        } else {
1280            kernel_size
1281        };
1282
1283        // Generate Gaussian kernel
1284        let half_size = kernel_size / 2;
1285        let mut kernel = Vec::with_capacity(kernel_size);
1286        let mut sum = 0.0f32;
1287
1288        for i in 0..kernel_size {
1289            let x = i as f32 - half_size as f32;
1290            let value = (-0.5 * (x / sigma).powi(2)).exp();
1291            kernel.push(value);
1292            sum += value;
1293        }
1294
1295        // Normalize kernel
1296        for value in &mut kernel {
1297            *value /= sum;
1298        }
1299
1300        // Create kernel tensor
1301        let kernel_data: Vec<T> = kernel
1302            .into_iter()
1303            .map(|v| {
1304                T::from(v as f64)
1305                    .unwrap_or_else(|| T::from(0.0).expect("numeric conversion should succeed"))
1306            })
1307            .collect();
1308        let kernel_tensor = Tensor::from_data(kernel_data, vec![kernel_size], self.device())?;
1309
1310        // Apply convolution (which is equivalent to correlation for symmetric kernels)
1311        self.xcorr1d(&kernel_tensor, CorrelationMode::Same)
1312    }
1313
1314    /// 2D Gaussian filter
1315    /// Applies a 2D Gaussian filter with specified sigma values
1316    pub fn gaussian_filter2d(
1317        &self,
1318        sigma: (f32, f32),
1319        kernel_size: Option<(usize, usize)>,
1320    ) -> Result<Self> {
1321        let tensor_shape = self.shape();
1322        let shape = tensor_shape.dims();
1323        if shape.len() != 2 {
1324            return Err(TorshError::InvalidArgument(
1325                "gaussian_filter2d requires 2D tensor".to_string(),
1326            ));
1327        }
1328
1329        let (sigma_x, sigma_y) = sigma;
1330        if sigma_x <= 0.0 || sigma_y <= 0.0 {
1331            return Err(TorshError::InvalidArgument(
1332                "Sigma values must be positive".to_string(),
1333            ));
1334        }
1335
1336        // Calculate kernel sizes if not provided
1337        let (kernel_h, kernel_w) = kernel_size.unwrap_or((
1338            ((6.0 * sigma_y) as usize).max(3),
1339            ((6.0 * sigma_x) as usize).max(3),
1340        ));
1341        let kernel_h = if kernel_h % 2 == 0 {
1342            kernel_h + 1
1343        } else {
1344            kernel_h
1345        };
1346        let kernel_w = if kernel_w % 2 == 0 {
1347            kernel_w + 1
1348        } else {
1349            kernel_w
1350        };
1351
1352        // Generate 2D Gaussian kernel
1353        let half_h = kernel_h / 2;
1354        let half_w = kernel_w / 2;
1355        let mut kernel = Vec::with_capacity(kernel_h * kernel_w);
1356        let mut sum = 0.0f32;
1357
1358        for i in 0..kernel_h {
1359            for j in 0..kernel_w {
1360                let y = i as f32 - half_h as f32;
1361                let x = j as f32 - half_w as f32;
1362                let value = (-0.5 * ((x / sigma_x).powi(2) + (y / sigma_y).powi(2))).exp();
1363                kernel.push(value);
1364                sum += value;
1365            }
1366        }
1367
1368        // Normalize kernel
1369        for value in &mut kernel {
1370            *value /= sum;
1371        }
1372
1373        // Create kernel tensor
1374        let kernel_data: Vec<T> = kernel
1375            .into_iter()
1376            .map(|v| {
1377                T::from(v as f64)
1378                    .unwrap_or_else(|| T::from(0.0).expect("numeric conversion should succeed"))
1379            })
1380            .collect();
1381        let kernel_tensor =
1382            Tensor::from_data(kernel_data, vec![kernel_h, kernel_w], self.device())?;
1383
1384        // Apply 2D correlation
1385        self.xcorr2d(&kernel_tensor, CorrelationMode::Same)
1386    }
1387}
1388
1389/// Correlation modes for signal processing operations
1390#[derive(Debug, Clone, Copy, PartialEq)]
1391pub enum CorrelationMode {
1392    /// Full correlation output
1393    Full,
1394    /// Valid correlation output (no padding)
1395    Valid,
1396    /// Same size as input (with padding)
1397    Same,
1398}