1use crate::infer::GraphExt as _;
28use crate::ops::upsample::InterpMode;
29use crate::{Graph, NodeId, Op};
30
31impl Graph {
32 pub fn conv3d(
40 &mut self,
41 input: NodeId,
42 weight: NodeId,
43 stride: [usize; 3],
44 padding: [usize; 3],
45 dilation: [usize; 3],
46 groups: usize,
47 ) -> NodeId {
48 let in_s = self.node(input).shape.clone();
49 let w_s = self.node(weight).shape.clone();
50 assert_eq!(in_s.rank(), 5, "conv3d: input must be [N, C_in, D, H, W]");
51 assert_eq!(
52 w_s.rank(),
53 5,
54 "conv3d: weight must be [C_out, C_in/groups, kD, kH, kW]"
55 );
56 let ks = [
57 w_s.dim(2).unwrap_static(),
58 w_s.dim(3).unwrap_static(),
59 w_s.dim(4).unwrap_static(),
60 ];
61 let out =
62 crate::shape::conv3d_output_shape(&in_s, &w_s, ks, stride, padding, dilation, groups)
63 .expect("conv3d shape inference");
64 self.push(
65 Op::Conv3d {
66 stride,
67 padding,
68 dilation,
69 groups,
70 },
71 vec![input, weight],
72 out,
73 None,
74 )
75 }
76
77 #[allow(clippy::too_many_arguments)]
86 pub fn conv_transpose3d(
87 &mut self,
88 input: NodeId,
89 weight: NodeId,
90 stride: [usize; 3],
91 padding: [usize; 3],
92 dilation: [usize; 3],
93 output_padding: [usize; 3],
94 groups: usize,
95 ) -> NodeId {
96 let in_s = self.node(input).shape.clone();
97 let w_s = self.node(weight).shape.clone();
98 assert_eq!(
99 in_s.rank(),
100 5,
101 "conv_transpose3d: input must be [N, C_in, D, H, W]"
102 );
103 assert_eq!(
104 w_s.rank(),
105 5,
106 "conv_transpose3d: weight must be [C_in, C_out/groups, kD, kH, kW]"
107 );
108 let ks = [
109 w_s.dim(2).unwrap_static(),
110 w_s.dim(3).unwrap_static(),
111 w_s.dim(4).unwrap_static(),
112 ];
113 let out = crate::shape::conv_transpose3d_output_shape(
114 &in_s,
115 &w_s,
116 ks,
117 stride,
118 padding,
119 dilation,
120 output_padding,
121 groups,
122 )
123 .expect("conv_transpose3d shape inference");
124 self.push(
125 Op::ConvTranspose3d {
126 stride,
127 padding,
128 dilation,
129 output_padding,
130 groups,
131 },
132 vec![input, weight],
133 out,
134 None,
135 )
136 }
137
138 pub fn interpolate3d(
151 &mut self,
152 x: NodeId,
153 out_dhw: [usize; 3],
154 mode: InterpMode,
155 align_corners: bool,
156 ) -> NodeId {
157 let xs = self.shape(x).clone();
158 assert_eq!(xs.rank(), 5, "interpolate3d: input must be [N, C, D, H, W]");
159 assert!(
160 out_dhw.iter().all(|&l| l > 0),
161 "interpolate3d: out_dhw must be positive"
162 );
163 let mut y = x;
166 for (i, &out_len) in out_dhw.iter().enumerate() {
167 y = self.resample_axis(y, 2 + i, out_len, mode, align_corners);
168 }
169 y
170 }
171
172 fn resample_axis(
175 &mut self,
176 x: NodeId,
177 axis: usize,
178 out_len: usize,
179 mode: InterpMode,
180 align_corners: bool,
181 ) -> NodeId {
182 let xs = self.shape(x).clone();
183 let rank = xs.rank();
184 let in_len = xs.dim(axis).unwrap_static();
185 if in_len == out_len {
186 return x;
187 }
188 let mut perm: Vec<usize> = (0..rank).filter(|&d| d != axis).collect();
190 perm.push(axis);
191 let xp = self.transpose_(x, perm.clone());
192 let ps = self.shape(xp).clone();
193 let batch: usize = (0..rank - 1).map(|d| ps.dim(d).unwrap_static()).product();
194
195 let w = interp_matrix(in_len, out_len, mode, align_corners);
196 let w_node = self.const_f32_tensor(w, &[in_len, out_len]);
197 let x2 = self.reshape_(xp, vec![batch as i64, in_len as i64]);
198 let y2 = self.mm(x2, w_node); let mut out_perm_dims: Vec<i64> = (0..rank - 1)
201 .map(|d| ps.dim(d).unwrap_static() as i64)
202 .collect();
203 out_perm_dims.push(out_len as i64);
204 let yp = self.reshape_(y2, out_perm_dims);
205
206 let mut inv = vec![0usize; rank];
208 for (new_pos, &old_axis) in perm.iter().enumerate() {
209 inv[old_axis] = new_pos;
210 }
211 self.transpose_(yp, inv)
212 }
213}
214
215fn interp_matrix(l_in: usize, l_out: usize, mode: InterpMode, align_corners: bool) -> Vec<f32> {
218 let mut w = vec![0f32; l_in * l_out];
219 for j in 0..l_out {
220 let pos = if align_corners {
222 if l_out == 1 {
223 0.0
224 } else {
225 j as f32 * (l_in as f32 - 1.0) / (l_out as f32 - 1.0)
226 }
227 } else {
228 let p = (j as f32 + 0.5) * (l_in as f32) / (l_out as f32) - 0.5;
231 p.clamp(0.0, l_in as f32 - 1.0)
232 };
233 match mode {
234 InterpMode::Nearest => {
235 let i = (pos + 0.5).floor() as usize;
236 let i = i.min(l_in - 1);
237 w[i * l_out + j] = 1.0;
238 }
239 InterpMode::Linear => {
240 let i0 = pos.floor() as usize;
241 let i0 = i0.min(l_in - 1);
242 let i1 = (i0 + 1).min(l_in - 1);
243 let frac = pos - i0 as f32;
244 w[i0 * l_out + j] += 1.0 - frac;
245 w[i1 * l_out + j] += frac;
246 }
247 }
248 }
249 w
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::{DType, Shape};
256
257 fn dims(g: &Graph, id: NodeId) -> Vec<usize> {
258 g.shape(id)
259 .dims()
260 .iter()
261 .map(|d| d.unwrap_static())
262 .collect()
263 }
264
265 #[test]
266 fn conv3d_output_shape_matches_formula() {
267 let mut g = Graph::new("conv3d");
268 let x = g.input("x", Shape::new(&[1, 2, 8, 8, 8], DType::F32));
269 let w = g.param("w", Shape::new(&[4, 2, 3, 3, 3], DType::F32));
270 let y = g.conv3d(x, w, [1, 1, 1], [1, 1, 1], [1, 1, 1], 1);
271 assert_eq!(dims(&g, y), vec![1, 4, 8, 8, 8]);
273 }
274
275 #[test]
276 fn conv_transpose3d_upsamples_by_stride() {
277 let mut g = Graph::new("ct3d");
278 let x = g.input("x", Shape::new(&[1, 3, 4, 4, 4], DType::F32));
279 let w = g.param("w", Shape::new(&[3, 5, 2, 2, 2], DType::F32));
280 let y = g.conv_transpose3d(x, w, [2, 2, 2], [0, 0, 0], [1, 1, 1], [0, 0, 0], 1);
281 assert_eq!(dims(&g, y), vec![1, 5, 8, 8, 8]);
283 }
284
285 #[test]
286 fn interpolate3d_shape() {
287 let mut g = Graph::new("interp3d");
288 let x = g.input("x", Shape::new(&[1, 2, 2, 3, 4], DType::F32));
289 let y = g.interpolate3d(x, [4, 6, 8], InterpMode::Linear, false);
290 assert_eq!(dims(&g, y), vec![1, 2, 4, 6, 8]);
291 }
292}