Shaped arrays in TiPi

A ShapedArray stores rectangular multi-dimensional arrays of elements of the same data type. Compared to a ShapedVector, the elements of a ShapedArray reside in conventional memory and may be stored in arbitrary order and in a non-contiguous way.

Class hierarchy

Various interfaces or abstract classes extend the ShapedArray interface depending on whether the type[1] and/or the rank[2] of the array are unveiled. The naming conventions are:

ShapedArray   // neither rank nor type is known
<Type>Array   // a shaped array of known type, but any rank
Array<Rank>D  // a shaped array of known rank, but any type
<Type><Rank>D // a shaped array with known type and rank

where the <Type> field is the capitalized name of the primitive type (e.g. Double for double, Int for int, etc.) and <Rank> is a decimal number in the range 1-9. Arrays representating scalars are also needed, as 0D is a bit odd for 0-rank (that is scalar) object it is replaced by the substantive Scalar in the rules above for 0-rank arrays and a Scalar denotes a scalar of any type. For instance:

All these are abstract classes or interfaces, the actual class depends on the storage of the elements of the array. Most of the time, an array can be manipulated regardless of its concrete class.

Operations on shaped arrays

This section describes the operations available for the most basic type, that is ShapedArray.

Basic operations on shaped arrays

The basic operations available for a ShapedArray, say arr, are the same as those of Shaped and Typed objects:

arr.getType()        // query the type of the array
arr.getRank()        // query the rank of the array
arr.getNumber()      // query the number of elements in the array
arr.getDimension(k)  // query the length of the (k+1)-th dimension
arr.getOrder()       // query the storage order of the elements in the array
arr.getShape()       // query the shape of the array

and type conversion:

arr.toByte()         // convert to ByteArray
arr.toShort()        // convert to ShortArray
arr.toInt()          // convert to IntArray
arr.toLong()         // convert to LongArray
arr.toFloat()        // convert to FloatArray
arr.toDouble()       // convert to DoubleArray

For efficiency reasons, conversion operations are lazzy: they return the same object if it is already of the correct type. Use the copy method to duplicate an array:

arr.copy()           // duplicate the contents of the array

This method yields a new shaped array which has the same shape, type and values as arr but whose contents is independent from that of arr. If arr is a view, then the copy method yields a compact array in a flat[3] form.

The elements of a shaped array are generic Java data, their type, as given by arr.getType(), can be one of:

The storage order, as given by arr.getOrder(), indicates how to travel the elements of the array for maximum efficiency. It can be one of:

Assign elements of a shaped array

It is possible to assign the values of an array from another object:

arr.assign(src)

where src is another shaped array, a shaped vector, or a Java array of a primitive type. If the type of the elements of src does not match that of the elements of arr, conversion is automatically and silently performed. If src is a Shaped object, its shape must however match that of arr. If src is a Java array, it must have as many elements as arr. The assign operation is optimized: if arr and src share their contents with the same storage, nothing is done as nothing has to be done. If arr and src share their contents in a different form, the result is unpredictable.

When the type and dimensions of a shaped array are unveiled by its class, individual elements become accessible via its get(...) and set(...) methods.

Create a similar shaped array

Creating a new array with the same type and shape as arr is simply done by:

arr.create()

which yields a flat array whose elements are contiguous and stored in column-major order. The ArrayFactory provides static methods to create shaped arrays of any given type and shape (providing the type of its elements is known).

1D view of a shaped array

Finally it is possible to view a shaped array as if it was a mono-dimensional (1-D) array. This is done by:

arr.as1D()

Since the rank is unveiled by this operation, it is available for any ShapedArray and yields an instance of the abstract class Array1D. Note that a view such as the one returned by the as1D() method let you fetch and assign values of the original array with get(...) and set(...) methods.

Operations available for arrays with unveiled type

Add/subtract/multiply all elements by a scalar:

arr.increment(value)
arr.decrement(value)
arr.scale(value)

Fill with given or generated value:

arr.fill(value)
arr.fill(generator)

Map a function to every elements:

arr.map(func)

Scan all the elements of the array:

arr.scan(scanner)

Extract the contents as a flat Java vector:

arr.flatten([forceCopy])

Other operations

Fetch a value:

arr.get(i1,...,iR)

element type and rank must be known so arr is an instance of some <Type><Rank>D abstract class.

Assign a value to an element:

arr.set(i1,...,iR, value)

Operations available for arrays with unveiled rank

Slicing

arr.slice(index[, dim])

By default dim = LAST (-1) to slice through the last dimension, result is an array of same data type with one less dimension (slicing a Float3D yields a Float2D, slicing a Double1D yields a DoubleScalar, etc.). The operation is fast as it returns a view on arr. A slice shares its contents with its parent. The same rules as for ranges (see this section) apply to index and dim which can have a negative value understood as being relative to the end.

Ranged sub-array

You can make a sub-array for given ranges of indices:

arr.view(rng1, rng2, ..., rngR)

which returns a view to arr limited to the elements indexed by the ranges rng* are either Range objects or null which means "all".

A range is a construction like:

Range rng = new Range(start, stop);

or:

Range rng = new Range(start, stop, step);

where start is the initial index of the range, stop is the last index and step is optional and defaults to +1 if omitted.

As in Java, indexes of a range start at 0 (also Range.FIRST) but may be negative. Starting/ending indexes of a range are interpreted as offsets relative to the length of the dimension of interest if they are strictly negative. That is -1 (also Range.LAST) is the last element along a dimension, -2 is the penultimate element, etc. This rule is however only applied once: if j is a start/stop index, then j (if j >= 0) or j+n (if j < 0) must be greater or equal 0 and strictly less than n, the length of the dimension of interest.

The convention is that the element corresponding to the first index of a range is always considered while the last one may not be reached. In pseudo-code, the rules are:

if (step > 0) {
    for (int index = first; index <= last; index += step) {
        ...
    }
} else if (step < 0) {
    for (int index = first; index >= last; index += step) {
        ...
    }
}

The sign of the step must be in agreement with the ordering of the first and last element of the range, otherwise the range may be empty. This occurs when (after applying the rules for negative indices) first > last and step > 0 and when first < last and step < 0.

To ease the construction of ranges and make the code more readable, we recommend to introduce a range factory in your class, e.g.:

static public Range range(int first, int last) {
    return new Range(first, last);
}
static public Range range(int first, int last, int step) {
    return new Range(first, last, step);
}

Then a ranged view of an array can be written as:

arr.view(range(0,-1,2), range(4,8))

which is lighter and more readable than:

arr.view(new Range(0,-1,2), new Range(4,8))

Selected sub-array

Likewise:

arr.view(sel1, sel2, ..., selR)

returns a view to arr from a selection of indices, all sel* arguments are either int[] index list or null which means all. Beware that in selections of indices, all indices must be reachable (-1 is considered as out-of-bounds).

To ease the construction of such views and make the code more readable, you can also use selection factories like:

static public int[] select(int i1) {
    return new int[]{i1};
}
static public int[] select(int i1, int i2) {
    return new int[]{i1, i2};
}
static public int[] select(int i1, int i2, int i3) {
    return new int[]{i1, i2, i3};
}
...

and write something like:

arr.view(select(3,2,9), select(5,1))

Note that sub-arrays and slices are views to the original array (which can itself be a view) with fast access to the elements of this array. If you do not want that the view and its parent share the same elements, use the copy() method after making the view.

It is currently not possible to mix ranges, slicing and index selection in a single sub-array construction, as it would result in too many possibilities (and potentially inefficient code). It is however possible to chain sub-array-constructions to obtain the same effect. For instance:

arr.view(null, range(2,11,3)).view(select(6,4,8,2), null)

to mimic:

arr.view(select(6,4,8,2), range(2,11,3))

which is not allowed for now.

[1] Type: The primitive type of the elements of a Typed object.

[2] Rank: The number of dimensions of a multi-dimensional (a.k.a. shaped) object.

[3] Flat: Zero-offset, contiguous storage in column-major order.