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.

In [1]:

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

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.

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.

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ᵇ

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 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)
```

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))

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.

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

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()`

.

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.

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

The 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`

.