1use crate::{FloatElement, Tensor};
4use torsh_core::error::{Result, TorshError};
5use torsh_core::TensorElement;
6
7impl<T: FloatElement> Tensor<T> {
8 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 fn add_channel_bias(
157 output_data: &mut [T],
158 bias_data: &[T],
159 out_channels: usize,
160 spatial_size: usize,
161 ) {
162 if spatial_size == 0 || out_channels == 0 {
164 return;
165 }
166
167 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 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 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 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 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 let mut output_data =
240 vec![<T as TensorElement>::zero(); batch_size * out_channels * output_length];
241
242 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 let mut output = Tensor::from_data(
300 output_data,
301 vec![batch_size, out_channels, output_length],
302 self.device(),
303 )?;
304
305 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 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 output = Tensor::from_data(
323 output_data,
324 vec![batch_size, out_channels, output_length],
325 self.device(),
326 )?;
327 }
328
329 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 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 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 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 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 let mut output_data = vec![
416 <T as TensorElement>::zero();
417 batch_size * out_channels * output_height * output_width
418 ];
419
420 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 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 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 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 output = Tensor::from_data(
513 output_data,
514 vec![batch_size, out_channels, output_height, output_width],
515 self.device(),
516 )?;
517 }
518
519 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut output_data = vec![
823 <T as TensorElement>::zero();
824 batch_size * in_channels * output_height * output_width
825 ];
826
827 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 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 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 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 output = Tensor::from_data(
919 output_data,
920 vec![batch_size, in_channels, output_height, output_width],
921 self.device(),
922 )?;
923 }
924
925 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 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 let depthwise_output = self.depthwise_conv2d(
958 depthwise_weight,
959 None, stride,
961 padding,
962 dilation,
963 )?;
964
965 let output = depthwise_output.conv2d(
967 pointwise_weight,
968 bias,
969 (1, 1), (0, 0), (1, 1), 1, )?;
974
975 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 #[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 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 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 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 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 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 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 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 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 output = Tensor::from_data(
1158 output_data,
1159 vec![batch_size, out_channels, output_height, output_width],
1160 self.device(),
1161 )?;
1162 }
1163
1164 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 for value in &mut kernel {
1474 *value /= sum;
1475 }
1476
1477 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 self.xcorr1d(&kernel_tensor, CorrelationMode::Same)
1489 }
1490
1491 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 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 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 for value in &mut kernel {
1547 *value /= sum;
1548 }
1549
1550 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 self.xcorr2d(&kernel_tensor, CorrelationMode::Same)
1563 }
1564}
1565
1566#[derive(Debug, Clone, Copy, PartialEq)]
1568pub enum CorrelationMode {
1569 Full,
1571 Valid,
1573 Same,
1575}