Tensor<Double> sfDegrees = from(degrees).extract(SF);
One very common structural operation is extracting sub-tensors from a tensor:
Tensor<Double> sfDegrees = from(degrees).extract(SF);
This will result in a 1-dimensional tensor, only containing coordinates of type Time
. The complementary operation to this is called merging tensors.
\textbf{Note:} while in the get
method, the number of coordinates always has to exactly match the dimensionality of the tensor (otherwise the method will throw), the \code{extract} method takes any subset of the dimensions as argument; the \code{get} method returns the values of the tensor, while the \code{extract} method returns again a tensor. This implies that if coordinates for all dimensions are provided as arguments for the extract method, then a zero-dimensional tensor is returned. The returned tensor can be empty in case no elements exist at the extracted coordinates.
One important motivation to use tensors is of course to have simple and intuitive ways to perform mathematical operations on them. While the structural operations - as described up to now - can be performed on tensors of any value types, it is clear that mathematical operations can be only done with tensor values of particular types.
Tensorics does not strictly restrict the types on which mathematical operations can be performed, but provides an extension mechanism through which - in principle - the mathematical capabilities can be added for any value type. In practice this makes only sense (and is only necessary) for a limited number of value types. The extension mechanism requires to provide (with $a,b,c$ being tensor values):
Two binary operations, addition ( + ) and multiplication ( * ) with the following properties:
both, + and * are associative: $a + (b + c) = (a + b) + c$; $a * (b * c) = (a * b) * c$.
both, + and * have an identity element (Called '0' for +, '1' for * ): $a + 0 = a$; $a * 1 = a$.
both, + and * have an inverse element (Called '-a' for +, '1/a' for * ): $a + (-a) = 0$; $a * 1/a = 1$.
both, + and * are commutative: $a + b = b + a$; $a * b = b * a$.
* is distributive over +: $a * (b + c) = a * b + a * c$.
Mathematically speaking, the two operations form the algebraic structure of a field \cite{wikipedia-field} over the tensor values <V>
:
---
* Two additional binary operations: Power ($a^b$) and Root ($\sqrt[b]{a}$).
* A conversion function of the tensor values to and from doubles.
---
If these operations are provided to generic support classes of \tensorics{}, then all the manipulations based in the following will be available by inheriting from these support classes. The biggest advantage of the approach used in tensorics for defining a field (and using external methods for calculations - not methods of the field elements) is that it (technically) does not impose any constraints on the value type and thus avoids e.g. wrapper objects as necessary in the field-implementations of other math libraries (e.g. Apache Commons Math \cite{apache-commons-math}).
Out of the box, tensorics currently provides an implementation of these requirements for doubles. To simplify these very frequently required operations, it provides also a convenience class (\code{TensoricsDoubles}) with static delegation methods to the support classes. Such convenience will not be available out of the box for custom value types, but can be easily added in a similar way. Whenever there is trailing method call in the following examples, we will assume that it is a static method from the class \code{TensoricDoubles}.
Next to operations on tensors, the support classes also provide convenience operations for iterables. For example:
Iterable<Double> v = Arrays.asList(1.0, 2.0);
Iterable<Double> negv = negativeOf(v);
Double vsize = sizeOf(v);
Tensor<Double> t; /* creation omitted */
Tensor<Double> negt = negativeOf(t);
Double tsize = sizeOf(t);
Some very simple statistical methods are provided out of the box. For iterables, the results are simply of type of the elements of the iterable:
Iterable<Double> v = Arrays.asList(1.0, 2.0);
Double avg = averageOf(v);
Double sum = sumOf(v);
Double rms = rmsOf(v);
On the other hand, for tensors the application of statistical operations is usually done only in one dimension. This corresponds to a reduction of the tensor by one dimension. The provided fluent API reflects this (continuing our example from before):
/* All these return Tensor<Double>: */
reduce(degrees).byAveragingOver(Time.class);
reduce(degrees).byRmsOver(Time.class);
reduce(degrees).bySummingOver(Time.class);
Calculating of operations between two tensors, finally makes the most use. These operations all start using the \code{TensoricDoubles.calculate(…)} method:
/* degrees and offset are Tensor<Double> */
calculate(degrees).plus(offset);
calculate(degrees).minus(offset);
calculate(degrees).elementTimes(other);
calculate(degrees).elementDividedBy(other);
/* All these return Tensor<Double> */
Here both, the left and right operands are assumed to be tensors. However, bare values are also supported on both sides and will be implicitly be converted to scalars. The four above-mentioned operations are the simplest ones, as they are based on element wise operations: Each element in the left tensor only requires the corresponding element in the right tensor to produce the corresponding element in the resulting tensor. However, this needs some other considerations: What happens if the two operands have different shapes? This problem can be treated in two stages, which are called \emph{broadcasting} and \emph{reshaping} in \tensorics{}. They are explained in the following two sections. \Tensorics{} has a very modular way to treat such cases: Different strategies can be used (and even implemented) by the user in special cases. If nothing is specified, a sensitive default will be used.
This is the simpler of the two possible shape-inconsistencies: It means that both tensors in question have the same dimensions, but they have values for different positions (e.g. one has less entries than the other). The default behaviour for this case is, that the resulting tensor will have only values for the positions, which are contained in each of the tensor (The intersection of the position set).
The term \emph{broadcasting} is borrowed from the python library \emph{numpy} \cite{numpy-github}. While the underlaying principle is very similar to the numpy one, there are several essential difference which comes from the fact that numpy uses multi-dimensional arrays with integer indices, while tensorics identifies its dimensions by classes: The default broadcasting strategy in \tensorics{} broadcasts all dimensions which are \emph{not} available in one tensor to the shape of the second tensor. In other words, a dimension which is not present in one, will be added to the other tensor and all coordinate values of the respective dimension will potentially be combined with all the positions of the other tensor. For example:
Tensor<Double> temps =
builder(Time.class)
.put(at(T1), 10.5)
.put(at(T2), 12.2)
.build();
Tensor<Double> offsets =
builder(City.class)
.put(at(SF), 2.0)
.put(at(LA), 7.0)
.build();
Tensor<Double> result = calculate(temps).elementTimes(factors);
/* Will contain 4 positions: (SF, T1), (SF, T2), (LA, T1), (LA, T2) */
The result will be exactly the same tensor as constructed in \lstref{buildingATensor}. When performing binary operations, the two operands are first both broadcasted and then reshaped. This ensures that the dimensions are correct and then that all the relevant elements operate on their corresponding partners.
This very particular multiplication of two tensors is basically the generalization of the matrix multiplication. The syntax is as simple as it can be:
calculate(degrees).times(other);
To have this yield the expected results, co- and contra-variant dimensions have to be distinguished. In \tensorics{}, this distinction is achieved by the following mechanism: By default, coordinates are assumed to be contravariant. Covariant coordinates are forced to inherit from the class \code{Covariant<C>}, where the generic parameter \code{<C>} is the type of the corresponding contravariant coordinate. Detailed information about this can be found in the respective