ΦFlow Math¶

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.Tensors have named and typed dimensions.

When performing operations such as +, -, *, /, %, ** or calling math functions on Tensors, dimensions are matched by name and type. This eliminates the need for manual reshaping or the use of singleton dimensions.

In [1]:
from phi import math

Shapes¶

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.

In [2]:
from phi.math import batch, spatial, instance, channel
In [3]:
channel(vector='x,y')
Out[3]:
(vectorᶜ=x,y)
In [4]:
batch(examples=10)
Out[4]:
(examplesᵇ=10)
In [5]:
spatial(x=4, y=3)
Out[5]:
(xˢ=4, yˢ=3)
In [6]:
instance(points=5)
Out[6]:
(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.

Tensor Creation¶

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.

In [7]:
math.tensor((1, 2, 3))
Out[7]:
(1, 2, 3) int64
In [8]:
import numpy
math.tensor(numpy.zeros([1, 5, 4, 2]), batch('batch'), spatial('x,y'), channel(vector='x,y'))
Out[8]:
(batchᵇ=1, xˢ=5, yˢ=4, vectorᶜ=x,y) float64 const 0.0
In [9]:
math.reshaped_tensor(numpy.zeros([1, 5, 4, 2]), [batch(), *spatial('x,y'), channel(vector='x,y')])
Out[9]:
(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.

  • zeros()
  • ones()
  • linspace()
  • random_normal()
  • random_uniform()
  • meshgrid()

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.

In [10]:
math.zeros(spatial(x=5, y=4))
Out[10]:
(xˢ=5, yˢ=4) const 0.0
In [11]:
math.random_uniform(channel(vector='x,y'))
Out[11]:
(x=0.005, y=0.725)
In [12]:
math.random_normal(batch(examples=6), dtype=math.DType(int, 32))
Out[12]:
(0, 0, 0, 0, -1, 0) along examplesᵇ

Backend Selection¶

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/SciPy
  • phi.tf.flow → TensorFlow
  • phi.torch.flow → PyTorch
  • phi.jax.flow → Jax

This 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.

In [13]:
from phi.math import backend
In [14]:
backend.default_backend()
Out[14]:
NumPy
In [15]:
from phi.torch import TORCH
with TORCH:
    print(math.zeros().default_backend)
PyTorch
In [16]:
backend.set_global_default_backend(backend.NUMPY)

Indexing, Slicing, Unstacking¶

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.

In [17]:
data = math.random_uniform(spatial(x=10, y=10, z=10), channel(vector='x,y,z'))
data.x[0].y[1:-1].vector['x']
Out[17]:
(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}].

In [18]:
data[{'x': 0, 'y': slice(1, -1), 'vector': 'x'}]
Out[18]:
(yˢ=8, zˢ=10) 0.488 ± 0.288 (3e-03...1e+00)

Dimensions can be iterated over or unstacked.

In [19]:
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)
In [20]:
tuple(data.x)
Out[20]:
((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))

Non-uniform Tensors¶

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 StaggeredGrids where the tensors holding the vector components have different shapes.

In [21]:
t0 = math.zeros(spatial(a=4, b=2))
t1 = math.ones(spatial(b=2, a=5))
stacked = math.stack([t0, t1], channel('c'))
stacked
Out[21]:
(aˢ=(4, 5) along cᶜ, bˢ=2, cᶜ=2) const 0.5555555820465088
In [22]:
stacked.shape.is_uniform
Out[22]:
False

Data Types and Precision¶

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.

In [23]:
from phi.math import DType
In [24]:
DType(float, 32)
Out[24]:
float32
In [25]:
DType(complex, 128)
Out[25]:
complex128
In [26]:
DType(bool)
Out[26]:
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().

Printing Options¶

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:

In [27]:
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.

Layout¶

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 NumPy

Additional Information¶

The keywords shape, no-shape, dtype and no-dtype can be used to show or hide additional properties of the tensor.

Color¶

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.

Number Format¶

You can additionally specify a format string for floating-point numbers like .3f or .2e.