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