Tensors

The name "Tensorics" is derived from "Tensor" which in mathematics - loosely spoken - represents a multidimensional data structure whose entries are adressed by indizes. In tensorics, we use the term "Tensor" in an even sloppier manner. The main particularity of a tensorics tensor is that a dimension is identified by a java type (class). Instances of the respective type we denote as coordinates. A value of the tensor is then adressed within the N-dimensional coordinate space by a set of objects (instances of coordinate classes).

A tensorics tensor has one type parameter, the type of the values it contains, usually denoted as <V>. Therefore, the tensor data structure can be used as container for any Java type. However, some operations on the tensors will be only possible for certain value types (e.g. mathematical operations).

An Example

Since tensorics concepts and syntax are best explained in a practical walk-through, we will use the following example throughout the subsequent sections:

Consider weather analysis: A data set consists of weather data from different cities and times. The class City and Time are defined and some constants are instantiated. Temperature values are stored in a tensor of doubles, for example:

City SF = City.ofName("San Francisco");
City LA = City.ofName("Los Angeles");

Time T1 = Time.of("2017-01-01 15:00");
Time T2 = Time.of("2017-01-02 15:00");

Tensor<Double> degrees;
/* creation omitted */

Accessing Values

Assuming the above constants, we can then simply get temperature values from the tensor:

Double t = degrees.get(T1, SF);

As visible here, this looks very similar to getting values from a map, with the following important differences:

  • The get method of a tensor accepts N arguments, one for each dimension.

  • The get method of a tensor never returns null. It will throw an appropriate exception in case there is no value available in the tensor for the given set of coordinates.

In general, it shall be noted that all methods within the tensorics library are designed to fail fast. This is particularly important because \tensorics{}, due to its flexible API, cannot rely on compile-time checks in many cases and thus some errors only appear at runtime.

The set of N coordinates is called a position in tensorics. Thus, the code from the above listing is equivalent to

Position position = Position.of(T1, SF);
Double t = degrees.get(position);

Main Entry Point

The interfaces of tensorics objects are kept very slim and usually only provide the absolutely necessary methods. All the other operations on these objects is based on static methods operating on them. The main entry point for these methods (containing all the methods which are not specific to certain value types) is the class \code{Tensorics}. This class contains also, for example, a delegation method to the \code{Position.of()} method:

Position position = Tensorics.at(T1, SF);
/* with static import: */
Position position = at(T1, SF);

Using a static import for this, allows concise code which will be particularly important when creating tensors.

In all the following code examples, we assume that, whenever there is a plain method call, then it is a static method from the Tensorics class (or in other words that Tensorics. is imported statically).

Creating Tensors

All currently available implementations of tensors are immutable. The usual way to create them is through builders. For example, to create our temperature tensor and put 4 values into it, we would have to do something like:

Tensor<Double> degrees =
    builder(City.class, Time.class)
         .put(at(SF, T1), 12.5)
         .put(at(SF, T2), 14.2)
         .put(at(LA, T1), 17.5)
         .put(at(LA, T2), 19.2)
         .build();

Again, the syntax is very similar to building an immutable map. And indeed this is another way how a \tensorics{} tensor can be seen: As a map from position to a value - and it can be transformed into one:

Map<Position, Double> degreesMap = mapFrom(degrees);

Scalar

A tensor can have zero dimensions. This particular tensor we denote as scalar in \tensorics{}. It has exactly one value at the position Position.empty(). A scalar can simply be created using the static factory method

Scalar<Double> scalar = scalarOf(2.5);

Shape

Up to now, we were simply using a tensor as a kind-of map with combined keys. However, the real power is unleashed only when it comes to transformations. As a precondition of this, it is important to introduct the concept of the tensors Shape: Just like a map has its set of keys, a tensorics tensor has a shape. It basically describes the structure of the tensor, without its values. Basically it contains the following information: - The dimensions of the tensor (e.g. Time.class and City.class in the above example) and - The available positions in the tensor.

The shape can be retrieved from the tensor and used for our example like the following:

Shape shape = degrees.shape();

Set<Class<?>> dims = shape.dimensionSet();
/* Contains Time.class and City.class */

int dim = shape.dimensionality();
/* Will be 2 */

Set<Position> poss = shape.positionSet();
/* contains the 4 positions */

int size = shape.size();
/* Will be 4 */