1use burn_backend::ops::ModuleOps;
2use burn_dispatch::Dispatch;
3use burn_std::{MatmulTransformAction, MatmulTransformAnalysis, MatmulTransformPolicy};
4
5use crate::{
6 Bool, DType, Int, Tensor, check,
7 check::TensorCheck,
8 kind::Basic,
9 ops::{
10 AttentionModuleOptions, AvgPoolOptions, BridgeTensor, ConvOptions, ConvTransposeOptions,
11 DeformConvOptions, InterpolateOptions, MaxPoolOptions, PadMode, UnfoldOptions,
12 },
13};
14
15pub fn batch_norm<const D: usize>(
24 input: Tensor<D>,
25 gamma: Tensor<1>,
26 beta: Tensor<1>,
27 mean: Tensor<1>,
28 variance: Tensor<1>,
29 epsilon: f64,
30) -> Tensor<D> {
31 assert!(D >= 2, "batch norm requires an input rank of at least 2");
32 let channels = input.dims()[1];
33 assert_eq!(gamma.dims(), [channels], "invalid batch norm gamma shape");
34 assert_eq!(beta.dims(), [channels], "invalid batch norm beta shape");
35 assert_eq!(mean.dims(), [channels], "invalid batch norm mean shape");
36 assert_eq!(
37 variance.dims(),
38 [channels],
39 "invalid batch norm variance shape"
40 );
41 Tensor::new(BridgeTensor::float(Dispatch::batch_norm(
42 input.primitive.into_float(),
43 gamma.primitive.into_float(),
44 beta.primitive.into_float(),
45 mean.primitive.into_float(),
46 variance.primitive.into_float(),
47 epsilon,
48 )))
49}
50
51pub struct BatchNormTrainOutput<const D: usize> {
53 pub output: Tensor<D>,
55
56 pub mean: Tensor<1>,
58
59 pub variance: Tensor<1>,
62}
63
64pub fn batch_norm_train<const D: usize>(
86 input: Tensor<D>,
87 gamma: Tensor<1>,
88 beta: Tensor<1>,
89 epsilon: f64,
90) -> BatchNormTrainOutput<D> {
91 assert!(D >= 2, "batch norm requires an input rank of at least 2");
92 let channels = input.dims()[1];
93 assert_eq!(gamma.dims(), [channels], "invalid batch norm gamma shape");
94 assert_eq!(beta.dims(), [channels], "invalid batch norm beta shape");
95 let result = Dispatch::batch_norm_train(
96 input.primitive.into_float(),
97 gamma.primitive.into_float(),
98 beta.primitive.into_float(),
99 epsilon,
100 );
101
102 BatchNormTrainOutput {
103 output: Tensor::new(BridgeTensor::float(result.output)),
104 mean: Tensor::new(BridgeTensor::float(result.mean)),
105 variance: Tensor::new(BridgeTensor::float(result.variance)),
106 }
107}
108
109pub fn ctc_loss(
123 log_probs: Tensor<3>,
124 targets: Tensor<2, Int>,
125 input_lengths: Tensor<1, Int>,
126 target_lengths: Tensor<1, Int>,
127 blank: usize,
128) -> Tensor<1> {
129 Tensor::new(BridgeTensor::float(Dispatch::ctc_loss(
130 log_probs.primitive.into_float(),
131 targets.primitive.into(),
132 input_lengths.primitive.into(),
133 target_lengths.primitive.into(),
134 blank,
135 )))
136}
137
138pub fn embedding(weights: Tensor<2>, indices: Tensor<2, Int>) -> Tensor<3> {
140 Tensor::new(BridgeTensor::float(Dispatch::embedding(
141 weights.primitive.into_float(),
142 indices.primitive.into(),
143 )))
144}
145
146pub fn conv1d(
152 x: Tensor<3>,
153 weight: Tensor<3>,
154 bias: Option<Tensor<1>>,
155 options: impl Into<ConvOptions<1>>,
156) -> Tensor<3> {
157 let options = options.into();
158 check!(TensorCheck::conv(
159 "conv1d",
160 x.dims(),
161 weight.dims(),
162 options.groups,
163 ));
164
165 Tensor::new(BridgeTensor::float(Dispatch::conv1d(
166 x.primitive.into_float(),
167 weight.primitive.into_float(),
168 bias.map(|b| b.primitive.into_float()),
169 options,
170 )))
171}
172
173pub fn conv2d(
179 x: Tensor<4>,
180 weight: Tensor<4>,
181 bias: Option<Tensor<1>>,
182 options: impl Into<ConvOptions<2>>,
183) -> Tensor<4> {
184 let options = options.into();
185 check!(TensorCheck::conv(
186 "conv2d",
187 x.dims(),
188 weight.dims(),
189 options.groups,
190 ));
191
192 Tensor::new(BridgeTensor::float(Dispatch::conv2d(
193 x.primitive.into_float(),
194 weight.primitive.into_float(),
195 bias.map(|b| b.primitive.into_float()),
196 options,
197 )))
198}
199
200pub fn conv3d(
206 x: Tensor<5>,
207 weight: Tensor<5>,
208 bias: Option<Tensor<1>>,
209 options: impl Into<ConvOptions<3>>,
210) -> Tensor<5> {
211 let options = options.into();
212 check!(TensorCheck::conv(
213 "conv3d",
214 x.dims(),
215 weight.dims(),
216 options.groups,
217 ));
218
219 if options.is_asymmetric() {
220 panic!("Asymmetric padding is not yet supported for conv3d");
221 }
222
223 Tensor::new(BridgeTensor::float(Dispatch::conv3d(
224 x.primitive.into_float(),
225 weight.primitive.into_float(),
226 bias.map(|b| b.primitive.into_float()),
227 options,
228 )))
229}
230
231pub fn deform_conv2d(
233 x: Tensor<4>,
234 offset: Tensor<4>,
235 weight: Tensor<4>,
236 mask: Option<Tensor<4>>,
237 bias: Option<Tensor<1>>,
238 options: DeformConvOptions<2>,
239) -> Tensor<4> {
240 check!(TensorCheck::conv(
241 "deform_conv2d",
242 x.dims(),
243 weight.dims(),
244 options.weight_groups,
245 ));
246 Tensor::new(BridgeTensor::float(Dispatch::deform_conv2d(
247 x.primitive.into_float(),
248 offset.primitive.into_float(),
249 weight.primitive.into_float(),
250 mask.map(|m| m.primitive.into_float()),
251 bias.map(|b| b.primitive.into_float()),
252 options,
253 )))
254}
255
256pub fn conv_transpose1d(
258 x: Tensor<3>,
259 weight: Tensor<3>,
260 bias: Option<Tensor<1>>,
261 options: ConvTransposeOptions<1>,
262) -> Tensor<3> {
263 check!(TensorCheck::conv_transpose(
264 "conv_transpose1d",
265 x.dims(),
266 weight.dims(),
267 ));
268 Tensor::new(BridgeTensor::float(Dispatch::conv_transpose1d(
269 x.primitive.into_float(),
270 weight.primitive.into_float(),
271 bias.map(|b| b.primitive.into_float()),
272 options,
273 )))
274}
275
276pub fn conv_transpose2d(
278 x: Tensor<4>,
279 weight: Tensor<4>,
280 bias: Option<Tensor<1>>,
281 options: ConvTransposeOptions<2>,
282) -> Tensor<4> {
283 check!(TensorCheck::conv_transpose(
284 "conv_transpose2d",
285 x.dims(),
286 weight.dims(),
287 ));
288 Tensor::new(BridgeTensor::float(Dispatch::conv_transpose2d(
289 x.primitive.into_float(),
290 weight.primitive.into_float(),
291 bias.map(|b| b.primitive.into_float()),
292 options,
293 )))
294}
295
296pub fn conv_transpose3d(
298 x: Tensor<5>,
299 weight: Tensor<5>,
300 bias: Option<Tensor<1>>,
301 options: ConvTransposeOptions<3>,
302) -> Tensor<5> {
303 check!(TensorCheck::conv_transpose(
304 "conv_transpose3d",
305 x.dims(),
306 weight.dims(),
307 ));
308 Tensor::new(BridgeTensor::float(Dispatch::conv_transpose3d(
309 x.primitive.into_float(),
310 weight.primitive.into_float(),
311 bias.map(|b| b.primitive.into_float()),
312 options,
313 )))
314}
315
316pub fn unfold4d(x: Tensor<4>, kernel_size: [usize; 2], options: UnfoldOptions) -> Tensor<3> {
318 Tensor::new(BridgeTensor::float(Dispatch::unfold4d(
319 x.primitive.into_float(),
320 kernel_size,
321 options,
322 )))
323}
324
325pub fn fold4d(
344 x: Tensor<3>,
345 output_size: [usize; 2],
346 kernel_size: [usize; 2],
347 options: UnfoldOptions,
348) -> Tensor<4> {
349 Tensor::new(BridgeTensor::float(Dispatch::fold4d(
350 x.primitive.into_float(),
351 output_size,
352 kernel_size,
353 options,
354 )))
355}
356
357pub fn max_pool1d(x: Tensor<3>, options: MaxPoolOptions<1>) -> Tensor<3> {
361 let dims = x.dims();
362 let (x, padding) = pad_max_pool_input(x, &options);
363 let output = Tensor::new(BridgeTensor::float(Dispatch::max_pool1d(
364 x.primitive.into_float(),
365 options.kernel_size[0],
366 options.stride[0],
367 padding[0],
368 options.dilation[0],
369 options.ceil_mode,
370 )));
371
372 drop_end_padding_windows(
373 output,
374 dims,
375 options.stride,
376 options.padding,
377 options.ceil_mode,
378 )
379}
380
381pub fn max_pool2d(x: Tensor<4>, options: MaxPoolOptions<2>) -> Tensor<4> {
385 let dims = x.dims();
386 let (x, padding) = pad_max_pool_input(x, &options);
387 let output = Tensor::new(BridgeTensor::float(Dispatch::max_pool2d(
388 x.primitive.into_float(),
389 options.kernel_size,
390 options.stride,
391 padding,
392 options.dilation,
393 options.ceil_mode,
394 )));
395
396 drop_end_padding_windows(
397 output,
398 dims,
399 options.stride,
400 options.padding,
401 options.ceil_mode,
402 )
403}
404
405pub fn avg_pool2d(x: Tensor<4>, options: AvgPoolOptions<2>) -> Tensor<4> {
409 avg_pool(x, &options, |x, padding, count_include_pad| {
410 Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d(
411 x.primitive.into_float(),
412 options.kernel_size,
413 options.stride,
414 padding,
415 count_include_pad,
416 options.ceil_mode,
417 )))
418 })
419}
420
421pub fn avg_pool1d(x: Tensor<3>, options: AvgPoolOptions<1>) -> Tensor<3> {
425 avg_pool(x, &options, |x, padding, count_include_pad| {
426 Tensor::new(BridgeTensor::float(Dispatch::avg_pool1d(
427 x.primitive.into_float(),
428 options.kernel_size[0],
429 options.stride[0],
430 padding[0],
431 count_include_pad,
432 options.ceil_mode,
433 )))
434 })
435}
436
437pub fn max_pool1d_with_indices(
442 x: Tensor<3>,
443 options: MaxPoolOptions<1>,
444) -> (Tensor<3>, Tensor<3, Int>) {
445 let dims = x.dims();
446 let [_, _, length] = dims;
447 let indices_dtype = x.device().get_or_init_settings().int_dtype;
448 let (x, padding) = pad_max_pool_input(x, &options);
449 let output = Dispatch::max_pool1d_with_indices(
450 x.primitive.into_float(),
451 options.kernel_size[0],
452 options.stride[0],
453 padding[0],
454 options.dilation[0],
455 options.ceil_mode,
456 indices_dtype,
457 );
458 let mut indices = Tensor::<3, Int>::new(BridgeTensor::int(output.indices));
459
460 if options.is_asymmetric() {
461 let (left, _) = options.padding[0];
462 indices = unpad_indices(indices, left, length);
463 }
464
465 let output = Tensor::new(BridgeTensor::float(output.output));
466 let MaxPoolOptions {
467 stride,
468 padding,
469 ceil_mode,
470 ..
471 } = options;
472 (
473 drop_end_padding_windows(output, dims, stride, padding, ceil_mode),
474 drop_end_padding_windows(indices, dims, stride, padding, ceil_mode),
475 )
476}
477
478pub fn max_pool2d_with_indices(
483 x: Tensor<4>,
484 options: MaxPoolOptions<2>,
485) -> (Tensor<4>, Tensor<4, Int>) {
486 let dims = x.dims();
487 let [_, _, height, width] = dims;
488 let indices_dtype = x.device().get_or_init_settings().int_dtype;
489 let (x, padding) = pad_max_pool_input(x, &options);
490 let output = Dispatch::max_pool2d_with_indices(
491 x.primitive.into_float(),
492 options.kernel_size,
493 options.stride,
494 padding,
495 options.dilation,
496 options.ceil_mode,
497 indices_dtype,
498 );
499 let mut indices = Tensor::<4, Int>::new(BridgeTensor::int(output.indices));
500
501 if options.is_asymmetric() {
502 let [(top, _), (left, right)] = options.padding;
503 let width_padded = width + left + right;
504 let rows = unpad_indices(indices.clone().div_scalar(width_padded as i64), top, height);
505 let cols = unpad_indices(indices.remainder_scalar(width_padded as i64), left, width);
506 indices = rows.mul_scalar(width as i64).add(cols);
507 }
508
509 let output = Tensor::new(BridgeTensor::float(output.output));
510 let MaxPoolOptions {
511 stride,
512 padding,
513 ceil_mode,
514 ..
515 } = options;
516 (
517 drop_end_padding_windows(output, dims, stride, padding, ceil_mode),
518 drop_end_padding_windows(indices, dims, stride, padding, ceil_mode),
519 )
520}
521
522fn pad_max_pool_input<const D: usize, const N: usize>(
527 x: Tensor<D>,
528 options: &MaxPoolOptions<N>,
529) -> (Tensor<D>, [usize; N]) {
530 if options.is_asymmetric() {
531 (
532 x.pad(options.padding, PadMode::Constant(f32::NEG_INFINITY)),
533 [0; N],
534 )
535 } else {
536 (x, options.padding.map(|(begin, _)| begin))
537 }
538}
539
540fn unpad_indices<const D: usize>(
545 indices: Tensor<D, Int>,
546 begin: usize,
547 size: usize,
548) -> Tensor<D, Int> {
549 indices.sub_scalar(begin as i64).clamp(0, size as i64 - 1)
550}
551
552fn avg_pool<const D: usize, const N: usize>(
557 x: Tensor<D>,
558 options: &AvgPoolOptions<N>,
559 pool: impl Fn(Tensor<D>, [usize; N], bool) -> Tensor<D>,
560) -> Tensor<D> {
561 if !options.is_asymmetric() {
562 let padding = options.padding.map(|(begin, _)| begin);
563 return pool(x, padding, options.count_include_pad);
564 }
565
566 let dims = x.dims();
567 let valid = (!options.count_include_pad).then(|| {
568 let mut shape = [1; D];
570 shape[D - N..].copy_from_slice(&dims[D - N..]);
571 Tensor::<D>::ones(shape, (&x.device(), x.dtype()))
572 .pad(options.padding, PadMode::Constant(0.0))
573 });
574 let output = pool(
575 x.pad(options.padding, PadMode::Constant(0.0)),
576 [0; N],
577 options.count_include_pad,
578 );
579
580 let output = match valid {
581 Some(valid) => {
585 let valid = pool(valid, [0; N], false);
586 let empty = valid.clone().equal_elem(0.0);
587 output / valid.mask_fill(empty, 1.0)
588 }
589 None => output,
590 };
591
592 drop_end_padding_windows(
593 output,
594 dims,
595 options.stride,
596 options.padding,
597 options.ceil_mode,
598 )
599}
600
601fn drop_end_padding_windows<const D: usize, const N: usize, K: Basic>(
613 output: Tensor<D, K>,
614 input_dims: [usize; D],
615 stride: [usize; N],
616 padding: [(usize, usize); N],
617 ceil_mode: bool,
618) -> Tensor<D, K> {
619 if !ceil_mode {
620 return output;
621 }
622
623 (0..N).fold(output, |output, i| {
624 let dim = D - N + i;
625 let size = output.dims()[dim];
626 let (begin, _) = padding[i];
627 if size > 1 && (size - 1) * stride[i] >= input_dims[dim] + begin {
628 output.narrow(dim, 0, size - 1)
629 } else {
630 output
631 }
632 })
633}
634
635pub fn adaptive_avg_pool2d(x: Tensor<4>, output_size: [usize; 2]) -> Tensor<4> {
637 Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool2d(
638 x.primitive.into_float(),
639 output_size,
640 )))
641}
642
643pub fn adaptive_avg_pool3d(x: Tensor<5>, output_size: [usize; 3]) -> Tensor<5> {
645 Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool3d(
646 x.primitive.into_float(),
647 output_size,
648 )))
649}
650
651pub fn adaptive_avg_pool1d(x: Tensor<3>, output_size: usize) -> Tensor<3> {
653 Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool1d(
654 x.primitive.into_float(),
655 output_size,
656 )))
657}
658
659pub fn interpolate(x: Tensor<4>, options: InterpolateOptions) -> Tensor<4> {
678 let [_, _, h, w] = x.dims();
679 let output_size = interpolate_output_size([h, w], &options);
680 Tensor::new(BridgeTensor::float(Dispatch::interpolate(
681 x.primitive.into_float(),
682 output_size,
683 options,
684 )))
685}
686
687fn interpolate_output_size(input_size: [usize; 2], options: &InterpolateOptions) -> [usize; 2] {
688 match (options.output_size, options.scale_factor) {
689 (Some(output_size), None) => output_size,
690 (None, Some(scale_factor)) => core::array::from_fn(|i| {
691 let size = input_size[i] as f64 * scale_factor[i] as f64;
692 assert!(
693 size <= usize::MAX as f64,
694 "Interpolate scale factor {} is too large for input size {}",
695 scale_factor[i],
696 input_size[i]
697 );
698 size as usize
699 }),
700 (Some(_), Some(_)) => {
701 panic!("Interpolate options must set only one of output_size or scale_factor")
702 }
703 (None, None) => panic!("Interpolate options must set output_size or scale_factor"),
704 }
705}
706
707pub fn linear<const D: usize>(
733 input: Tensor<D>,
734 weight: Tensor<2>,
735 bias: Option<Tensor<1>>,
736) -> Tensor<D> {
737 if D == 1 {
738 let input = input.unsqueeze::<2>();
740 let output = linear(input, weight, bias);
741 return output.squeeze_dim(0);
742 }
743
744 if let DType::QFloat(_) = weight.dtype() {
752 let dims = input.dims();
753 let analysis = MatmulTransformAnalysis::from_shapes(&input.shape(), &weight.shape());
754
755 let output = match MatmulTransformPolicy::default().action(&analysis) {
756 MatmulTransformAction::MergeBatches { rows } => {
757 let d_in = dims[D - 1];
758 let d_out = weight.dims()[1];
759
760 let folded = input.reshape([rows, d_in]).matmul(weight);
761
762 let mut out_dims = dims;
763 out_dims[D - 1] = d_out;
764 folded.reshape(out_dims)
765 }
766 MatmulTransformAction::Keep => input.matmul(weight.unsqueeze::<D>()),
767 };
768
769 return match bias {
770 Some(bias) => output + bias.unsqueeze(),
771 None => output,
772 };
773 }
774
775 Tensor::new(linear_impl(
776 input.primitive,
777 weight.primitive,
778 bias.map(|b| b.primitive),
779 ))
780}
781
782fn linear_impl(
783 input: BridgeTensor,
784 weight: BridgeTensor,
785 bias: Option<BridgeTensor>,
786) -> BridgeTensor {
787 BridgeTensor::float(Dispatch::linear(
788 input.into_float(),
789 weight.into_float(),
790 bias.map(|b| b.into_float()),
791 ))
792}
793
794pub fn attention(
816 query: Tensor<4>,
817 key: Tensor<4>,
818 value: Tensor<4>,
819 mask: Option<Tensor<4, Bool>>,
820 attn_bias: Option<Tensor<4>>,
821 options: AttentionModuleOptions,
822) -> Tensor<4> {
823 Tensor::new(BridgeTensor::float(Dispatch::attention(
824 query.primitive.into_float(),
825 key.primitive.into_float(),
826 value.primitive.into_float(),
827 mask.map(|mask| mask.primitive.into()),
828 attn_bias.map(|bias| bias.primitive.into_float()),
829 options,
830 )))
831}
832
833pub fn attention_fallback(
835 query: Tensor<4>,
836 key: Tensor<4>,
837 value: Tensor<4>,
838 mask: Option<Tensor<4, Bool>>,
839 attn_bias: Option<Tensor<4>>,
840 options: AttentionModuleOptions,
841) -> Tensor<4> {
842 Tensor::new(BridgeTensor::float(
843 burn_backend::ops::attention::attention_fallback::<Dispatch>(
844 query.primitive.into_float(),
845 key.primitive.into_float(),
846 value.primitive.into_float(),
847 mask.map(|mask| mask.primitive.into()),
848 attn_bias.map(|bias| bias.primitive.into_float()),
849 options,
850 ),
851 ))
852}
853
854pub fn conv2d_weight_backward(
856 x: Tensor<4>,
857 weight: Tensor<4>,
858 output_grad: Tensor<4>,
859 options: ConvOptions<2>,
860) -> Tensor<4> {
861 Tensor::new(BridgeTensor::float(Dispatch::conv2d_weight_backward(
862 x.primitive.into_float(),
863 weight.primitive.into_float(),
864 output_grad.primitive.into_float(),
865 options,
866 )))
867}
868
869pub fn avg_pool2d_backward(
871 x: Tensor<4>,
872 grad: Tensor<4>,
873 kernel_size: [usize; 2],
874 stride: [usize; 2],
875 padding: [usize; 2],
876 count_include_pad: bool,
877 ceil_mode: bool,
878) -> Tensor<4> {
879 Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d_backward(
880 x.primitive.into_float(),
881 grad.primitive.into_float(),
882 kernel_size,
883 stride,
884 padding,
885 count_include_pad,
886 ceil_mode,
887 )))
888}
889
890#[allow(clippy::too_many_arguments)]
892pub fn max_pool2d_with_indices_backward(
893 x: Tensor<4>,
894 kernel_size: [usize; 2],
895 stride: [usize; 2],
896 padding: [usize; 2],
897 dilation: [usize; 2],
898 ceil_mode: bool,
899 output_grad: Tensor<4>,
900 indices: Tensor<4, Int>,
901) -> Tensor<4> {
902 Tensor::new(BridgeTensor::float(
903 Dispatch::max_pool2d_with_indices_backward(
904 x.primitive.into_float(),
905 kernel_size,
906 stride,
907 padding,
908 dilation,
909 ceil_mode,
910 output_grad.primitive.into_float(),
911 indices.primitive.into(),
912 )
913 .x_grad,
914 ))
915}
916
917pub fn layer_norm<const D: usize>(
927 input: Tensor<D>,
928 gamma: Tensor<1>,
929 beta: Option<Tensor<1>>,
930 epsilon: f64,
931) -> Tensor<D> {
932 Tensor::new(layer_norm_impl(
933 input.primitive,
934 gamma.primitive,
935 beta.map(|b| b.primitive),
936 epsilon,
937 ))
938}
939
940fn layer_norm_impl(
941 input: BridgeTensor,
942 gamma: BridgeTensor,
943 beta: Option<BridgeTensor>,
944 epsilon: f64,
945) -> BridgeTensor {
946 BridgeTensor::float(Dispatch::layer_norm(
947 input.into_float(),
948 gamma.into_float(),
949 beta.map(|b| b.into_float()),
950 epsilon,
951 ))
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957 use crate::ops::InterpolateMode;
958
959 fn options() -> InterpolateOptions {
960 InterpolateOptions::new(InterpolateMode::Nearest)
961 }
962
963 #[test]
964 fn interpolate_output_size_from_output_size() {
965 let size = interpolate_output_size([4, 4], &options().with_output_size([2, 3]));
966 assert_eq!(size, [2, 3]);
967 }
968
969 #[test]
970 fn interpolate_output_size_from_scale_factor_floors() {
971 let size = interpolate_output_size([4, 5], &options().with_scale_factor([2.0, 1.5]));
972 assert_eq!(size, [8, 7]);
973 }
974
975 #[test]
976 fn interpolate_options_last_sizing_builder_wins() {
977 let size = interpolate_output_size(
978 [4, 4],
979 &options()
980 .with_output_size([2, 2])
981 .with_scale_factor([2.0, 2.0]),
982 );
983 assert_eq!(size, [8, 8]);
984 }
985
986 #[test]
987 #[should_panic(expected = "must set output_size or scale_factor")]
988 fn interpolate_output_size_requires_size() {
989 interpolate_output_size([4, 4], &options());
990 }
991
992 #[test]
993 #[should_panic(expected = "only one of output_size or scale_factor")]
994 fn interpolate_output_size_rejects_both() {
995 let mut options = options().with_output_size([2, 2]);
996 options.scale_factor = Some([2.0, 2.0]);
997 interpolate_output_size([4, 4], &options);
998 }
999
1000 #[test]
1001 #[should_panic(expected = "too large")]
1002 fn interpolate_output_size_rejects_overflow() {
1003 interpolate_output_size(
1004 [4, usize::MAX - 1],
1005 &options().with_scale_factor([1.0, 2.0]),
1006 );
1007 }
1008}