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