diff --git a/src/display.rs b/src/display.rs index 8932894..de1bc02 100644 --- a/src/display.rs +++ b/src/display.rs @@ -3,7 +3,7 @@ //! render the same way, by walking a shape. use std::fmt; -use std::fmt::Write as _; +use std::fmt::{Display, Write as _}; use crate::domain::HRect; use crate::mapping::{BoxedMapping, BoxedPowersetMapping}; @@ -13,41 +13,43 @@ use crate::tensor::{ /// A tensor whose domain is a hrect. Only these can be printed, since formatting /// walks a shape and visits every index of it. -pub trait BoxedTensor: Tensor { +pub trait BoxedTensor: Tensor { fn hrect(&self) -> &HRect; } /// Renders a boxed tensor. Every `Display` implementation below defers to this, /// and each is written for the boxed instantiation of its tensor only. -pub fn format_boxed(tensor: &dyn BoxedTensor) -> String { +pub fn format_boxed(tensor: &dyn BoxedTensor) -> String { format_tensor(tensor.hrect().shape(), &|index| tensor.get(index)) } -impl fmt::Display for Physical { +impl Display for Physical { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&format_boxed(self)) } } -impl fmt::Display for RightComposition { +impl Display for RightComposition { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&format_boxed(self)) } } -impl fmt::Display for LeftComposition { +impl + ?Sized, A, B: Display> Display for LeftComposition { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&format_boxed(self)) } } -impl fmt::Display for ReductionTensor { +impl, M: BoxedPowersetMapping + ?Sized, A: Display> Display + for ReductionTensor +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&format_boxed(self)) } } -fn format_tensor(shape: &[usize], accessor: &dyn Fn(&[usize]) -> f32) -> String { +fn format_tensor(shape: &[usize], accessor: &dyn Fn(&[usize]) -> A) -> String { if shape.is_empty() { return format!("tensor({})", accessor(&[])); } @@ -70,11 +72,11 @@ fn format_tensor(shape: &[usize], accessor: &dyn Fn(&[usize]) -> f32) -> String out } -fn max_width( +fn max_width( shape: &[usize], index: &mut [usize], dim: usize, - accessor: &dyn Fn(&[usize]) -> f32, + accessor: &dyn Fn(&[usize]) -> A, ) -> usize { let mut width = 0; for i in 0..shape[dim] { @@ -88,14 +90,14 @@ fn max_width( width } -fn format_nested( +fn format_nested( out: &mut String, shape: &[usize], index: &mut [usize], dim: usize, indent: &str, width: usize, - accessor: &dyn Fn(&[usize]) -> f32, + accessor: &dyn Fn(&[usize]) -> A, ) { let innermost = dim == shape.len() - 1; out.push('['); diff --git a/src/functions.rs b/src/functions.rs index 93307fb..5e4569a 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -3,6 +3,8 @@ //! machinery of its own. use std::assert_eq; +use std::fmt::Display; +use std::ops::{Mul, Neg}; use std::rc::Rc; use crate::display::BoxedTensor; @@ -12,40 +14,55 @@ use crate::mapping::{ Squeeze, Unsqueeze, Where, }; use crate::tensor::{ - JaggedTensor, LeftComposition, Physical, Product, Reduction, ReductionTensor, RightComposition, - Sum, Tensor, + JaggedTensor, LeftComposition, Physical, Product, ProductReduction, Reduction, ReductionTensor, + RightComposition, Sum, SumReduction, Tensor, }; -pub fn square(u: Rc) -> LeftComposition { +pub fn square + Copy + 'static, U: Tensor + ?Sized>( + u: Rc, +) -> LeftComposition { LeftComposition::new(u, |x| x * x) } -pub fn negate(u: Rc) -> LeftComposition { +pub fn negate + 'static, U: Tensor + ?Sized>( + u: Rc, +) -> LeftComposition { LeftComposition::new(u, |x| -x) } -pub fn scale(u: Rc, factor: f32) -> LeftComposition { +pub fn scale + Copy + 'static, U: Tensor + ?Sized>( + u: Rc, + factor: A, +) -> LeftComposition { LeftComposition::new(u, move |x| x * factor) } -pub fn pow(u: Rc, exponent: f32) -> LeftComposition { - LeftComposition::new(u, move |x| x.powf(exponent)) -} - -/// Checking the bounds here rather than leaving it to `f32::clamp` reports the -/// mistake where it is made, not at the first read, and rules out the NaN -/// bounds that would panic there too. -pub fn clamp(u: Rc, low: f32, high: f32) -> LeftComposition { +/// Holds values between `low` and `high`. Checking the bounds here reports the +/// mistake where it is made, not at the first read, and rules out NaN bounds, +/// which nothing would be below or above. +pub fn clamp + ?Sized>( + u: Rc, + low: A, + high: A, +) -> LeftComposition { assert!(low <= high, "clamp needs low <= high, got {low} and {high}"); - LeftComposition::new(u, move |x| x.clamp(low, high)) + LeftComposition::new(u, move |x| { + if x < low { + low + } else if x > high { + high + } else { + x + } + }) } /// Reorders the axes of a tensor: `axes[i]` is the axis of `u` that becomes /// axis `i` of the result. -pub fn permute( - u: Rc, +pub fn permute + 'static>( + u: Rc, axes: Vec, -) -> RightComposition { +) -> RightComposition { let codomain = u.hrect().clone(); RightComposition::new(u, Rc::new(Permutation::new(codomain, axes))) } @@ -53,7 +70,9 @@ pub fn permute( /// Swaps the last two axes and leaves any leading ones alone, so a stack of /// matrices transposes matrix by matrix. At rank two this is the plain /// transpose. -pub fn transpose(u: Rc) -> RightComposition { +pub fn transpose + 'static>( + u: Rc, +) -> RightComposition { let ndim = u.domain().ndim(); assert!(ndim >= 2, "transpose needs at least two dimensions"); let mut axes: Vec = (0..ndim).collect(); @@ -63,11 +82,11 @@ pub fn transpose(u: Rc) -> RightComposition( - u: Rc, +pub fn movedim + 'static>( + u: Rc, source: usize, destination: usize, -) -> RightComposition { +) -> RightComposition { let ndim = u.domain().ndim(); assert!(source < ndim, "source axis {source} is out of range"); assert!( @@ -82,7 +101,10 @@ pub fn movedim( /// Inserts an axis of extent 1 at `dim`, which can be anywhere from before the /// first axis to after the last. -pub fn unsqueeze(u: Rc, dim: usize) -> RightComposition { +pub fn unsqueeze + 'static>( + u: Rc, + dim: usize, +) -> RightComposition { let codomain = u.hrect().clone(); RightComposition::new(u, Rc::new(Unsqueeze::new(codomain, dim))) } @@ -127,7 +149,10 @@ impl Dims for &[usize] { /// Drops axes of extent 1, as torch's `squeeze` does: `squeeze(u, ())` drops all /// of them, `squeeze(u, 1)` only axis 1, and `squeeze(u, [0, 2])` axes 0 and 2. /// A named axis whose extent isn't 1 is left alone. -pub fn squeeze(u: Rc, dims: impl Dims) -> RightComposition { +pub fn squeeze + 'static>( + u: Rc, + dims: impl Dims, +) -> RightComposition { let codomain = u.hrect().clone(); let ndim = codomain.shape().len(); let named = dims.axes(ndim); @@ -146,7 +171,10 @@ pub fn squeeze(u: Rc, dims: impl Dims) -> RightComp } /// Joins tensors end to end along `dim`; their other extents must agree. -pub fn concatenate(tensors: Vec>, dim: usize) -> RightComposition { +pub fn concatenate( + tensors: Vec>>, + dim: usize, +) -> RightComposition { assert!(!tensors.is_empty(), "concatenate needs at least one tensor"); let underlying = JaggedTensor::new(tensors); let codomain = underlying.jagged_domain().clone(); @@ -155,7 +183,10 @@ pub fn concatenate(tensors: Vec>, dim: usize) -> RightCompos /// Joins tensors of one shape along a new axis at `dim`, by concatenating them /// once each has an axis of extent 1 there. -pub fn stack(tensors: Vec>, dim: usize) -> RightComposition { +pub fn stack( + tensors: Vec>>, + dim: usize, +) -> RightComposition { assert!(!tensors.is_empty(), "stack needs at least one tensor"); let shape = tensors[0].hrect().clone(); assert!( @@ -166,20 +197,19 @@ pub fn stack(tensors: Vec>, dim: usize) -> RightComposition< .into_iter() .map(|tensor| { let unsqueeze = Unsqueeze::new(tensor.hrect().clone(), dim); - Rc::new(RightComposition::new(tensor, Rc::new(unsqueeze))) as Rc + Rc::new(RightComposition::new(tensor, Rc::new(unsqueeze))) as Rc> }) .collect(); concatenate(unsqueezed, dim) } -/// Reads `input` where `condition` is not zero and `other` where it is. torch's -/// `where` takes a boolean condition; cten's tensors hold floats, so zero stands -/// for false. `where` is a Rust keyword, hence the underscore. -pub fn where_( - condition: Rc, - input: Rc, - other: Rc, -) -> RightComposition { +/// Reads `input` where `condition` holds and `other` where it does not, as +/// torch's `where` does. `where` is a Rust keyword, hence the underscore. +pub fn where_( + condition: Rc>, + input: Rc>, + other: Rc>, +) -> RightComposition { let underlying = JaggedTensor::new(vec![input, other]); let codomain = underlying.jagged_domain().clone(); RightComposition::new( @@ -191,7 +221,13 @@ pub fn where_( /// Spreads a vector over the diagonal of a tensor of the given rank: rank two /// gives a diagonal matrix, rank three a tensor whose only non-zero indices /// are of the form `[i, i, i]`. -pub fn diagonalize(u: Rc, rank: usize) -> Sum { +pub fn diagonalize + 'static>( + u: Rc, + rank: usize, +) -> Sum +where + SumReduction: Reduction, +{ assert_eq!(u.domain().ndim(), 1, "diagonalize takes a vector"); assert!(rank > 0, "a diagonal tensor has at least one dimension"); let extent = u.hrect().shape()[0]; @@ -200,7 +236,7 @@ pub fn diagonalize(u: Rc, rank: usize) -> Sum(shape: Vec) -> ReductionTensor { +fn neutral>(shape: Vec) -> ReductionTensor { let mapping = EmptyMapping::new(HRect::new(shape), HRect::new(vec![0])); ReductionTensor::new(Rc::new(Physical::new(vec![])), Rc::new(mapping)) } @@ -231,11 +267,14 @@ pub fn eye(n: usize) -> Sum { /// `indices`, and zero everywhere else. As in torch, `indices` has one row per /// dimension, so column `k` is the coordinate of `values[k]`, and values at a /// repeated coordinate add up. -pub fn sparse_coo_tensor( +pub fn sparse_coo_tensor( indices: Vec>, - values: Rc, + values: Rc>, size: Vec, -) -> Sum { +) -> Sum +where + SumReduction: Reduction, +{ assert_eq!( indices.len(), size.len(), @@ -273,30 +312,49 @@ pub fn sparse_coo_tensor( /// `physical` sits at `basisĀ·x + offset`, and the tensor is zero off that /// lattice. `basis` has a row per dimension of `shape` and a column per /// dimension of `physical`. Values the lattice sends to one index add up. -pub fn sparse_latticed_tensor( - physical: Rc, +pub fn sparse_latticed_tensor + 'static>( + physical: Rc, basis: Vec>, offset: Vec, shape: Vec, -) -> Sum { +) -> Sum +where + SumReduction: Reduction, +{ let affine = Affine::new(physical.hrect().clone(), basis, offset, HRect::new(shape)); Sum::new(physical, Rc::new(InvAffine::new(affine))) } -pub fn sum(input: Rc, dim: usize) -> Sum { +pub fn sum + 'static>( + input: Rc, + dim: usize, +) -> Sum +where + SumReduction: Reduction, +{ let codomain = input.hrect().clone(); Sum::new(input, Rc::new(InvProjection::new(codomain, dim))) } -pub fn prod(input: Rc, dim: usize) -> Product { +pub fn prod + 'static>( + input: Rc, + dim: usize, +) -> Product +where + ProductReduction: Reduction, +{ let codomain = input.hrect().clone(); Product::new(input, Rc::new(InvProjection::new(codomain, dim))) } -pub fn mm( - mat1: Rc, - mat2: Rc, -) -> Sum { +pub fn mm + 'static, V: BoxedTensor + 'static>( + mat1: Rc, + mat2: Rc, +) -> Sum +where + SumReduction: Reduction, + ProductReduction: Reduction, +{ assert_eq!(mat1.hrect().shape().len(), 2, "mat1 must be a matrix"); assert_eq!(mat2.hrect().shape().len(), 2, "mat2 must be a matrix"); assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index d1e8b96..638af52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,8 +12,8 @@ pub mod tensor; pub use display::{BoxedTensor, format_boxed}; pub use domain::{Domain, HRect, JaggedDomain}; pub use functions::{ - Dims, clamp, concatenate, diagonalize, eye, eye_rect, mm, movedim, negate, ones, permute, pow, - prod, scale, sparse_coo_tensor, sparse_latticed_tensor, square, squeeze, stack, sum, transpose, + Dims, clamp, concatenate, diagonalize, eye, eye_rect, mm, movedim, negate, ones, permute, prod, + scale, sparse_coo_tensor, sparse_latticed_tensor, square, squeeze, stack, sum, transpose, unsqueeze, where_, zeros, }; pub use mapping::{ diff --git a/src/mapping.rs b/src/mapping.rs index b2642d0..61c0bcf 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -348,18 +348,18 @@ impl HRectMapping for Concat { } } -/// Reads the first of two tensors where `condition` is not zero and the second -/// where it is. Unlike the mappings above, where an index is sent depends on a +/// Reads the first of two tensors where `condition` holds and the second where +/// it does not. Unlike the mappings above, where an index is sent depends on a /// tensor's values and not only on the index itself. pub struct Where { signature: Signature, - condition: Rc, + condition: Rc>, } impl Where { /// Takes the codomain, the two tensors picked from. The domain is their /// common shape, which `condition` must share. - pub fn new(codomain: JaggedDomain, condition: Rc) -> Self { + pub fn new(codomain: JaggedDomain, condition: Rc>) -> Self { let parts = codomain.parts(); assert_eq!(parts.len(), 2, "where picks from exactly two tensors"); let shape = parts[0].clone(); @@ -381,12 +381,7 @@ impl HRectMapping for Where { type Codomain = JaggedDomain; fn map(&self, index: &[usize]) -> Vec { - // Zero reads as false and anything else, NaN included, as true. - let part = if self.condition.get(index) != 0.0 { - 0 - } else { - 1 - }; + let part = if self.condition.get(index) { 0 } else { 1 }; let mut result = Vec::with_capacity(index.len() + 1); result.push(part); result.extend_from_slice(index); diff --git a/src/tensor.rs b/src/tensor.rs index d9bf73d..72b63a0 100644 --- a/src/tensor.rs +++ b/src/tensor.rs @@ -6,9 +6,10 @@ use crate::domain::{Domain, HRect, JaggedDomain}; use crate::mapping::{BoxedMapping, BoxedPowersetMapping, IndexMapping, PowersetMapping}; /// A mapping from an index set to a value set. The index set is the tensor's -/// domain; the value set is fixed to the float numbers for now, for simplicity. -pub trait Tensor { - fn get(&self, index: &[usize]) -> f32; +/// domain, and the value set is its value type `A`, `f32` unless said +/// otherwise. +pub trait Tensor { + fn get(&self, index: &[usize]) -> A; fn domain(&self) -> &dyn Domain; } @@ -17,7 +18,7 @@ pub trait Tensor { /// which is all that can be checked without enumerating a domain. In /// particular, a mapping into flat positions needs a one-dimensional tensor /// under it. -fn check_composable(codomain: &dyn Domain, u: &dyn Tensor) { +fn check_composable(codomain: &dyn Domain, u: &dyn Tensor) { assert_eq!( codomain.ndim(), u.domain().ndim(), @@ -27,26 +28,26 @@ fn check_composable(codomain: &dyn Domain, u: &dyn Tensor) { ); } -pub struct Physical { - values: Vec, +pub struct Physical { + values: Vec, domain: HRect, } -impl Physical { - pub fn new(values: Vec) -> Self { +impl Physical { + pub fn new(values: Vec) -> Self { let domain = HRect::new(vec![values.len()]); Self { values, domain } } } -impl Tensor for Physical { - fn get(&self, index: &[usize]) -> f32 { +impl Tensor for Physical { + fn get(&self, index: &[usize]) -> A { assert_eq!( index.len(), 1, "a physical tensor is indexed by a single coordinate" ); - self.values[index[0]] + self.values[index[0]].clone() } fn domain(&self) -> &dyn Domain { @@ -54,7 +55,7 @@ impl Tensor for Physical { } } -impl BoxedTensor for Physical { +impl BoxedTensor for Physical { fn hrect(&self) -> &HRect { &self.domain } @@ -63,13 +64,13 @@ impl BoxedTensor for Physical { /// Several tensors read as one: `[t, rest..]` reads part `t` at `rest`. This is /// the underlying that concatenating, stacking and `where` remap into. Its domain /// is jagged rather than a hrect, so it is not printable. -pub struct JaggedTensor { - parts: Vec>, +pub struct JaggedTensor { + parts: Vec>>, domain: JaggedDomain, } -impl JaggedTensor { - pub fn new(parts: Vec>) -> Self { +impl JaggedTensor { + pub fn new(parts: Vec>>) -> Self { let domain = JaggedDomain::new(parts.iter().map(|part| part.hrect().clone()).collect()); Self { parts, domain } } @@ -79,8 +80,8 @@ impl JaggedTensor { } } -impl Tensor for JaggedTensor { - fn get(&self, index: &[usize]) -> f32 { +impl Tensor for JaggedTensor { + fn get(&self, index: &[usize]) -> A { self.parts[index[0]].get(&index[1..]) } @@ -92,21 +93,22 @@ impl Tensor for JaggedTensor { // The domain is the mapping's own domain, so it is never passed in. The mapping // type is a parameter rather than always `dyn IndexMapping` so that composing // over a boxed mapping is known to give a boxed, printable tensor. Pass -// `dyn BoxedMapping` or `dyn IndexMapping` for it to erase the mapping. -pub struct RightComposition { - u: Rc, +// `dyn BoxedMapping` or `dyn IndexMapping` for it to erase the mapping. The +// values are the underlying tensor's, read elsewhere. +pub struct RightComposition { + u: Rc>, mapping: Rc, } -impl RightComposition { - pub fn new(u: Rc, mapping: Rc) -> Self { +impl RightComposition { + pub fn new(u: Rc>, mapping: Rc) -> Self { check_composable(mapping.codomain(), &*u); Self { u, mapping } } } -impl Tensor for RightComposition { - fn get(&self, virtual_index: &[usize]) -> f32 { +impl Tensor for RightComposition { + fn get(&self, virtual_index: &[usize]) -> A { let underlying_index = self.mapping.map(virtual_index); self.u.get(&underlying_index) } @@ -116,7 +118,7 @@ impl Tensor for RightComposition { } } -impl BoxedTensor for RightComposition { +impl BoxedTensor for RightComposition { fn hrect(&self) -> &HRect { self.mapping.hrect() } @@ -124,22 +126,23 @@ impl BoxedTensor for RightComposition { // Transforming the values leaves the index set alone, so the domain is the // underlying tensor's. Its type is a parameter so that a transform of a boxed -// tensor is known to be boxed, and printable, in turn. -pub struct LeftComposition { - u: Rc, +// tensor is known to be boxed, and printable, in turn. The transform may change +// the value type, from `A` to `B`, as a comparison turns numbers into booleans. +pub struct LeftComposition { + u: Rc, // Held as a closure rather than a plain `fn` pointer so that a transform can // carry values of its own, as `scale` and `clamp` do. - f: Rc f32>, + f: Rc B>, } -impl LeftComposition { - pub fn new(u: Rc, f: impl Fn(f32) -> f32 + 'static) -> Self { +impl + ?Sized, A, B> LeftComposition { + pub fn new(u: Rc, f: impl Fn(A) -> B + 'static) -> Self { Self { u, f: Rc::new(f) } } } -impl Tensor for LeftComposition { - fn get(&self, index: &[usize]) -> f32 { +impl + ?Sized, A, B> Tensor for LeftComposition { + fn get(&self, index: &[usize]) -> B { (self.f)(self.u.get(index)) } @@ -148,64 +151,74 @@ impl Tensor for LeftComposition { } } -impl BoxedTensor for LeftComposition { +impl + ?Sized, A, B> BoxedTensor for LeftComposition { fn hrect(&self) -> &HRect { self.u.hrect() } } -pub trait Reduction { - const NEUTRAL: f32; - fn combine(a: f32, b: f32) -> f32; +/// A way of combining the values of a tensor of value type `A` into one. +pub trait Reduction { + const NEUTRAL: A; + fn combine(a: A, b: A) -> A; } pub struct SumReduction; - -impl Reduction for SumReduction { - const NEUTRAL: f32 = 0.0; - fn combine(a: f32, b: f32) -> f32 { - a + b - } -} - pub struct ProductReduction; +pub struct SupReduction; +pub struct InfReduction; -impl Reduction for ProductReduction { - const NEUTRAL: f32 = 1.0; - fn combine(a: f32, b: f32) -> f32 { - a * b - } -} +// The four reductions for each value type they apply to: a sum starts from 0, a +// product from 1, and a sup and an inf from the bottom and top of the type. +macro_rules! reductions { + ($($value:ty: $zero:expr, $one:expr, $bottom:expr, $top:expr;)*) => {$( + impl Reduction<$value> for SumReduction { + const NEUTRAL: $value = $zero; + fn combine(a: $value, b: $value) -> $value { + a + b + } + } -pub struct SupReduction; + impl Reduction<$value> for ProductReduction { + const NEUTRAL: $value = $one; + fn combine(a: $value, b: $value) -> $value { + a * b + } + } -impl Reduction for SupReduction { - const NEUTRAL: f32 = f32::NEG_INFINITY; - fn combine(a: f32, b: f32) -> f32 { - a.max(b) - } -} + impl Reduction<$value> for SupReduction { + const NEUTRAL: $value = $bottom; + fn combine(a: $value, b: $value) -> $value { + a.max(b) + } + } -pub struct InfReduction; + impl Reduction<$value> for InfReduction { + const NEUTRAL: $value = $top; + fn combine(a: $value, b: $value) -> $value { + a.min(b) + } + } + )*}; +} -impl Reduction for InfReduction { - const NEUTRAL: f32 = f32::INFINITY; - fn combine(a: f32, b: f32) -> f32 { - a.min(b) - } +reductions! { + f32: 0.0, 1.0, f32::NEG_INFINITY, f32::INFINITY; + f64: 0.0, 1.0, f64::NEG_INFINITY, f64::INFINITY; + i64: 0, 1, i64::MIN, i64::MAX; } // As in a right composition, the domain is the mapping's own domain, and the // mapping type is a parameter so that reducing over a boxed mapping is known to // give a boxed, printable tensor. -pub struct ReductionTensor { - u: Rc, +pub struct ReductionTensor { + u: Rc>, mapping: Rc, _marker: PhantomData R>, } -impl ReductionTensor { - pub fn new(u: Rc, mapping: Rc) -> Self { +impl, M: PowersetMapping + ?Sized, A> ReductionTensor { + pub fn new(u: Rc>, mapping: Rc) -> Self { check_composable(mapping.codomain(), &*u); Self { u, @@ -215,8 +228,8 @@ impl ReductionTensor { } } -impl Tensor for ReductionTensor { - fn get(&self, index: &[usize]) -> f32 { +impl, M: PowersetMapping + ?Sized, A> Tensor for ReductionTensor { + fn get(&self, index: &[usize]) -> A { let indices = self.mapping.map(index); let mut result = R::NEUTRAL; for i in &indices { @@ -230,13 +243,15 @@ impl Tensor for ReductionTensor } } -impl BoxedTensor for ReductionTensor { +impl, M: BoxedPowersetMapping + ?Sized, A> BoxedTensor + for ReductionTensor +{ fn hrect(&self) -> &HRect { self.mapping.hrect() } } -pub type Sum = ReductionTensor; -pub type Product = ReductionTensor; -pub type Sup = ReductionTensor; -pub type Inf = ReductionTensor; +pub type Sum = ReductionTensor; +pub type Product = ReductionTensor; +pub type Sup = ReductionTensor; +pub type Inf = ReductionTensor; diff --git a/tests/display.rs b/tests/display.rs index ba52527..e81aba4 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -13,6 +13,15 @@ fn a_physical_vector_prints_flat() { assert_eq!(physical().to_string(), "tensor([1, 2, 3, 4, 5, 6])"); } +#[test] +fn tensors_of_other_value_types_print_the_same_way() { + let ints = Physical::new(vec![1i64, -20, 3]); + let flags = Physical::new(vec![true, false]); + + assert_eq!(ints.to_string(), "tensor([ 1, -20, 3])"); + assert_eq!(flags.to_string(), "tensor([ true, false])"); +} + #[test] fn a_rank_two_tensor_prints_one_row_per_line_aligned_under_the_bracket() { let strided = RightComposition::new( diff --git a/tests/functions.rs b/tests/functions.rs index c6a7a49..873b07e 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -1,10 +1,10 @@ use std::rc::Rc; use cten::{ - BoxedTensor, COO, Domain, HRect, Physical, RightComposition, Stride, Sum, Tensor, clamp, - concatenate, diagonalize, eye, eye_rect, mm, movedim, negate, ones, permute, pow, prod, scale, - sparse_coo_tensor, sparse_latticed_tensor, square, squeeze, stack, sum, transpose, unsqueeze, - where_, zeros, + BoxedTensor, COO, Domain, HRect, LeftComposition, Physical, RightComposition, Stride, Sum, + Tensor, clamp, concatenate, diagonalize, eye, eye_rect, mm, movedim, negate, ones, permute, + prod, scale, sparse_coo_tensor, sparse_latticed_tensor, square, squeeze, stack, sum, transpose, + unsqueeze, where_, zeros, }; fn physical() -> Rc { @@ -36,11 +36,19 @@ fn matrix(rows: usize, cols: usize, values: Vec) -> Rc Vec { +/// A row-major matrix over stored integers. +fn int_matrix(rows: usize, cols: usize, values: Vec) -> Rc> { + Rc::new(RightComposition::new( + Rc::new(Physical::new(values)), + Rc::new(Stride::new(HRect::new(vec![rows, cols]), vec![cols, 1])), + )) +} + +fn collect_1d(tensor: &dyn Tensor, len: usize) -> Vec { (0..len).map(|i| tensor.get(&[i])).collect() } -fn collect_2d(tensor: &dyn Tensor, rows: usize, cols: usize) -> Vec> { +fn collect_2d(tensor: &dyn Tensor, rows: usize, cols: usize) -> Vec> { (0..rows) .map(|i| (0..cols).map(|j| tensor.get(&[i, j])).collect()) .collect() @@ -72,13 +80,11 @@ fn elementwise_functions_compose() { #[test] fn parameterised_transforms_remember_their_argument() { let doubled = scale(physical(), 2.0); - let cubed = pow(physical(), 3.0); let bounded = clamp(physical(), 2.0, 4.0); for i in 0..6 { let value = (i + 1) as f32; assert_eq!(doubled.get(&[i]), value * 2.0); - assert_eq!(cubed.get(&[i]), value * value * value); } // [1, 2, 3, 4, 5, 6] held between 2 and 4. @@ -113,13 +119,23 @@ fn clamp_rejects_a_reversed_range() { clamp(physical(), 4.0, 2.0); } -// Rejecting these up front is what keeps `f32::clamp` from panicking on a read. +// Nothing is below or above a NaN bound, so it is rejected up front too. #[test] #[should_panic(expected = "clamp needs low <= high")] fn clamp_rejects_a_nan_bound() { clamp(physical(), f32::NAN, 4.0); } +#[test] +fn elementwise_functions_work_on_integer_tensors() { + let ints = Rc::new(Physical::new(vec![-3i64, 1, 5])); + + assert_eq!(collect_1d(&square(ints.clone()), 3), vec![9, 1, 25]); + assert_eq!(collect_1d(&negate(ints.clone()), 3), vec![3, -1, -5]); + assert_eq!(collect_1d(&scale(ints.clone(), 2), 3), vec![-6, 2, 10]); + assert_eq!(collect_1d(&clamp(ints, 0, 4), 3), vec![0, 1, 4]); +} + #[test] fn an_elementwise_transform_keeps_the_domain() { let squared = square(strided()); @@ -516,7 +532,7 @@ fn concatenate_rejects_tensors_whose_other_extents_differ() { #[test] #[should_panic(expected = "at least one tensor")] fn concatenate_rejects_an_empty_list() { - concatenate(vec![], 0); + concatenate::(vec![], 0); } #[test] @@ -549,7 +565,7 @@ fn stack_rejects_tensors_of_different_shapes() { #[test] fn where_reads_input_where_the_condition_holds_and_other_elsewhere() { - let condition = Rc::new(Physical::new(vec![1.0, 0.0, 0.0, 1.0, 0.0, 1.0])); + let condition = Rc::new(Physical::new(vec![true, false, false, true, false, true])); let picked = where_(condition, physical(), Rc::new(negate(physical()))); assert_eq!( @@ -558,22 +574,22 @@ fn where_reads_input_where_the_condition_holds_and_other_elsewhere() { ); } -// Zero reads as false, and so does negative zero, which equals it; anything -// else reads as true, NaN included, as it would in C or Python. +// A comparison turns a float tensor into a boolean one, to use as a condition. #[test] -fn where_counts_any_condition_other_than_zero_as_true() { - let condition = Rc::new(Physical::new(vec![-1.0, f32::NAN, 0.5, 0.0, -0.0, 0.0])); - let picked = where_(condition, physical(), Rc::new(zeros(vec![6]))); +fn where_can_take_its_condition_from_a_comparison() { + let above_three = Rc::new(LeftComposition::new(physical(), |x: f32| x > 3.0)); + let picked = where_(above_three, physical(), Rc::new(zeros(vec![6]))); - assert_eq!(collect_1d(&picked, 6), vec![1.0, 2.0, 3.0, 0.0, 0.0, 0.0]); + assert_eq!(collect_1d(&picked, 6), vec![0.0, 0.0, 0.0, 4.0, 5.0, 6.0]); } -// The condition is a tensor like any other: here the identity keeps the -// diagonal of one matrix and takes everything else from another. +// The condition is a tensor like any other: here the non-zeros of the identity +// keep the diagonal of one matrix and take everything else from another. #[test] fn where_can_take_its_condition_from_a_tensor_with_no_data() { let m = matrix(2, 2, vec![1.0, 2.0, 3.0, 4.0]); - let picked = where_(Rc::new(eye(2)), m.clone(), Rc::new(negate(m))); + let diagonal = Rc::new(LeftComposition::new(Rc::new(eye(2)), |x: f32| x != 0.0)); + let picked = where_(diagonal, m.clone(), Rc::new(negate(m))); assert_eq!( collect_2d(&picked, 2, 2), @@ -584,7 +600,11 @@ fn where_can_take_its_condition_from_a_tensor_with_no_data() { #[test] #[should_panic(expected = "one shape")] fn where_rejects_a_condition_of_another_shape() { - where_(Rc::new(ones(vec![2])), physical(), physical()); + where_( + Rc::new(Physical::new(vec![true, false])), + physical(), + physical(), + ); } // The example from torch's documentation for `sparse_coo_tensor`. @@ -822,3 +842,56 @@ fn mm_multiplies_matrices() { vec![vec![58.0, 64.0], vec![139.0, 154.0]] ); } + +// Functions that only move indices work whatever the values are. +#[test] +fn index_functions_work_on_integer_tensors() { + let m = int_matrix(2, 3, vec![1, 2, 3, 4, 5, 6]); + let joined = concatenate(vec![m.clone(), m.clone()], 0); + let stacked = stack(vec![m.clone(), m.clone()], 0); + + assert_eq!( + collect_2d(&transpose(m), 3, 2), + vec![vec![1, 4], vec![2, 5], vec![3, 6]] + ); + assert_eq!(joined.hrect().shape(), &[4, 3]); + assert_eq!(joined.get(&[3, 2]), 6); + assert_eq!(stacked.hrect().shape(), &[2, 2, 3]); + assert_eq!(stacked.get(&[1, 1, 0]), 4); +} + +#[test] +fn where_picks_between_integer_tensors() { + let condition = Rc::new(Physical::new(vec![true, false, true])); + let picked = where_( + condition, + Rc::new(Physical::new(vec![1i64, 2, 3])), + Rc::new(Physical::new(vec![-1i64, -2, -3])), + ); + + assert_eq!(collect_1d(&picked, 3), vec![1, -2, 3]); +} + +#[test] +fn sum_and_prod_reduce_integer_and_double_tensors() { + let ints = int_matrix(2, 3, vec![1, 2, 3, 4, 5, 6]); + let doubles = Rc::new(Physical::new(vec![0.5f64, 0.25, 2.0])); + + assert_eq!(collect_1d(&sum(ints.clone(), 0), 3), vec![5, 7, 9]); + assert_eq!(collect_1d(&prod(ints, 1), 2), vec![6, 120]); + assert_eq!(sum(doubles.clone(), 0).get(&[]), 2.75); + assert_eq!(prod(doubles, 0).get(&[]), 0.25); +} + +#[test] +fn mm_multiplies_integer_matrices() { + let product = mm( + int_matrix(2, 3, vec![1, 2, 3, 4, 5, 6]), + int_matrix(3, 2, vec![7, 8, 9, 10, 11, 12]), + ); + + assert_eq!( + collect_2d(&product, 2, 2), + vec![vec![58, 64], vec![139, 154]] + ); +} diff --git a/tests/logic.rs b/tests/logic.rs index bab84db..9557ae5 100644 --- a/tests/logic.rs +++ b/tests/logic.rs @@ -232,7 +232,7 @@ fn diagonal_reduction_generalises_to_rank_three() { // comes out of an empty store; putting those on the diagonal gives identity. #[test] fn empty_mapping_under_a_product_yields_ones_from_an_empty_store() { - let empty = Rc::new(Physical::new(vec![])); + let empty = Rc::new(Physical::::new(vec![])); let ones = Rc::new(Product::new( empty, Rc::new(EmptyMapping::new(HRect::new(vec![6]), HRect::new(vec![0]))), diff --git a/tests/mapping.rs b/tests/mapping.rs index 0150e65..8e2888d 100644 --- a/tests/mapping.rs +++ b/tests/mapping.rs @@ -386,7 +386,7 @@ fn a_concat_rejects_parts_whose_other_extents_differ() { fn a_where_sends_each_index_to_the_part_its_condition_picks() { let picks = Where::new( JaggedDomain::new(vec![HRect::new(vec![3]), HRect::new(vec![3])]), - Rc::new(Physical::new(vec![1.0, 0.0, 2.0])), + Rc::new(Physical::new(vec![true, false, true])), ); assert!(picks.domain().contains(&[2])); diff --git a/tests/tensor.rs b/tests/tensor.rs index 675cdf7..aa376e7 100644 --- a/tests/tensor.rs +++ b/tests/tensor.rs @@ -68,6 +68,27 @@ fn a_left_composition_takes_its_domain_from_the_tensor_it_transforms() { assert_eq!(squared.get(&[2, 1]), 36.0); } +#[test] +fn a_physical_tensor_holds_values_of_any_type() { + let ints = Physical::new(vec![3i64, -1]); + let flags = Physical::new(vec![true, false]); + + assert_eq!(ints.get(&[1]), -1); + assert!(flags.get(&[0])); + assert!(!flags.get(&[1])); +} + +// A transform may change the value type, as a comparison turns numbers into +// booleans. +#[test] +fn a_left_composition_can_change_the_value_type() { + let above_three = LeftComposition::new(physical(), |x: f32| x > 3.0); + + assert_eq!(above_three.hrect().shape(), &[6]); + assert!(!above_three.get(&[2])); + assert!(above_three.get(&[3])); +} + #[test] fn a_jagged_tensor_reads_each_part_under_its_first_coordinate() { let jagged = JaggedTensor::new(vec![physical(), Rc::new(Physical::new(vec![10.0, 20.0]))]);