1use kopitiam_core::{DType, Error, Result, Shape};
13
14use super::Tensor;
15
16impl Tensor {
17 pub fn conv1d(&self, weight: &Tensor, bias: Option<&Tensor>, stride: usize) -> Result<Tensor> {
37 self.require_dtype(DType::F32)?;
38 weight.require_dtype(DType::F32)?;
39
40 if self.rank() != 2 || weight.rank() != 3 || stride == 0 {
41 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: weight.shape.clone() });
42 }
43 let (in_channels, length) = (self.shape.dims()[0], self.shape.dims()[1]);
44 let wdims = weight.shape.dims();
45 let (out_channels, w_in_channels, kernel) = (wdims[0], wdims[1], wdims[2]);
46 if w_in_channels != in_channels || kernel == 0 || kernel > length {
47 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: weight.shape.clone() });
48 }
49 let out_length = (length - kernel) / stride + 1;
50
51 let input = self.to_vec_f32()?;
55 let w = weight.to_vec_f32()?;
56 let bias_vec = match bias {
57 Some(b) => {
58 b.require_dtype(DType::F32)?;
59 if b.elem_count() != out_channels {
60 return Err(Error::ShapeMismatch { expected: weight.shape.clone(), actual: b.shape.clone() });
61 }
62 b.to_vec_f32()?
63 }
64 None => vec![0.0f32; out_channels],
65 };
66
67 let mut out = vec![0f32; out_channels * out_length];
68 for oc in 0..out_channels {
69 for t in 0..out_length {
70 let start = t * stride;
71 let mut acc = bias_vec[oc];
72 for ic in 0..in_channels {
73 let in_row = &input[ic * length + start..ic * length + start + kernel];
74 let w_row = &w[(oc * in_channels + ic) * kernel..(oc * in_channels + ic) * kernel + kernel];
75 for (x, wv) in in_row.iter().zip(w_row) {
76 acc += x * wv;
77 }
78 }
79 out[oc * out_length + t] = acc;
80 }
81 }
82 Tensor::from_f32(out, Shape::new([out_channels, out_length]))
83 }
84
85 pub fn maxpool1d(&self, kernel: usize, stride: usize) -> Result<Tensor> {
98 self.require_dtype(DType::F32)?;
99 if self.rank() != 2 || stride == 0 || kernel == 0 {
100 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: self.shape.clone() });
101 }
102 let (channels, length) = (self.shape.dims()[0], self.shape.dims()[1]);
103 if kernel > length {
104 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: self.shape.clone() });
105 }
106 let out_length = (length - kernel) / stride + 1;
107
108 let input = self.to_vec_f32()?;
109
110 let mut out = vec![0f32; channels * out_length];
111 for c in 0..channels {
112 for t in 0..out_length {
113 let start = t * stride;
114 let window = &input[c * length + start..c * length + start + kernel];
115 out[c * out_length + t] = window.iter().copied().fold(f32::NEG_INFINITY, f32::max);
116 }
117 }
118 Tensor::from_f32(out, Shape::new([channels, out_length]))
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn conv1d_single_channel_matches_hand_computation() {
128 let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], [1, 5]).unwrap();
134 let w = Tensor::from_f32(vec![1.0, 0.0, -1.0], [1, 1, 3]).unwrap();
135 let out = x.conv1d(&w, None, 1).unwrap();
136 assert_eq!(out.shape().dims(), &[1, 3]);
137 assert_eq!(out.to_vec_f32().unwrap(), vec![-2.0, -2.0, -2.0]);
138 }
139
140 #[test]
141 fn conv1d_applies_bias_and_stride() {
142 let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], [1, 5]).unwrap();
146 let w = Tensor::from_f32(vec![1.0, 1.0], [1, 1, 2]).unwrap();
147 let bias = Tensor::from_f32(vec![10.0], [1]).unwrap();
148 let out = x.conv1d(&w, Some(&bias), 2).unwrap();
149 assert_eq!(out.shape().dims(), &[1, 2]);
150 assert_eq!(out.to_vec_f32().unwrap(), vec![13.0, 17.0]);
151 }
152
153 #[test]
154 fn conv1d_sums_over_input_channels_and_across_output_channels() {
155 let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
162 let w = Tensor::from_f32(
163 vec![
164 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, ],
167 [2, 2, 2],
168 )
169 .unwrap();
170 let out = x.conv1d(&w, None, 1).unwrap();
171 assert_eq!(out.shape().dims(), &[2, 2]);
172 assert_eq!(out.to_vec_f32().unwrap(), vec![6.0, 8.0, 12.0, 16.0]);
173 }
174
175 #[test]
176 fn conv1d_rejects_a_channel_count_mismatch() {
177 let x = Tensor::from_f32(vec![1.0; 5], [1, 5]).unwrap();
178 let w = Tensor::from_f32(vec![1.0; 6], [1, 2, 3]).unwrap(); assert!(matches!(x.conv1d(&w, None, 1), Err(Error::ShapeMismatch { .. })));
180 }
181
182 #[test]
183 fn conv1d_rejects_a_kernel_wider_than_the_input() {
184 let x = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
185 let w = Tensor::from_f32(vec![1.0; 3], [1, 1, 3]).unwrap();
186 assert!(matches!(x.conv1d(&w, None, 1), Err(Error::ShapeMismatch { .. })));
187 }
188
189 #[test]
190 fn conv1d_rejects_non_f32_input() {
191 let x = Tensor::from_i32(vec![1; 5], [1, 5]).unwrap();
192 let w = Tensor::from_f32(vec![1.0; 3], [1, 1, 3]).unwrap();
193 assert!(matches!(x.conv1d(&w, None, 1), Err(Error::DTypeMismatch { .. })));
194 }
195
196 #[test]
197 fn maxpool1d_matches_hand_computation() {
198 let x = Tensor::from_f32(vec![1.0, 3.0, 2.0, 5.0, 4.0, 0.0], [1, 6]).unwrap();
201 let out = x.maxpool1d(2, 2).unwrap();
202 assert_eq!(out.shape().dims(), &[1, 3]);
203 assert_eq!(out.to_vec_f32().unwrap(), vec![3.0, 5.0, 4.0]);
204 }
205
206 #[test]
207 fn maxpool1d_overlapping_windows_per_channel() {
208 let x = Tensor::from_f32(vec![1.0, 3.0, 2.0, 4.0, 9.0, 0.0, 7.0, 7.0], [2, 4]).unwrap();
212 let out = x.maxpool1d(2, 1).unwrap();
213 assert_eq!(out.shape().dims(), &[2, 3]);
214 assert_eq!(out.to_vec_f32().unwrap(), vec![3.0, 3.0, 4.0, 9.0, 7.0, 7.0]);
215 }
216
217 #[test]
218 fn maxpool1d_handles_negative_values() {
219 let x = Tensor::from_f32(vec![-3.0, -1.0, -5.0, -2.0], [1, 4]).unwrap();
221 let out = x.maxpool1d(2, 2).unwrap();
222 assert_eq!(out.to_vec_f32().unwrap(), vec![-1.0, -2.0]);
223 }
224
225 #[test]
226 fn maxpool1d_rejects_a_kernel_wider_than_the_input() {
227 let x = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
228 assert!(matches!(x.maxpool1d(3, 1), Err(Error::ShapeMismatch { .. })));
229 }
230
231 #[test]
232 fn maxpool1d_rejects_non_f32_input() {
233 let x = Tensor::from_i32(vec![1; 4], [1, 4]).unwrap();
234 assert!(matches!(x.maxpool1d(2, 2), Err(Error::DTypeMismatch { .. })));
235 }
236}