diff --git a/src/display.rs b/src/display.rs
index de1bc02..7f38a3e 100644
--- a/src/display.rs
+++ b/src/display.rs
@@ -1,26 +1,18 @@
-//! Printing of boxed tensors: those whose domain is a hrect. The `Display`
-//! implementations live here rather than next to each tensor, since they all
-//! render the same way, by walking a shape.
+//! Printing of functions on a hrect: tensors, and multiset-valued tensors before
+//! a reduction. The `Display` implementations live here rather than next to
+//! each tensor, since they all render the same way, by walking a shape.
use std::fmt;
use std::fmt::{Display, Write as _};
-use crate::domain::HRect;
-use crate::mapping::{BoxedMapping, BoxedPowersetMapping};
-use crate::tensor::{
- LeftComposition, Physical, Reduction, ReductionTensor, RightComposition, Tensor,
-};
+use crate::function::{Composition, Function};
+use crate::sets::HRect;
+use crate::sets::Set;
+use crate::tensor::{Physical, 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 {
- 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 {
- format_tensor(tensor.hrect().shape(), &|index| tensor.get(index))
+/// Renders a tensor on a hrect.
+pub fn format_boxed(tensor: &dyn Tensor) -> String {
+ format_tensor(tensor.domain().shape(), &|index| tensor.apply(index))
}
impl Display for Physical {
@@ -29,23 +21,18 @@ impl Display for Physical {
}
}
-impl Display for RightComposition {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(&format_boxed(self))
- }
-}
-
-impl + ?Sized, A, B: Display> Display for LeftComposition {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(&format_boxed(self))
- }
-}
-
-impl, M: BoxedPowersetMapping + ?Sized, A: Display> Display
- for ReductionTensor
+// A composition prints when the function applied first is one on a hrect and the
+// one applied last gives values that print, multisets included.
+impl Display for Composition
+where
+ F: Function,
+ G: Function,
+ ::Owned: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(&format_boxed(self))
+ f.write_str(&format_tensor(self.domain().shape(), &|index| {
+ self.apply(index)
+ }))
}
}
diff --git a/src/domain.rs b/src/domain.rs
deleted file mode 100644
index b121832..0000000
--- a/src/domain.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-/// A set of indices, an index being a vector of coordinates. Tensors are
-/// defined on a domain, and mappings go from a domain to a codomain.
-pub trait Domain {
- /// Number of coordinates the indices of this domain have.
- fn ndim(&self) -> usize;
-
- fn contains(&self, index: &[usize]) -> bool;
-}
-
-/// A hyperrectangle of indices: coordinate `i` runs over `0..shape[i]`.
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub struct HRect {
- shape: Vec,
-}
-
-impl HRect {
- pub fn new(shape: Vec) -> Self {
- Self { shape }
- }
-
- pub fn shape(&self) -> &[usize] {
- &self.shape
- }
-}
-
-impl Domain for HRect {
- fn ndim(&self) -> usize {
- self.shape.len()
- }
-
- fn contains(&self, index: &[usize]) -> bool {
- index.len() == self.shape.len()
- && index
- .iter()
- .zip(&self.shape)
- .all(|(coord, extent)| coord < extent)
- }
-}
-
-/// The disjoint union of several hrects: `[t, rest..]` belongs to it when `rest`
-/// is in hrect `t`. This is the index set of a jagged tensor, whose parts may
-/// have different shapes.
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub struct JaggedDomain {
- parts: Vec,
-}
-
-impl JaggedDomain {
- pub fn new(parts: Vec) -> Self {
- assert!(!parts.is_empty(), "a jagged domain needs at least one part");
- let ndim = parts[0].ndim();
- assert!(
- parts.iter().all(|part| part.ndim() == ndim),
- "the parts of a jagged domain must all have the same number of coordinates"
- );
- Self { parts }
- }
-
- pub fn parts(&self) -> &[HRect] {
- &self.parts
- }
-}
-
-impl Domain for JaggedDomain {
- fn ndim(&self) -> usize {
- 1 + self.parts[0].ndim()
- }
-
- fn contains(&self, index: &[usize]) -> bool {
- match index.split_first() {
- Some((&part, rest)) => part < self.parts.len() && self.parts[part].contains(rest),
- None => false,
- }
- }
-}
diff --git a/src/function.rs b/src/function.rs
new file mode 100644
index 0000000..baf5cb4
--- /dev/null
+++ b/src/function.rs
@@ -0,0 +1,147 @@
+//! The building bricks: functions, their composition, and their extension to
+//! multisets. Tensors, index maps, index functions and reductions are all
+//! functions, and every operation on them is built from these.
+
+use std::borrow::Borrow;
+use std::rc::Rc;
+
+use crate::sets::{Multisets, Set, Values};
+
+/// A function from its domain to its codomain, which may be sets of indices,
+/// of values, or of multisets of either. A tensor goes from indices to values,
+/// an index map from indices to indices, an index function from indices to
+/// multisets of indices, and a reduction from multisets of values to values.
+pub trait Function {
+ type Domain: Set;
+ type Codomain: Set;
+
+ fn apply(&self, x: &::Element) -> ::Owned;
+
+ fn domain(&self) -> Self::Domain;
+
+ fn codomain(&self) -> Self::Codomain;
+}
+
+// A shared function is the same function, so that one can be used in several
+// compositions, or have its type erased as an `Rc>`.
+impl Function for Rc {
+ type Domain = T::Domain;
+ type Codomain = T::Codomain;
+
+ fn apply(&self, x: &::Element) -> ::Owned {
+ (**self).apply(x)
+ }
+
+ fn domain(&self) -> Self::Domain {
+ (**self).domain()
+ }
+
+ fn codomain(&self) -> Self::Codomain {
+ (**self).codomain()
+ }
+}
+
+/// `f ∘ g`: applies `g`, then `f` to the result.
+pub struct Composition {
+ f: F,
+ g: G,
+}
+
+/// Composes `f` after `g`. Composing a tensor after an index map re-indexes it,
+/// and a function on values after a tensor applies it element-wise. The
+/// codomain of `g` must lie in the domain of `f`.
+pub fn compose(f: F, g: G) -> Composition
+where
+ F: Function,
+ G: Function,
+{
+ let (codomain, domain) = (g.codomain(), f.domain());
+ assert!(
+ codomain.lies_in(&domain),
+ "the inner function lands in {codomain:?}, which does not lie in the domain {domain:?} of the outer one"
+ );
+ Composition { f, g }
+}
+
+impl Function for Composition
+where
+ F: Function,
+ G: Function,
+{
+ type Domain = G::Domain;
+ type Codomain = F::Codomain;
+
+ fn apply(&self, x: &::Element) -> ::Owned {
+ self.f.apply(self.g.apply(x).borrow())
+ }
+
+ fn domain(&self) -> Self::Domain {
+ self.g.domain()
+ }
+
+ fn codomain(&self) -> Self::Codomain {
+ self.f.codomain()
+ }
+}
+
+/// A function from multisets to multisets: the extension of `f` to multisets,
+/// written `f̄` in the paper. It maps a multiset to the multiset of the images
+/// of its elements, repeats included, so it goes from the multisets of the
+/// domain of `f` to those of its codomain.
+pub struct MultiFunction {
+ f: F,
+}
+
+pub fn multiset_lift(f: F) -> MultiFunction {
+ MultiFunction { f }
+}
+
+impl Function for MultiFunction {
+ type Domain = Multisets;
+ type Codomain = Multisets;
+
+ fn apply(&self, x: &::Element) -> ::Owned {
+ x.iter()
+ .map(|element| self.f.apply(element.borrow()))
+ .collect()
+ }
+
+ fn domain(&self) -> Self::Domain {
+ Multisets::new(self.f.domain())
+ }
+
+ fn codomain(&self) -> Self::Codomain {
+ Multisets::new(self.f.codomain())
+ }
+}
+
+/// A function on values, given as a closure, from `A` to `B`. Composed after a
+/// tensor, it applies element-wise.
+pub struct ValueFunction {
+ // Held as a closure rather than a plain `fn` pointer so that a function can
+ // carry values of its own, as `scale` and `clamp` do.
+ f: Rc B>,
+}
+
+impl ValueFunction {
+ pub fn new(f: impl Fn(A) -> B + 'static) -> Self {
+ Self { f: Rc::new(f) }
+ }
+}
+
+impl Function for ValueFunction {
+ type Domain = Values;
+ type Codomain = Values;
+
+ fn apply(&self, x: &A) -> B {
+ (self.f)(x.clone())
+ }
+
+ fn domain(&self) -> Values {
+ Values::new()
+ }
+
+ fn codomain(&self) -> Values {
+ Values::new()
+ }
+}
diff --git a/src/functions.rs b/src/functions.rs
index 5e4569a..14692c8 100644
--- a/src/functions.rs
+++ b/src/functions.rs
@@ -1,52 +1,52 @@
//! Named constructors for the compositions the library can already express.
-//! Everything here is assembled from `mapping` and `tensor`; none of it adds
-//! machinery of its own.
+//! Everything here is assembled from `function`, `index_functions`,
+//! `reductions` and `tensor`; none of it adds 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;
-use crate::domain::HRect;
-use crate::mapping::{
+use crate::function::{Composition, Function, ValueFunction, compose, multiset_lift};
+use crate::index_functions::{
Affine, COO, Concat, Diagonal, EmptyMapping, InvAffine, InvProjection, Permutation, Projection,
Squeeze, Unsqueeze, Where,
};
-use crate::tensor::{
- JaggedTensor, LeftComposition, Physical, Product, ProductReduction, Reduction, ReductionTensor,
- RightComposition, Sum, SumReduction, Tensor,
+use crate::reductions::{
+ Product, ProductReduction, Reduction, ReductionFunction, Sum, SumReduction,
};
+use crate::sets::{HRect, IndexSet, Values};
+use crate::tensor::{JaggedTensor, Physical, ReductionTensor, Tensor};
-pub fn square + Copy + 'static, U: Tensor + ?Sized>(
- u: Rc,
-) -> LeftComposition {
- LeftComposition::new(u, |x| x * x)
+pub fn square + Copy + 'static, U: Function>>(
+ u: U,
+) -> Composition, U> {
+ compose(ValueFunction::new(|x: A| x * x), u)
}
-pub fn negate + 'static, U: Tensor + ?Sized>(
- u: Rc,
-) -> LeftComposition {
- LeftComposition::new(u, |x| -x)
+pub fn negate + Clone + 'static, U: Function>>(
+ u: U,
+) -> Composition, U> {
+ compose(ValueFunction::new(|x: A| -x), u)
}
-pub fn scale + Copy + 'static, U: Tensor + ?Sized>(
- u: Rc,
+pub fn scale + Copy + 'static, U: Function>>(
+ u: U,
factor: A,
-) -> LeftComposition {
- LeftComposition::new(u, move |x| x * factor)
+) -> Composition, U> {
+ compose(ValueFunction::new(move |x: A| x * factor), u)
}
/// 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,
+pub fn clamp>>(
+ u: U,
low: A,
high: A,
-) -> LeftComposition {
+) -> Composition, U> {
assert!(low <= high, "clamp needs low <= high, got {low} and {high}");
- LeftComposition::new(u, move |x| {
+ let clamped = ValueFunction::new(move |x: A| {
if x < low {
low
} else if x > high {
@@ -54,25 +54,21 @@ pub fn clamp + ?Sized>(
} else {
x
}
- })
+ });
+ compose(clamped, u)
}
/// Reorders the axes of a tensor: `axes[i]` is the axis of `u` that becomes
/// axis `i` of the result.
-pub fn permute + 'static>(
- u: Rc,
- axes: Vec,
-) -> RightComposition {
- let codomain = u.hrect().clone();
- RightComposition::new(u, Rc::new(Permutation::new(codomain, axes)))
+pub fn permute>(u: U, axes: Vec) -> Composition {
+ let codomain = u.domain();
+ compose(u, Permutation::new(codomain, axes))
}
/// 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 + 'static>(
- u: Rc,
-) -> RightComposition {
+pub fn transpose>(u: U) -> Composition {
let ndim = u.domain().ndim();
assert!(ndim >= 2, "transpose needs at least two dimensions");
let mut axes: Vec = (0..ndim).collect();
@@ -82,11 +78,11 @@ pub fn transpose + 'static>(
/// Moves the axis at `source` to `destination`; the axes left over keep their
/// relative order.
-pub fn movedim + 'static>(
- u: Rc,
+pub fn movedim>(
+ u: U,
source: usize,
destination: usize,
-) -> RightComposition {
+) -> Composition {
let ndim = u.domain().ndim();
assert!(source < ndim, "source axis {source} is out of range");
assert!(
@@ -101,12 +97,9 @@ pub fn movedim + 'static>(
/// Inserts an axis of extent 1 at `dim`, which can be anywhere from before the
/// first axis to after the last.
-pub fn unsqueeze + 'static>(
- u: Rc,
- dim: usize,
-) -> RightComposition {
- let codomain = u.hrect().clone();
- RightComposition::new(u, Rc::new(Unsqueeze::new(codomain, dim)))
+pub fn unsqueeze>(u: U, dim: usize) -> Composition {
+ let codomain = u.domain();
+ compose(u, Unsqueeze::new(codomain, dim))
}
/// The axes a function is told to act on, in place of torch's optional `dim`
@@ -149,11 +142,8 @@ 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 + 'static>(
- u: Rc,
- dims: impl Dims,
-) -> RightComposition {
- let codomain = u.hrect().clone();
+pub fn squeeze>(u: U, dims: impl Dims) -> Composition {
+ let codomain = u.domain();
let ndim = codomain.shape().len();
let named = dims.axes(ndim);
for (i, &axis) in named.iter().enumerate() {
@@ -167,37 +157,37 @@ pub fn squeeze + 'static>(
.into_iter()
.filter(|&axis| codomain.shape()[axis] == 1)
.collect();
- RightComposition::new(u, Rc::new(Squeeze::new(codomain, removed)))
+ compose(u, Squeeze::new(codomain, removed))
}
/// Joins tensors end to end along `dim`; their other extents must agree.
-pub fn concatenate(
- tensors: Vec>>,
+pub fn concatenate(
+ tensors: Vec>>,
dim: usize,
-) -> RightComposition {
+) -> Composition, Concat> {
assert!(!tensors.is_empty(), "concatenate needs at least one tensor");
let underlying = JaggedTensor::new(tensors);
- let codomain = underlying.jagged_domain().clone();
- RightComposition::new(Rc::new(underlying), Rc::new(Concat::new(codomain, dim)))
+ let codomain = underlying.domain();
+ compose(underlying, Concat::new(codomain, dim))
}
/// 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>>,
+ tensors: Vec>>,
dim: usize,
-) -> RightComposition {
+) -> Composition, Concat> {
assert!(!tensors.is_empty(), "stack needs at least one tensor");
- let shape = tensors[0].hrect().clone();
+ let shape = tensors[0].domain();
assert!(
- tensors.iter().all(|tensor| *tensor.hrect() == shape),
+ tensors.iter().all(|tensor| tensor.domain() == shape),
"stack needs tensors of one shape"
);
let unsqueezed = tensors
.into_iter()
.map(|tensor| {
- let unsqueeze = Unsqueeze::new(tensor.hrect().clone(), dim);
- Rc::new(RightComposition::new(tensor, Rc::new(unsqueeze))) as Rc>
+ let unsqueeze = Unsqueeze::new(tensor.domain(), dim);
+ Rc::new(compose(tensor, unsqueeze)) as Rc>
})
.collect();
concatenate(unsqueezed, dim)
@@ -205,61 +195,70 @@ pub fn stack(
/// 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 {
+pub fn where_(
+ condition: Rc>,
+ input: Rc>,
+ other: Rc>,
+) -> Composition, Where> {
let underlying = JaggedTensor::new(vec![input, other]);
- let codomain = underlying.jagged_domain().clone();
- RightComposition::new(
- Rc::new(underlying),
- Rc::new(Where::new(codomain, condition)),
- )
+ let codomain = underlying.domain();
+ compose(underlying, Where::new(codomain, condition))
}
/// 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 + 'static>(
- u: Rc,
+pub fn diagonalize>(
+ u: U,
rank: usize,
-) -> Sum
+) -> ReductionTensor, U, Diagonal>
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];
- Sum::new(u, Rc::new(Diagonal::new(HRect::new(vec![extent; rank]))))
+ let extent = u.domain().shape()[0];
+ let diagonal = Diagonal::new(HRect::new(vec![extent; rank]));
+ compose(Sum::new(), compose(multiset_lift(u), diagonal))
}
/// Reducing over the empty set gives the reduction's neutral element at every
/// index, so a constant tensor needs no underlying data at all.
-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))
+fn neutral>(
+ shape: Vec,
+) -> ReductionTensor, Physical, EmptyMapping> {
+ let empty = EmptyMapping::new(HRect::new(shape), HRect::new(vec![0]));
+ compose(
+ ReductionFunction::new(),
+ compose(multiset_lift(Physical::new(vec![])), empty),
+ )
}
-pub fn zeros(shape: Vec) -> Sum {
+pub fn zeros(shape: Vec) -> ReductionTensor {
neutral(shape)
}
-pub fn ones(shape: Vec) -> Product {
+pub fn ones(shape: Vec) -> ReductionTensor {
neutral(shape)
}
/// Ones down the diagonal and zeros elsewhere, over no stored data: the ones
/// are the neutral element of a product, the zeros that of a sum.
-pub fn eye_rect(rows: usize, cols: usize) -> Sum {
+pub fn eye_rect(
+ rows: usize,
+ cols: usize,
+) -> ReductionTensor, Diagonal> {
let extent = rows.min(cols);
- Sum::new(
- Rc::new(ones(vec![extent])),
- Rc::new(Diagonal::new(HRect::new(vec![rows, cols]))),
+ let diagonal = Diagonal::new(HRect::new(vec![rows, cols]));
+ compose(
+ Sum::new(),
+ compose(multiset_lift(ones(vec![extent])), diagonal),
)
}
-pub fn eye(n: usize) -> Sum {
+pub fn eye(
+ n: usize,
+) -> ReductionTensor, Diagonal> {
eye_rect(n, n)
}
@@ -267,11 +266,11 @@ 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: U,
size: Vec,
-) -> Sum
+) -> ReductionTensor, U, COO>
where
SumReduction: Reduction,
{
@@ -285,7 +284,7 @@ where
1,
"sparse_coo_tensor takes a vector of values"
);
- let count = values.hrect().shape()[0];
+ let count = values.domain().shape()[0];
for row in &indices {
assert_eq!(
row.len(),
@@ -305,80 +304,93 @@ where
"coordinate {coordinate:?} lies outside size {size:?}"
);
}
- Sum::new(values, Rc::new(COO::new(HRect::new(size), coordinates)))
+ let coo = COO::new(HRect::new(size), coordinates);
+ compose(Sum::new(), compose(multiset_lift(values), coo))
}
/// A sparse latticed tensor of shape `shape`: the value at index `x` of
/// `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 + 'static>(
- physical: Rc,
+pub fn sparse_latticed_tensor>(
+ physical: U,
basis: Vec>,
offset: Vec,
shape: Vec,
-) -> Sum
+) -> ReductionTensor, U, InvAffine>
where
SumReduction: Reduction,
{
- let affine = Affine::new(physical.hrect().clone(), basis, offset, HRect::new(shape));
- Sum::new(physical, Rc::new(InvAffine::new(affine)))
+ let affine = Affine::new(physical.domain(), basis, offset, HRect::new(shape));
+ compose(
+ Sum::new(),
+ compose(multiset_lift(physical), InvAffine::new(affine)),
+ )
}
-pub fn sum + 'static>(
- input: Rc,
+pub fn sum>(
+ input: U,
dim: usize,
-) -> Sum
+) -> ReductionTensor, U, InvProjection>
where
SumReduction: Reduction,
{
- let codomain = input.hrect().clone();
- Sum::new(input, Rc::new(InvProjection::new(codomain, dim)))
+ let rows = InvProjection::new(input.domain(), dim);
+ compose(Sum::new(), compose(multiset_lift(input), rows))
}
-pub fn prod + 'static>(
- input: Rc,
+pub fn prod>(
+ input: U,
dim: usize,
-) -> Product
+) -> ReductionTensor, U, InvProjection>
where
ProductReduction: Reduction,
{
- let codomain = input.hrect().clone();
- Product::new(input, Rc::new(InvProjection::new(codomain, dim)))
+ let rows = InvProjection::new(input.domain(), dim);
+ compose(Product::new(), compose(multiset_lift(input), rows))
}
-pub fn mm + 'static, V: BoxedTensor + 'static>(
- mat1: Rc,
- mat2: Rc,
-) -> Sum
+/// The tensors `mm` multiplies and then sums, stacked: the product of the two
+/// matrices at each `[m, n, p]`.
+type Products = ReductionTensor, Composition, Concat>, InvProjection>;
+
+pub fn mm + 'static, V: Tensor + 'static>(
+ mat1: U,
+ mat2: V,
+) -> ReductionTensor, Products, InvProjection>
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!(mat1.domain().shape().len(), 2, "mat1 must be a matrix");
+ assert_eq!(mat2.domain().shape().len(), 2, "mat2 must be a matrix");
assert_eq!(
- mat1.hrect().shape()[1],
- mat2.hrect().shape()[0],
+ mat1.domain().shape()[1],
+ mat2.domain().shape()[0],
"mat1 must have as many columns as mat2 has rows"
);
- let m = mat1.hrect().shape()[0];
- let n = mat1.hrect().shape()[1];
- let p = mat2.hrect().shape()[1];
+ let m = mat1.domain().shape()[0];
+ let n = mat1.domain().shape()[1];
+ let p = mat2.domain().shape()[1];
let latent_domain = HRect::new(vec![m, n, p]);
let stacked_latent_domain = HRect::new(vec![2, m, n, p]);
- let phi1 = Rc::new(Projection::new(latent_domain.clone(), 2)); // phi1(m, n, p) = [m, n]^T
- let phi2 = Rc::new(Projection::new(latent_domain.clone(), 0)); // phi2(m, n, p) = [n, p]^T
- let latent_inv_proj = Rc::new(InvProjection::new(stacked_latent_domain, 0)); // l(2, m, n, p) = [m, n, p]^T
- let psi_inv = Rc::new(InvProjection::new(latent_domain.clone(), 1)); // psi(m, n, p) = [m, p]^T
+ let phi1 = Projection::new(latent_domain.clone(), 2); // phi1(m, n, p) = [m, n]^T
+ let phi2 = Projection::new(latent_domain.clone(), 0); // phi2(m, n, p) = [n, p]^T
+ let latent_inv_proj = InvProjection::new(stacked_latent_domain, 0); // l(2, m, n, p) = [m, n, p]^T
+ let psi_inv = InvProjection::new(latent_domain.clone(), 1); // psi(m, n, p) = [m, p]^T
- let latent_mat1 = Rc::new(RightComposition::new(mat1, phi1)); // shape [m, n, p]
- let latent_mat2 = Rc::new(RightComposition::new(mat2, phi2)); // shape [m, n, p]
- let stacked = Rc::new(stack(vec![latent_mat1, latent_mat2], 0)); // shape [2, m, n, p]
+ let latent_mats: Vec>> = vec![
+ Rc::new(compose(mat1, phi1)), // shape [m, n, p]
+ Rc::new(compose(mat2, phi2)), // shape [m, n, p]
+ ];
+ let stacked = stack(latent_mats, 0); // shape [2, m, n, p]
- let prod_tensor = Rc::new(Product::new(stacked, latent_inv_proj)); // shape [m, n, p]
+ let prod_tensor = compose(
+ Product::new(),
+ compose(multiset_lift(stacked), latent_inv_proj),
+ ); // shape [m, n, p]
- Sum::new(prod_tensor, psi_inv) // shape [m, p]
+ compose(Sum::new(), compose(multiset_lift(prod_tensor), psi_inv)) // shape [m, p]
}
diff --git a/src/mapping.rs b/src/index_functions.rs
similarity index 75%
rename from src/mapping.rs
rename to src/index_functions.rs
index 61c0bcf..273d5ce 100644
--- a/src/mapping.rs
+++ b/src/index_functions.rs
@@ -1,104 +1,42 @@
-use std::collections::HashSet;
use std::rc::Rc;
-use crate::display::BoxedTensor;
-use crate::domain::{Domain, HRect, JaggedDomain};
+use crate::function::Function;
use crate::lattice::Preimage;
+use crate::multiset::Multiset;
+use crate::sets::{HRect, IndexSet, JaggedDomain, Multisets, Set};
use crate::signature::{HRectSignature, Signature};
+use crate::tensor::Tensor;
-/// A set of indices, as returned by a `PowersetMapping`.
-pub type Powerset = HashSet>;
+/// The signature of a multi-index function: from a hrect to the multisets of
+/// indices of another.
+pub type MultiIndexFunctionSignature = Signature>;
-/// A mapping between two index sets, from its domain to its codomain.
-pub trait IndexMapping {
- fn map(&self, index: &[usize]) -> Vec;
+/// A function on the indices of a hrect, sending each index to an index of its
+/// codomain, or, for a multi-index function, to a multiset of them.
+/// Implementing this gets `Function` for free, with the domain and codomain of
+/// the signature.
+pub trait IndexFunction {
+ type Codomain: Set;
- fn domain(&self) -> &dyn Domain;
-
- fn codomain(&self) -> &dyn Domain;
-}
-
-/// A mapping whose domain is a hrect. Composing a tensor over one of these gives
-/// a boxed tensor, hence a printable one.
-pub trait BoxedMapping: IndexMapping {
- fn hrect(&self) -> &HRect;
-}
-
-/// A mapping sending each index of its domain to a set of indices of its
-/// codomain. Note that the codomain is the set the returned indices are drawn
-/// from, not its powerset.
-pub trait PowersetMapping {
- fn map(&self, index: &[usize]) -> Powerset;
-
- fn domain(&self) -> &dyn Domain;
-
- fn codomain(&self) -> &dyn Domain;
-}
-
-/// A powerset mapping whose domain is a hrect. Reducing over one of these gives a
-/// boxed tensor, hence a printable one.
-pub trait BoxedPowersetMapping: PowersetMapping {
- fn hrect(&self) -> &HRect;
-}
-
-/// What a mapping from a hyperrectangle has to provide. Implementing this rather
-/// than `IndexMapping` gets the domain accessors and printability below for
-/// free, whatever the codomain; a mapping over a domain that is not a hrect
-/// implements `IndexMapping` directly instead, and is not printable.
-pub trait HRectMapping {
- type Codomain: Domain;
-
- fn map(&self, index: &[usize]) -> Vec;
-
- fn signature(&self) -> &Signature;
-}
-
-impl IndexMapping for M {
- fn map(&self, index: &[usize]) -> Vec {
- HRectMapping::map(self, index)
- }
-
- fn domain(&self) -> &dyn Domain {
- self.signature().domain()
- }
-
- fn codomain(&self) -> &dyn Domain {
- self.signature().codomain()
- }
-}
-
-impl BoxedMapping for M {
- fn hrect(&self) -> &HRect {
- self.signature().domain()
- }
-}
-
-/// The powerset counterpart of `HRectMapping`.
-pub trait HRectPowersetMapping {
- type Codomain: Domain;
-
- fn map(&self, index: &[usize]) -> Powerset;
+ fn map(&self, index: &[usize]) -> ::Owned;
fn signature(&self) -> &Signature;
}
-impl PowersetMapping for M {
- fn map(&self, index: &[usize]) -> Powerset {
- HRectPowersetMapping::map(self, index)
- }
+impl Function for M {
+ type Domain = HRect;
+ type Codomain = M::Codomain;
- fn domain(&self) -> &dyn Domain {
- self.signature().domain()
+ fn apply(&self, index: &[usize]) -> ::Owned {
+ self.map(index)
}
- fn codomain(&self) -> &dyn Domain {
- self.signature().codomain()
+ fn domain(&self) -> HRect {
+ self.signature().domain().clone()
}
-}
-impl BoxedPowersetMapping for M {
- fn hrect(&self) -> &HRect {
- self.signature().domain()
+ fn codomain(&self) -> M::Codomain {
+ self.signature().codomain().clone()
}
}
@@ -138,7 +76,7 @@ fn flat_extent(shape: &[usize], strides: &[usize]) -> usize {
.sum::()
}
-impl HRectMapping for Stride {
+impl IndexFunction for Stride {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -161,7 +99,7 @@ pub struct Permutation {
impl Permutation {
/// Takes the codomain and derives the domain, the other way round from the
- /// mappings above: the shape being permuted is what is known, and the
+ /// index functions above: the shape being permuted is what is known, and the
/// permuted shape follows from it. `axes[i]` is the axis of the codomain
/// that becomes axis `i` of the domain.
pub fn new(codomain: HRect, axes: Vec) -> Self {
@@ -188,7 +126,7 @@ impl Permutation {
}
}
-impl HRectMapping for Permutation {
+impl IndexFunction for Permutation {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -227,7 +165,7 @@ impl Unsqueeze {
}
}
-impl HRectMapping for Unsqueeze {
+impl IndexFunction for Unsqueeze {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -274,7 +212,7 @@ impl Squeeze {
}
}
-impl HRectMapping for Squeeze {
+impl IndexFunction for Squeeze {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -328,7 +266,7 @@ impl Concat {
}
}
-impl HRectMapping for Concat {
+impl IndexFunction for Concat {
type Codomain = JaggedDomain;
fn map(&self, index: &[usize]) -> Vec {
@@ -349,24 +287,24 @@ impl HRectMapping for Concat {
}
/// 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
+/// it does not. Unlike the index functions 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();
assert!(
- parts[1] == shape && *condition.hrect() == shape,
+ parts[1] == shape && condition.domain() == shape,
"where needs its condition and both tensors to have one shape, got {:?}, {:?} and {:?}",
- condition.hrect().shape(),
+ condition.domain().shape(),
parts[0].shape(),
parts[1].shape()
);
@@ -377,11 +315,11 @@ impl Where {
}
}
-impl HRectMapping for Where {
+impl IndexFunction for Where {
type Codomain = JaggedDomain;
fn map(&self, index: &[usize]) -> Vec {
- let part = if self.condition.get(index) { 0 } else { 1 };
+ let part = if self.condition.apply(index) { 0 } else { 1 };
let mut result = Vec::with_capacity(index.len() + 1);
result.push(part);
result.extend_from_slice(index);
@@ -394,31 +332,31 @@ impl HRectMapping for Where {
}
pub struct EmptyMapping {
- signature: HRectSignature,
+ signature: MultiIndexFunctionSignature,
}
impl EmptyMapping {
pub fn new(domain: HRect, codomain: HRect) -> Self {
Self {
- signature: HRectSignature::new(domain, codomain),
+ signature: MultiIndexFunctionSignature::new(domain, Multisets::new(codomain)),
}
}
}
-impl HRectPowersetMapping for EmptyMapping {
- type Codomain = HRect;
+impl IndexFunction for EmptyMapping {
+ type Codomain = Multisets;
- fn map(&self, _index: &[usize]) -> Powerset {
- Powerset::new()
+ fn map(&self, _index: &[usize]) -> Multiset> {
+ Multiset::default()
}
- fn signature(&self) -> &HRectSignature {
+ fn signature(&self) -> &MultiIndexFunctionSignature {
&self.signature
}
}
pub struct COO {
- signature: HRectSignature,
+ signature: MultiIndexFunctionSignature,
inverse_coords: Vec>,
}
@@ -426,7 +364,7 @@ impl COO {
pub fn new(domain: HRect, inverse_coords: Vec>) -> Self {
let codomain = HRect::new(vec![inverse_coords.len()]);
Self {
- signature: HRectSignature::new(domain, codomain),
+ signature: MultiIndexFunctionSignature::new(domain, Multisets::new(codomain)),
inverse_coords,
}
}
@@ -436,30 +374,30 @@ impl COO {
}
}
-impl HRectPowersetMapping for COO {
- type Codomain = HRect;
+impl IndexFunction for COO {
+ type Codomain = Multisets;
- fn map(&self, index: &[usize]) -> Powerset {
- let mut result = Powerset::new();
+ fn map(&self, index: &[usize]) -> Multiset> {
+ let mut result = Multiset::default();
for (j, coords) in self.inverse_coords.iter().enumerate() {
let matches = coords
.iter()
.enumerate()
.all(|(i, coord)| *coord == index[i]);
if matches {
- result.insert(vec![j]);
+ result.push(vec![j]);
}
}
result
}
- fn signature(&self) -> &HRectSignature {
+ fn signature(&self) -> &MultiIndexFunctionSignature {
&self.signature
}
}
pub struct Diagonal {
- signature: HRectSignature,
+ signature: MultiIndexFunctionSignature,
}
impl Diagonal {
@@ -468,27 +406,30 @@ impl Diagonal {
// is as long as the shortest dimension.
let extent = domain.shape().iter().copied().min().unwrap_or(0);
Self {
- signature: HRectSignature::new(domain, HRect::new(vec![extent])),
+ signature: MultiIndexFunctionSignature::new(
+ domain,
+ Multisets::new(HRect::new(vec![extent])),
+ ),
}
}
}
-impl HRectPowersetMapping for Diagonal {
- type Codomain = HRect;
+impl IndexFunction for Diagonal {
+ type Codomain = Multisets;
- fn map(&self, index: &[usize]) -> Powerset {
- let mut result = Powerset::new();
+ fn map(&self, index: &[usize]) -> Multiset> {
+ let mut result = Multiset::default();
let first = index[0];
for coord in index {
if *coord != first {
return result;
}
}
- result.insert(vec![first]);
+ result.push(vec![first]);
result
}
- fn signature(&self) -> &HRectSignature {
+ fn signature(&self) -> &MultiIndexFunctionSignature {
&self.signature
}
}
@@ -516,7 +457,7 @@ impl Projection {
}
}
-impl HRectMapping for Projection {
+impl IndexFunction for Projection {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -533,7 +474,7 @@ impl HRectMapping for Projection {
/// The inverse of the projection dropping axis `dim`.
/// e.g. with dim=1, f(m, p) = {[m n p]^T | n in \[N\]}
pub struct InvProjection {
- signature: HRectSignature,
+ signature: MultiIndexFunctionSignature,
dim: usize,
}
@@ -548,17 +489,20 @@ impl InvProjection {
let mut shape = codomain.shape().to_vec();
shape.remove(dim);
Self {
- signature: HRectSignature::new(HRect::new(shape), codomain),
+ signature: MultiIndexFunctionSignature::new(
+ HRect::new(shape),
+ Multisets::new(codomain),
+ ),
dim,
}
}
}
-impl HRectPowersetMapping for InvProjection {
- type Codomain = HRect;
+impl IndexFunction for InvProjection {
+ type Codomain = Multisets;
- fn map(&self, index: &[usize]) -> Powerset {
- (0..self.signature.codomain().shape()[self.dim])
+ fn map(&self, index: &[usize]) -> Multiset> {
+ (0..self.signature.codomain().set().shape()[self.dim])
.map(|coord| {
let mut result = index.to_vec();
result.insert(self.dim, coord);
@@ -567,7 +511,7 @@ impl HRectPowersetMapping for InvProjection {
.collect()
}
- fn signature(&self) -> &HRectSignature {
+ fn signature(&self) -> &MultiIndexFunctionSignature {
&self.signature
}
}
@@ -626,7 +570,7 @@ impl Affine {
}
}
-impl HRectMapping for Affine {
+impl IndexFunction for Affine {
type Codomain = HRect;
fn map(&self, index: &[usize]) -> Vec {
@@ -658,7 +602,7 @@ impl HRectMapping for Affine {
/// `basis·x = index - offset`, so a read costs about as much as the basis is
/// large, however large the tensors.
pub struct InvAffine {
- signature: HRectSignature,
+ signature: MultiIndexFunctionSignature,
offset: Vec,
preimage: Preimage,
}
@@ -672,9 +616,9 @@ impl InvAffine {
} = affine;
let preimage = Preimage::new(&basis, signature.domain().shape());
Self {
- signature: HRectSignature::new(
+ signature: MultiIndexFunctionSignature::new(
signature.codomain().clone(),
- signature.domain().clone(),
+ Multisets::new(signature.domain().clone()),
),
offset,
preimage,
@@ -682,10 +626,10 @@ impl InvAffine {
}
}
-impl HRectPowersetMapping for InvAffine {
- type Codomain = HRect;
+impl IndexFunction for InvAffine {
+ type Codomain = Multisets;
- fn map(&self, index: &[usize]) -> Powerset {
+ fn map(&self, index: &[usize]) -> Multiset> {
let target: Vec = index
.iter()
.zip(&self.offset)
@@ -694,7 +638,7 @@ impl HRectPowersetMapping for InvAffine {
self.preimage.solve(&target).into_iter().collect()
}
- fn signature(&self) -> &HRectSignature {
+ fn signature(&self) -> &MultiIndexFunctionSignature {
&self.signature
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 638af52..89ab9db 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,27 +2,32 @@
//! compositions or functional transforms.
pub mod display;
-pub mod domain;
+pub mod function;
pub mod functions;
+pub mod index_functions;
mod lattice;
-pub mod mapping;
+pub mod multiset;
+pub mod reductions;
+pub mod sets;
pub mod signature;
pub mod tensor;
-pub use display::{BoxedTensor, format_boxed};
-pub use domain::{Domain, HRect, JaggedDomain};
+pub use display::format_boxed;
+pub use function::{Composition, Function, MultiFunction, ValueFunction, compose, multiset_lift};
pub use functions::{
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::{
- Affine, BoxedMapping, BoxedPowersetMapping, COO, Concat, Diagonal, EmptyMapping, IndexMapping,
- InvAffine, InvProjection, Permutation, Powerset, PowersetMapping, Projection, Squeeze, Stride,
- Unsqueeze, Where,
+pub use index_functions::{
+ Affine, COO, Concat, Diagonal, EmptyMapping, IndexFunction, InvAffine, InvProjection,
+ MultiIndexFunctionSignature, Permutation, Projection, Squeeze, Stride, Unsqueeze, Where,
};
-pub use signature::{HRectSignature, Signature};
-pub use tensor::{
- Inf, InfReduction, JaggedTensor, LeftComposition, Physical, Product, ProductReduction,
- Reduction, ReductionTensor, RightComposition, Sum, SumReduction, Sup, SupReduction, Tensor,
+pub use multiset::Multiset;
+pub use reductions::{
+ Inf, InfReduction, Product, ProductReduction, Reduction, ReductionFunction, Sum, SumReduction,
+ Sup, SupReduction,
};
+pub use sets::{HRect, IndexSet, JaggedDomain, Multisets, Set, Values};
+pub use signature::{HRectSignature, Signature};
+pub use tensor::{JaggedTensor, Physical, ReductionTensor, Tensor};
diff --git a/src/main.rs b/src/main.rs
index b8d88bc..1b89d3b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,8 @@
use std::rc::Rc;
use cten::{
- COO, HRect, Physical, Product, RightComposition, Stride, Sum, Sup, Tensor, diagonalize, eye,
- movedim, negate, ones, square, transpose, zeros,
+ COO, Function, HRect, Physical, Product, Stride, Sum, Sup, compose, diagonalize, eye, movedim,
+ multiset_lift, negate, ones, square, transpose, zeros,
};
fn main() {
@@ -27,12 +27,16 @@ fn main() {
// Tensors
let physical = Rc::new(Physical::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
- let strided = Rc::new(RightComposition::new(physical.clone(), stride));
- let sparse_coo_vector = Rc::new(Sum::new(physical.clone(), coo_vector));
- let repeated_sparse = Rc::new(RightComposition::new(sparse_coo_vector.clone(), repeat));
- let sparse_coo = Rc::new(Sum::new(physical.clone(), coo.clone()));
- let prod_coo = Rc::new(Product::new(physical.clone(), coo.clone()));
- let sup_coo = Rc::new(Sup::new(physical.clone(), coo));
+ let lifted = Rc::new(multiset_lift(physical.clone()));
+ let strided = Rc::new(compose(physical.clone(), stride));
+ let sparse_coo_vector = Rc::new(compose(Sum::new(), compose(lifted.clone(), coo_vector)));
+ let repeated_sparse = Rc::new(compose(sparse_coo_vector.clone(), repeat));
+ let sparse_coo = Rc::new(compose(Sum::new(), compose(lifted.clone(), coo.clone())));
+ let prod_coo = Rc::new(compose(
+ Product::new(),
+ compose(lifted.clone(), coo.clone()),
+ ));
+ let sup_coo = Rc::new(compose(Sup::new(), compose(lifted, coo)));
let diag2d = Rc::new(diagonalize(physical.clone(), 2));
let diag3d = Rc::new(diagonalize(physical.clone(), 3));
let identity = Rc::new(eye(6));
@@ -64,7 +68,7 @@ fn main() {
"strided({}, {}) = {}",
index[0],
index[1],
- strided.get(&index)
+ strided.apply(&index)
);
println!("\ntransposed:\n{transposed}");
diff --git a/src/multiset.rs b/src/multiset.rs
new file mode 100644
index 0000000..6f0956b
--- /dev/null
+++ b/src/multiset.rs
@@ -0,0 +1,74 @@
+//! Multisets: collections in which an element may appear more than once, and
+//! in which the multiplicity of each element counts but the order does not.
+
+use std::fmt;
+use std::fmt::Display;
+
+/// An unordered collection of elements, repeats included. It is stored as a
+/// vector, whose order carries no meaning.
+#[derive(Clone, Debug)]
+pub struct Multiset {
+ elements: Vec,
+}
+
+impl Multiset {
+ pub fn new(elements: Vec) -> Self {
+ Self { elements }
+ }
+
+ pub fn push(&mut self, element: A) {
+ self.elements.push(element);
+ }
+
+ pub fn iter(&self) -> std::slice::Iter<'_, A> {
+ self.elements.iter()
+ }
+
+ pub fn len(&self) -> usize {
+ self.elements.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.elements.is_empty()
+ }
+}
+
+// Written out rather than derived, which would require `A: Default`.
+impl Default for Multiset {
+ fn default() -> Self {
+ Self::new(Vec::new())
+ }
+}
+
+impl FromIterator for Multiset {
+ fn from_iter>(iter: I) -> Self {
+ Self::new(iter.into_iter().collect())
+ }
+}
+
+impl IntoIterator for Multiset {
+ type Item = A;
+ type IntoIter = std::vec::IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.elements.into_iter()
+ }
+}
+
+impl<'a, A> IntoIterator for &'a Multiset {
+ type Item = &'a A;
+ type IntoIter = std::slice::Iter<'a, A>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.elements.iter()
+ }
+}
+
+/// Written as in the paper, with double braces: `{{1, 3, 3}}`. Padded as a whole,
+/// so that multisets line up in a printed tensor as numbers do.
+impl Display for Multiset {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let elements: Vec