The phi.math
module provides abstract access to tensor operations.
It internally uses NumPy/SciPy, TensorFlow or PyTorch to execute the actual operations, depending on which backend is selected (see below).
This ensures that code written against phi.math
functions produces equal results on all backends.
To that end, phi.math
provides a new Tensor
class which should be used instead of directly accessing native tensors from NumPy, TensorFlow or PyTorch.
While similar to the native tensor classes, phi.math.Tensor
s have named and typed dimensions.
When performing operations such as +, -, *, /, %, **
or calling math
functions on Tensor
s, dimensions are matched by name and type.
This eliminates the need for manual reshaping or the use of singleton dimensions.
from phi import math
The shape of a Tensor
is represented by a Shape
object which can be accessed as tensor.shape
.
In addition to the dimension sizes, the shape also stores the dimension names which determine their types.
There are four types of dimensions
Dimension type | Description | Examples |
---|---|---|
spatial |
Spans a grid with equidistant sample points. | x , y , z |
channel |
Set of properties sampled at per sample point per instance. | vector , color |
instance |
Collection of (interacting) objects belonging to one instance. | points , particles |
batch |
Lists non-interacting instances. | batch , frames |
The default dimension order is (batch, instance, channel, spatial)
.
When a dimension is not present on a tensor, values are assumed to be constant along that dimension.
Based on these rules rule, operators and functions may add dimensions to tensors as needed.
Many math functions handle dimensions differently depending on their type, or only work with certain types of dimensions.
Batch dimensions are ignored by all operations. The result is equal to calling the function on each slice.
Spatial operations, such as spatial_gradient()
or divergence()
operate on spatial dimensions by default, ignoring all others.
When operating on multiple spatial tensors, these tensors are typically required to have the same spatial dimensions, else an IncompatibleShapes
error may be raised.
The function join_spaces()
can be used to add the missing spatial dimensions so that these errors are avoided.
Operation | Batch | instance | Spatial | Channel |
---|---|---|---|---|
convolve | - | - | ★ | ⟷ |
nonzero | - | ★/⟷ | ★/⟷ | ⟷ |
scatter (grid) scatter (indices) scatter (values) |
- - - |
🗙 ⟷ ⟷ |
★ 🗙 🗙 |
- ⟷/🗙 - |
gather/sample (grid) gather/sample (indices) |
- - |
🗙 - |
★/⟷ - |
- ⟷/🗙 |
In the above table, -
denotes batch-type dimensions, 🗙 are not allowed, ⟷ are reduced in the operation, ★ are active
The preferred way to define a Shape
is via the shape()
function.
It takes the dimension sizes as keyword arguments.
from phi.math import batch, spatial, instance, channel
channel(vector='x,y')
(vectorᶜ=x,y)
batch(examples=10)
(examplesᵇ=10)
spatial(x=4, y=3)
(xˢ=4, yˢ=3)
instance(points=5)
(pointsⁱ=5)
Shape
objects should be considered immutable.
Do not change any property of a Shape
directly.
Important Shape
properties (see the API documentation for a full list):
.sizes: tuple
enumerates the sizes as ints or None, similar to NumPy's shapes..names: tuple
enumerates the dimension names..rank: int
or len(shape)
number of dimensions..batch, .spatial, .instance, .channel: Shape
or math.batch(shape)
Filter by dimension type..non_batch: Shape
etc. Filter by dimension type..volume
number of elements a tensor of this shape contains.Important Shape
methods:
get_size(dim)
returns the size of a dimension.get_item_names(dim)
returns the names given to slices along a dimension.without(dims)
drops the specified dimensions.only(dims)
drops all other dimensions.Additional tips and tricks
'x' in shape
tests whether a dimension by the name of 'x' is present.shape1 == shape2
tests equality including names, types and order of dimensions.shape1 & shape2
or math.merge_shapes()
combines the shapes.The tensor()
function
converts a scalar, a list
, a tuple
, a NumPy array or a TensorFlow/PyTorch tensor to a Tensor
.
The dimension names can be specified using the names
keyword and dimension types are inferred from the names.
Otherwise, they are determined automatically.
math.tensor((1, 2, 3))
(1, 2, 3) int64
import numpy
math.tensor(numpy.zeros([1, 5, 4, 2]), batch('batch'), spatial('x,y'), channel(vector='x,y'))
(batchᵇ=1, xˢ=5, yˢ=4, vectorᶜ=x,y) float64 const 0.0
math.reshaped_tensor(numpy.zeros([1, 5, 4, 2]), [batch(), *spatial('x,y'), channel(vector='x,y')])
(xˢ=5, yˢ=4, vectorᶜ=x,y) float64 const 0.0
There are a couple of functions in the phi.math
module for creating basic tensors.
Most functions allow the shape of the tensor to be specified via a Shape
object or alternatively through the keyword arguments.
In the latter case, the dimension types are inferred from the names.
math.zeros(spatial(x=5, y=4))
(xˢ=5, yˢ=4) const 0.0
math.random_uniform(channel(vector='x,y'))
(x=0.005, y=0.725)
math.random_normal(batch(examples=6), dtype=math.DType(int, 32))
(0, 0, 0, 0, -1, 0) along examplesᵇ
The phi.math
library does not implement basic operators directly but rather delegates the calls to another computing library.
Currently, it supports three such libraries: NumPy/SciPy, TensorFlow and PyTorch.
These are referred to as backends.
The easiest way to use a certain backend is via the import statement:
phi.flow
→ NumPy/SciPyphi.tf.flow
→ TensorFlowphi.torch.flow
→ PyTorchphi.jax.flow
→ JaxThis determines what backend is used to create new tensors. Existing tensors created with a different backend will keep using that backend. For example, even if TensorFlow is set as the default backend, NumPy-backed tensors will continue using NumPy functions.
The global backend can be set directly using math.backend.set_global_default_backend()
.
Backends also support context scopes, i.e. tensors created within a with backend:
block will use that backend to back the new tensors.
The three backends can be referenced via the global variables phi.math.NUMPY
, phi.tf.TENSORFLOW
and phi.torch.TORCH
.
When passing tensors of different backends to one function, an automatic conversion will be performed, e.g. NumPy arrays will be converted to TensorFlow or PyTorch tensors.
from phi.math import backend
backend.default_backend()
NumPy
from phi.torch import TORCH
with TORCH:
print(math.zeros().default_backend)
PyTorch
backend.set_global_default_backend(backend.NUMPY)
Indexing is read-only. The recommended way of indexing or slicing tensors is using the syntax
tensor.<dim>[start:end:step]
where start >= 0
, end
and step > 0
are integers.
The access tensor.<dim>
returns a temporary TensorDim
object which can be used for slicing and unstacking along a specific dimension.
This syntax can be chained to index or slice multiple dimensions.
data = math.random_uniform(spatial(x=10, y=10, z=10), channel(vector='x,y,z'))
data.x[0].y[1:-1].vector['x']
(yˢ=8, zˢ=10) 0.488 ± 0.288 (3e-03...1e+00)
Alternatively tensors can be indexed using a dictionary of the form tensor[{'dim': slice or int}]
.
data[{'x': 0, 'y': slice(1, -1), 'vector': 'x'}]
(yˢ=8, zˢ=10) 0.488 ± 0.288 (3e-03...1e+00)
Dimensions can be iterated over or unstacked.
for slice in data.x:
print(slice)
(yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.488 ± 0.289 (2e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.539 ± 0.291 (5e-04...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.468 ± 0.290 (1e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.507 ± 0.282 (3e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.499 ± 0.288 (8e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.525 ± 0.291 (9e-04...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.501 ± 0.290 (3e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.506 ± 0.289 (2e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.500 ± 0.293 (2e-03...1e+00) (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.482 ± 0.285 (3e-04...1e+00)
tuple(data.x)
((yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.488 ± 0.289 (2e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.539 ± 0.291 (5e-04...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.468 ± 0.290 (1e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.507 ± 0.282 (3e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.499 ± 0.288 (8e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.525 ± 0.291 (9e-04...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.501 ± 0.290 (3e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.506 ± 0.289 (2e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.500 ± 0.293 (2e-03...1e+00), (yˢ=10, zˢ=10, vectorᶜ=x,y,z) 0.482 ± 0.285 (3e-04...1e+00))
The math
package allows tensors of varying sizes to be stacked into a single tensor.
This tensor then has dimension sizes of type Tensor
where the source tensors vary in size.
One use case of this are StaggeredGrid
s where the tensors holding the vector components have different shapes.
t0 = math.zeros(spatial(a=4, b=2))
t1 = math.ones(spatial(b=2, a=5))
stacked = math.stack([t0, t1], channel('c'))
stacked
(aˢ=(4, 5) along cᶜ, bˢ=2, cᶜ=2) const 0.5555555820465088
stacked.shape.is_uniform
False
The package phi.math
provides a custom DataType
class that can be used with all backends.
There are no global variables for common data types; instead you can create one by specifying the kind and length in bits.
from phi.math import DType
DType(float, 32)
float32
DType(complex, 128)
complex128
DType(bool)
bool8
By default, floating point operations use 32 bit (single precision). This can be changed globally using math.set_global_precision(64)
or locally using with math.precision(64):
.
This setting does not affect integers. To specify the number of integer bits, use math.to_int()
or cast the data type directly using math.cast()
.
Tensors can be printed in a variety of ways. These options can be specified in the format string, separated by colons. Here is an example:
print(f"{math.zeros(spatial(x=8, y=6)):summary:color:shape:dtype:.5e}")
(xˢ=8, yˢ=6) float32 const 0.00000e+00
The order of the arguments is not important.
The layout determines what is printed and where. The following options are available:
summary
Summarizes the values by mean, standard deviation, minimum and maximum value.row
Prints the tensor as a single-line vector.full
Prints all values in the tensors as a multi-line string.numpy
Uses the formatting of NumPyThe keywords shape
, no-shape
, dtype
and no-dtype
can be used to show or hide additional properties of the tensor.
Use the keywords color
or no-color
.
Currently color
will use ANSI color codes which are supported by most terminals, IDEs as well as Jupyter notebooks.
You can additionally specify a format string for floating-point numbers like .3f
or .2e
.