1use smallvec::{SmallVec, smallvec};
11
12use crate::shape::{Shape, Strides};
13use crate::view::{self, DimSlice};
14use crate::{Array, DType, Error, Result};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum UnaryOp {
20 Negative,
22 Abs,
24 Exp,
26 Log,
28 Sqrt,
30 Sin,
32 Cos,
34 Tanh,
36 Sigmoid,
38}
39
40impl UnaryOp {
41 pub(crate) fn is_transcendental(self) -> bool {
43 !matches!(self, Self::Negative | Self::Abs)
44 }
45
46 pub(crate) fn name(self) -> &'static str {
47 match self {
48 Self::Negative => "negative",
49 Self::Abs => "abs",
50 Self::Exp => "exp",
51 Self::Log => "log",
52 Self::Sqrt => "sqrt",
53 Self::Sin => "sin",
54 Self::Cos => "cos",
55 Self::Tanh => "tanh",
56 Self::Sigmoid => "sigmoid",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63#[non_exhaustive]
64pub enum BinaryOp {
65 Add,
67 Subtract,
69 Multiply,
71 Divide,
73 Maximum,
75 Minimum,
77 Equal,
79 NotEqual,
81 Less,
83 LessEqual,
85 Greater,
87 GreaterEqual,
89}
90
91impl BinaryOp {
92 pub(crate) fn is_comparison(self) -> bool {
94 matches!(
95 self,
96 Self::Equal
97 | Self::NotEqual
98 | Self::Less
99 | Self::LessEqual
100 | Self::Greater
101 | Self::GreaterEqual
102 )
103 }
104
105 pub(crate) fn name(self) -> &'static str {
106 match self {
107 Self::Add => "add",
108 Self::Subtract => "subtract",
109 Self::Multiply => "multiply",
110 Self::Divide => "divide",
111 Self::Maximum => "maximum",
112 Self::Minimum => "minimum",
113 Self::Equal => "equal",
114 Self::NotEqual => "not_equal",
115 Self::Less => "less",
116 Self::LessEqual => "less_equal",
117 Self::Greater => "greater",
118 Self::GreaterEqual => "greater_equal",
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125#[non_exhaustive]
126pub enum ReduceOp {
127 Sum,
129 Prod,
131 Max,
133 Min,
135 And,
137 Or,
139}
140
141impl ReduceOp {
142 pub(crate) fn name(self) -> &'static str {
143 match self {
144 Self::Sum => "sum",
145 Self::Prod => "prod",
146 Self::Max => "max",
147 Self::Min => "min",
148 Self::And => "all",
149 Self::Or => "any",
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156#[non_exhaustive]
157pub enum ArgReduceOp {
158 Max,
160 Min,
162}
163
164impl ArgReduceOp {
165 pub(crate) fn name(self) -> &'static str {
166 match self {
167 Self::Max => "argmax",
168 Self::Min => "argmin",
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175#[non_exhaustive]
176pub enum Primitive {
177 Unary(UnaryOp),
179 Binary(BinaryOp),
181 Select,
183 AsType(DType),
185 Reshape(Vec<usize>),
187 Transpose(Vec<usize>),
189 Slice {
191 start: Vec<usize>,
193 step: Vec<isize>,
195 len: Vec<usize>,
197 },
198 Broadcast(Vec<usize>),
200 Reduce {
202 op: ReduceOp,
204 axes: Vec<usize>,
206 },
207 ArgReduce {
209 op: ArgReduceOp,
211 axis: usize,
213 },
214 Matmul,
217 #[cfg(test)]
219 AddMultiply,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub(crate) struct OutputSpec {
225 pub(crate) dtype: DType,
226 pub(crate) shape: Shape,
227 pub(crate) strides: Option<Strides>,
229}
230
231impl Primitive {
232 pub fn name(&self) -> &'static str {
234 match self {
235 Self::Unary(op) => op.name(),
236 Self::Binary(op) => op.name(),
237 Self::Select => "where",
238 Self::AsType(_) => "astype",
239 Self::Reshape(_) => "reshape",
240 Self::Transpose(_) => "transpose",
241 Self::Slice { .. } => "slice",
242 Self::Broadcast(_) => "broadcast_to",
243 Self::Reduce { op, .. } => op.name(),
244 Self::ArgReduce { op, .. } => op.name(),
245 Self::Matmul => "matmul",
246 #[cfg(test)]
247 Self::AddMultiply => "add_multiply",
248 }
249 }
250
251 pub(crate) fn output_specs(&self, inputs: &[Array]) -> Result<SmallVec<[OutputSpec; 1]>> {
253 let computed = |dtype: DType, shape: &[usize]| OutputSpec {
254 dtype,
255 shape: shape.into(),
256 strides: None,
257 };
258 let spec = match self {
259 Self::Unary(op) => {
260 let [a] = self.arity::<1>(inputs)?;
261 let dtype = match (op, a.dtype()) {
262 (UnaryOp::Abs, DType::Complex64) => DType::F32,
263 (_, d) => d,
264 };
265 computed(dtype, a.shape())
266 }
267 Self::Binary(op) => {
268 let [a, b] = self.arity::<2>(inputs)?;
269 self.same_shape_and_dtype(a, b)?;
270 computed(
271 if op.is_comparison() {
272 DType::Bool
273 } else {
274 a.dtype()
275 },
276 a.shape(),
277 )
278 }
279 Self::Select => {
280 let [c, a, b] = self.arity::<3>(inputs)?;
281 self.same_shape_and_dtype(a, b)?;
282 if c.dtype() != DType::Bool || c.shape() != a.shape() {
283 return Err(Error::InvalidArgument(
284 "where: condition must be a bool array of the same shape".into(),
285 ));
286 }
287 computed(a.dtype(), a.shape())
288 }
289 Self::AsType(dtype) => {
290 let [a] = self.arity::<1>(inputs)?;
291 computed(*dtype, a.shape())
292 }
293 Self::Reshape(shape) => {
294 let [a] = self.arity::<1>(inputs)?;
295 OutputSpec {
296 dtype: a.dtype(),
297 shape: shape.as_slice().into(),
298 strides: view::nocopy_reshape(a.shape(), a.strides(), shape),
299 }
300 }
301 Self::Transpose(axes) => {
302 let [a] = self.arity::<1>(inputs)?;
303 let (shape, strides) = view::permute(a.shape(), a.strides(), axes)?;
304 OutputSpec {
305 dtype: a.dtype(),
306 shape,
307 strides: Some(strides),
308 }
309 }
310 Self::Slice { len, .. } => {
311 let [a] = self.arity::<1>(inputs)?;
312 let (strides, _) = view::slice_geometry(a.strides(), &self.dim_slices(a.ndim())?);
313 OutputSpec {
314 dtype: a.dtype(),
315 shape: len.as_slice().into(),
316 strides: Some(strides),
317 }
318 }
319 Self::Broadcast(shape) => {
320 let [a] = self.arity::<1>(inputs)?;
321 let strides = view::broadcast_strides(a.shape(), a.strides(), shape)?;
322 OutputSpec {
323 dtype: a.dtype(),
324 shape: shape.as_slice().into(),
325 strides: Some(strides),
326 }
327 }
328 Self::Reduce { axes, .. } => {
329 let [a] = self.arity::<1>(inputs)?;
330 computed(a.dtype(), &self.reduced_shape(a.shape(), axes)?)
331 }
332 Self::ArgReduce { axis, .. } => {
333 let [a] = self.arity::<1>(inputs)?;
334 computed(DType::U32, &self.reduced_shape(a.shape(), &[*axis])?)
335 }
336 Self::Matmul => {
337 let [a, b] = self.arity::<2>(inputs)?;
338 let (sa, sb) = (a.shape(), b.shape());
339 let n = sa.len();
340 let valid =
341 n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
342 if !valid {
343 return Err(Error::ShapeMismatch {
344 op: "matmul",
345 lhs: sa.to_vec(),
346 rhs: sb.to_vec(),
347 });
348 }
349 if a.dtype() != b.dtype() {
350 return Err(Error::DTypeMismatch {
351 op: "matmul",
352 lhs: a.dtype(),
353 rhs: b.dtype(),
354 });
355 }
356 let mut shape: Shape = sa.into();
357 shape[n - 1] = sb[n - 1];
358 computed(a.dtype(), &shape)
359 }
360 #[cfg(test)]
361 Self::AddMultiply => {
362 let [a, b] = self.arity::<2>(inputs)?;
363 self.same_shape_and_dtype(a, b)?;
364 let spec = computed(a.dtype(), a.shape());
365 return Ok(smallvec![spec.clone(), spec]);
366 }
367 };
368 Ok(smallvec![spec])
369 }
370
371 pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
373 match self {
374 Self::Slice { start, step, len }
375 if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
376 {
377 Ok((0..ndim)
378 .map(|i| DimSlice {
379 start: start[i],
380 step: step[i],
381 len: len[i],
382 })
383 .collect())
384 }
385 _ => Err(Error::InvalidArgument(
386 "slice: one start, step and length per dimension".into(),
387 )),
388 }
389 }
390
391 fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
393 let mut out: Shape = shape.into();
394 for &axis in axes {
395 *out.get_mut(axis).ok_or_else(|| {
396 Error::InvalidArgument(format!(
397 "{}: axis {axis} out of range for shape {shape:?}",
398 self.name()
399 ))
400 })? = 1;
401 }
402 Ok(out)
403 }
404
405 fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
406 inputs.try_into().map_err(|_| {
407 Error::InvalidArgument(format!(
408 "{} takes {N} inputs, got {}",
409 self.name(),
410 inputs.len()
411 ))
412 })
413 }
414
415 fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
416 if a.shape() != b.shape() {
417 return Err(Error::ShapeMismatch {
418 op: self.name(),
419 lhs: a.shape().to_vec(),
420 rhs: b.shape().to_vec(),
421 });
422 }
423 if a.dtype() != b.dtype() {
424 return Err(Error::DTypeMismatch {
425 op: self.name(),
426 lhs: a.dtype(),
427 rhs: b.dtype(),
428 });
429 }
430 Ok(())
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use std::collections::hash_map::DefaultHasher;
437 use std::hash::{Hash, Hasher};
438
439 use super::*;
440
441 fn hash(p: &Primitive) -> u64 {
442 let mut h = DefaultHasher::new();
443 p.hash(&mut h);
444 h.finish()
445 }
446
447 #[test]
448 fn equal_primitives_compare_and_hash_equal() {
449 let add = Primitive::Binary(BinaryOp::Add);
450 assert_eq!(add, Primitive::Binary(BinaryOp::Add));
451 assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
452 assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
453 let s = |start: Vec<usize>| Primitive::Slice {
454 start,
455 step: vec![1],
456 len: vec![2],
457 };
458 assert_eq!(s(vec![1]), s(vec![1]));
459 assert_ne!(s(vec![1]), s(vec![2]));
460 }
461}