Module phi.field

The fields module provides a number of data structures and functions to represent continuous, spatially varying data.

All fields are subclasses of Field which provides abstract functions for sampling field values at physical locations.

The most important field types are:

  • CenteredGrid embeds a tensor in the physical space. Uses linear interpolation between grid points.
  • StaggeredGrid samples the vector components at face centers instead of at cell centers.
  • Noise is a function that produces a procedurally generated noise field

Use grid() to create a Grid from data or by sampling another Field or Geometry. Alternatively, the phi.physics.Domain class provides convenience methods for grid creation.

All fields can be sampled at physical locations or volumes using sample() or reduce_sample().

See the phi.field module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html

Expand source code
"""
The fields module provides a number of data structures and functions to represent continuous, spatially varying data.

All fields are subclasses of `Field` which provides abstract functions for sampling field values at physical locations.

The most important field types are:

* `CenteredGrid` embeds a tensor in the physical space. Uses linear interpolation between grid points.
* `StaggeredGrid` samples the vector components at face centers instead of at cell centers.
* `Noise` is a function that produces a procedurally generated noise field

Use `grid()` to create a `Grid` from data or by sampling another `Field` or `phi.geom.Geometry`.
Alternatively, the `phi.physics.Domain` class provides convenience methods for grid creation.

All fields can be sampled at physical locations or volumes using `sample()` or `reduce_sample()`.

See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
"""

from ._field import Field, SampledField, sample, reduce_sample, resample, as_extrapolation
from ._mask import HardGeometryMask, SoftGeometryMask as GeometryMask, SoftGeometryMask
from ._grid import Grid, CenteredGrid, StaggeredGrid
from ._point_cloud import PointCloud
from ._noise import Noise
from ._angular_velocity import AngularVelocity
from phiml.math import (
    abs, sign, round, ceil, floor, sqrt, exp, is_finite as isfinite, is_finite, real, imag, sin, cos, cast, to_float, to_int32, to_int64, convert,
    stop_gradient,
    jit_compile, jit_compile_linear, gradient as functional_gradient, jacobian, gradient,
    solve_linear, solve_nonlinear, minimize,
    l2_loss, l1_loss, frequency_loss,
    unstack, stack, concat  # expand, rename_dims, pack_dims, unpack_dims
)
from ._field_math import (
    assert_close,
    bake_extrapolation,
    laplace, spatial_gradient, divergence, stagger, curl,  # spatial operators
    fourier_poisson, fourier_laplace,
    mean, pad, shift, normalize, center_of_mass,
    concat, stack,
    where, maximum, minimum,
    vec_squared, vec_length as vec_abs, vec_length,
    downsample2x, upsample2x,
    finite_fill,
    native_call,
    integrate,
    pack_dims,
    support, mask,
    connect, connect_neighbors,
)
from ._field_io import write, read
from ._scene import Scene

__all__ = [key for key in globals().keys() if not key.startswith('_')]

__pdoc__ = {
    'Grid.__init__': False,
    'Scene.__init__': False,
}

Functions

def abs(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes ||x||1. Complex x result in matching precision float values.

Note: The gradient of this operation is undefined for x=0. TensorFlow and PyTorch return 0 while Jax returns 1.

Args

x
Tensor or phiml.math.magic.PhiTreeNode

Returns

Absolute value of x of same type as x.

Expand source code
def abs_(x: TensorOrTree) -> TensorOrTree:
    """
    Computes *||x||<sub>1</sub>*.
    Complex `x` result in matching precision float values.

    *Note*: The gradient of this operation is undefined for *x=0*.
    TensorFlow and PyTorch return 0 while Jax returns 1.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode`

    Returns:
        Absolute value of `x` of same type as `x`.
    """
    return _backend_op1(x, Backend.abs)
def as_extrapolation(obj: Union[phiml.math.extrapolation.Extrapolation, float, phi.field._field.Field, None]) ‑> phiml.math.extrapolation.Extrapolation

Returns an Extrapolation representing obj.

Args

obj

One of

  • float or Tensor: Extrapolate with a constant value
  • Extrapolation: Use as-is.
  • Field: Sample values from obj, embedding another field inside obj.

Returns

Extrapolation

Expand source code
def as_extrapolation(obj: Union[Extrapolation, float, Field, None]) -> Extrapolation:
    """
    Returns an `Extrapolation` representing `obj`.

    Args:
        obj: One of

            * `float` or `Tensor`: Extrapolate with a constant value
            * `Extrapolation`: Use as-is.
            * `Field`: Sample values from `obj`, embedding another field inside `obj`.

    Returns:
        `Extrapolation`
    """
    if isinstance(obj, Field):
        from ._embed import FieldEmbedding
        return FieldEmbedding(obj)
    else:
        return math.extrapolation.as_extrapolation(obj)
def assert_close(*fields: Union[phi.field._field.SampledField, phiml.math._tensors.Tensor, numbers.Number], rel_tolerance: float = 1e-05, abs_tolerance: float = 0, msg: str = '', verbose: bool = True)

Raises an AssertionError if the values of the given fields are not close. See phiml.math.assert_close().

Expand source code
def assert_close(*fields: Union[SampledField, Tensor, Number],
                 rel_tolerance: float = 1e-5,
                 abs_tolerance: float = 0,
                 msg: str = "",
                 verbose: bool = True):
    """ Raises an AssertionError if the `values` of the given fields are not close. See `phiml.math.assert_close()`. """
    f0 = next(filter(lambda t: isinstance(t, SampledField), fields))
    values = [(f @ f0).values if isinstance(f, SampledField) else math.wrap(f) for f in fields]
    math.assert_close(*values, rel_tolerance=rel_tolerance, abs_tolerance=abs_tolerance, msg=msg, verbose=verbose)
def bake_extrapolation(grid: ~GridType) ‑> ~GridType

Pads grid with its current extrapolation. For StaggeredGrids, the resulting grid will have a consistent shape, independent of the original extrapolation.

Args

grid
CenteredGrid or StaggeredGrid.

Returns

Padded grid with extrapolation phiml.math.extrapolation.NONE.

Expand source code
def bake_extrapolation(grid: GridType) -> GridType:
    """
    Pads `grid` with its current extrapolation.
    For `StaggeredGrid`s, the resulting grid will have a consistent shape, independent of the original extrapolation.

    Args:
        grid: `CenteredGrid` or `StaggeredGrid`.

    Returns:
        Padded grid with extrapolation `phiml.math.extrapolation.NONE`.
    """
    if grid.extrapolation == math.extrapolation.NONE:
        return grid
    if isinstance(grid, StaggeredGrid):
        values = tuple(grid.values.vector)
        padded = []
        for dim, value in zip(grid.shape.spatial.names, values):
            lower, upper = grid.extrapolation.valid_outer_faces(dim)
            padded.append(math.pad(value, {dim: (0 if lower else 1, 0 if upper else 1)}, grid.extrapolation[{'vector': dim}], bounds=grid.bounds))
        return StaggeredGrid(math.stack(padded, grid.shape['vector']), bounds=grid.bounds, extrapolation=math.extrapolation.NONE)
    elif isinstance(grid, CenteredGrid):
        return pad(grid, 1).with_extrapolation(math.extrapolation.NONE)
    else:
        raise ValueError(f"Not a valid grid: {grid}")
def cast(x: ~MagicType, dtype: Union[phiml.backend._dtype.DType, type]) ‑> ~OtherMagicType

Casts x to a different data type.

Implementations:

See Also: to_float(), to_int32(), to_int64(), to_complex.

Args

x
Tensor
dtype
New data type as phiml.math.DType, e.g. DType(int, 16).

Returns

Tensor with data type dtype

Expand source code
def cast(x: MagicType, dtype: Union[DType, type]) -> OtherMagicType:
    """
    Casts `x` to a different data type.

    Implementations:

    * NumPy: [`x.astype()`](numpy.ndarray.astype)
    * PyTorch: [`x.to()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.to)
    * TensorFlow: [`tf.cast`](https://www.tensorflow.org/api_docs/python/tf/cast)
    * Jax: [`jax.numpy.array`](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)

    See Also:
        `to_float`, `to_int32`, `to_int64`, `to_complex`.

    Args:
        x: `Tensor`
        dtype: New data type as `phiml.math.DType`, e.g. `DType(int, 16)`.

    Returns:
        `Tensor` with data type `dtype`
    """
    if not isinstance(dtype, DType):
        dtype = DType.as_dtype(dtype)
    if hasattr(x, '__cast__'):
        return x.__cast__(dtype)
    elif isinstance(x, (Number, bool)):
        return dtype.kind(x)
    elif isinstance(x, PhiTreeNode):
        attrs = {key: getattr(x, key) for key in value_attributes(x)}
        new_attrs = {k: cast(v, dtype) for k, v in attrs.items()}
        return copy_with(x, **new_attrs)
    try:
        backend = choose_backend(x)
        return backend.cast(x, dtype)
    except NoBackendFound:
        if dtype.kind == bool:
            return bool(x)
        raise ValueError(f"Cannot cast object of type '{type(x).__name__}'")
def ceil(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes ⌈x⌉ of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def ceil(x: TensorOrTree) -> TensorOrTree:
    """ Computes *⌈x⌉* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.ceil)
def center_of_mass(density: phi.field._field.SampledField)

Compute the center of mass of a density field.

Args

density
Scalar SampledField

Returns

Tensor holding only batch dimensions.

Expand source code
def center_of_mass(density: SampledField):
    """
    Compute the center of mass of a density field.

    Args:
        density: Scalar `SampledField`

    Returns:
        `Tensor` holding only batch dimensions.
    """
    assert 'vector' not in density.shape
    return mean(density.points * density) / mean(density)
def concat(fields: Union[List[~SampledFieldType], Tuple[~SampledFieldType, ...]], dim: Union[str, phiml.math._shape.Shape]) ‑> ~SampledFieldType

Concatenates the given SampledFields along dim.

See Also: stack().

Args

fields
List of matching SampledField instances.
dim
Concatenation dimension as Shape. Size is ignored.

Returns

SampledField matching concatenated fields.

Expand source code
def concat(fields: Union[List[SampledFieldType], Tuple[SampledFieldType, ...]], dim: Union[str, Shape]) -> SampledFieldType:
    """
    Concatenates the given `SampledField`s along `dim`.

    See Also:
        `stack()`.

    Args:
        fields: List of matching `SampledField` instances.
        dim: Concatenation dimension as `Shape`. Size is ignored.

    Returns:
        `SampledField` matching concatenated fields.
    """
    assert all(isinstance(f, SampledField) for f in fields)
    assert all(isinstance(f, type(fields[0])) for f in fields)
    if any(f.extrapolation != fields[0].extrapolation for f in fields):
        raise NotImplementedError("Concatenating extrapolations not supported")
    if isinstance(fields[0], Grid):
        values = math.concat([f.values for f in fields], dim)
        return fields[0].with_values(values)
    elif isinstance(fields[0], PointCloud):
        elements = geom.concat([f.elements for f in fields], dim)
        values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim)
        return PointCloud(elements=elements, values=values, extrapolation=fields[0].extrapolation, add_overlapping=fields[0]._add_overlapping, bounds=fields[0]._bounds)
    raise NotImplementedError(type(fields[0]))
def connect(obj: phi.field._field.SampledField, connections: phiml.math._tensors.Tensor) ‑> phi.field._mesh.Mesh

Build a Mesh by connecting elements from a field.

See Also: connect_neighbors().

Args

obj
PointCloud or Mesh.
connections
Connectivity matrix. Any non-zero entry represents a connection.

Returns

Mesh

Expand source code
def connect(obj: SampledField, connections: Tensor) -> Mesh:
    """
    Build a `Mesh` by connecting elements from a field.

    See Also:
        `connect_neighbors()`.

    Args:
        obj: `PointCloud` or `Mesh`.
        connections: Connectivity matrix. Any non-zero entry represents a connection.

    Returns:
        `Mesh`
    """
    if isinstance(obj, (PointCloud, Mesh)):
        return Mesh(obj.elements, connections, obj.values, extrapolation=obj.extrapolation, bounds=obj.bounds)
    else:
        raise ValueError(f"connect requires a PointCloud or Mesh but got {type(obj)}")
def connect_neighbors(obj: phi.field._field.SampledField, max_distance: float, format: str = 'dense') ‑> phi.field._mesh.Mesh

Build a Mesh by connecting proximate elements of a SampledField.

See Also: connect().

Args

obj
PointCloud, Mesh, CenteredGrid or StaggeredGrid.
max_distance
Connectivity threshold distance. Elements further apart than this will not be connected.
format
Connectivity matrix format, 'dense', 'coo' or 'csr'.

Returns

Mesh.

Expand source code
def connect_neighbors(obj: SampledField, max_distance: float or Tensor, format: str = 'dense') -> Mesh:
    """
    Build  a `Mesh` by connecting proximate elements of a `SampledField`.

    See Also:
        `connect()`.

    Args:
        obj: `PointCloud`, `Mesh`, `CenteredGrid` or `StaggeredGrid`.
        max_distance: Connectivity threshold distance. Elements further apart than this will not be connected.
        format: Connectivity matrix format, `'dense'`, `'coo'` or `'csr'`.

    Returns:
        `Mesh`.
    """
    if isinstance(obj, CenteredGrid):
        elements = flatten(obj.elements, instance('elements'))
        values = math.pack_dims(obj.values, spatial, instance('elements'))
        obj = PointCloud(elements, values, obj.extrapolation, bounds=obj.bounds)
    elif isinstance(obj, StaggeredGrid):
        elements = flatten(obj.elements, instance('elements'), flatten_batch=True)
        values = math.pack_dims(obj.values, spatial(obj.values).names + ('vector',), instance('elements'))
        obj = PointCloud(elements, values, obj.extrapolation, bounds=obj.bounds)
    assert isinstance(obj, (PointCloud, Mesh)), f"obj must be a PointCloud, Mesh or Grid but got {type(obj)}"
    points = math.rename_dims(obj.elements, spatial, instance).center
    dx = math.pairwise_distances(points, max_distance=max_distance, format=format)
    con = math.vec_length(dx) > 0
    return connect(obj, con)
def convert(x, backend: phiml.backend._backend.Backend = None, use_dlpack=True)

Convert the native representation of a Tensor or phiml.math.magic.PhiTreeNode to the native format of backend.

Warning: This operation breaks the automatic differentiation chain.

See Also: phiml.math.backend.convert().

Args

x
Tensor to convert. If x is a phiml.math.magic.PhiTreeNode, its variable attributes are converted.
backend
Target backend. If None, uses the current default backend, see phiml.math.backend.default_backend().

Returns

Tensor with native representation belonging to backend.

Expand source code
def convert(x, backend: Backend = None, use_dlpack=True):
    """
    Convert the native representation of a `Tensor` or `phiml.math.magic.PhiTreeNode` to the native format of `backend`.

    *Warning*: This operation breaks the automatic differentiation chain.

    See Also:
        `phiml.math.backend.convert()`.

    Args:
        x: `Tensor` to convert. If `x` is a `phiml.math.magic.PhiTreeNode`, its variable attributes are converted.
        backend: Target backend. If `None`, uses the current default backend, see `phiml.math.backend.default_backend()`.

    Returns:
        `Tensor` with native representation belonging to `backend`.
    """
    if isinstance(x, Tensor):
        return x._op1(lambda native: b_convert(native, backend, use_dlpack=use_dlpack))
    elif isinstance(x, PhiTreeNode):
        return copy_with(x, **{a: convert(getattr(x, a), backend, use_dlpack=use_dlpack) for a in variable_attributes(x)})
    else:
        return b_convert(x, backend, use_dlpack=use_dlpack)
def cos(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes cos(x) of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def cos(x: TensorOrTree) -> TensorOrTree:
    """ Computes *cos(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.cos)
def curl(field: phi.field._grid.Grid, type: type = phi.field._grid.CenteredGrid)

Computes the finite-difference curl of the give 2D StaggeredGrid.

Expand source code
def curl(field: Grid, type: type = CenteredGrid):
    """ Computes the finite-difference curl of the give 2D `StaggeredGrid`. """
    assert field.spatial_rank in (2, 3), "curl is only defined in 2 and 3 spatial dimensions."
    if isinstance(field, CenteredGrid) and field.spatial_rank == 2:
        if 'vector' not in field.shape and type == StaggeredGrid:
            # 2D curl of scalar field
            grad = math.spatial_gradient(field.values, dx=field.dx, difference='forward', padding=None, stack_dim=channel('vector'))
            result = grad.vector[::-1] * (1, -1)  # (d/dy, -d/dx)
            bounds = Box(field.bounds.lower + 0.5 * field.dx, field.bounds.upper - 0.5 * field.dx)  # lose 1 cell per dimension
            return StaggeredGrid(result, bounds=bounds, extrapolation=field.extrapolation.spatial_gradient())
        if 'vector' in field.shape and type == CenteredGrid:
            # 2D curl of vector field
            x, y = field.shape.spatial.names
            vy_dx = math.spatial_gradient(field.values.vector[1], dx=field.dx.vector[0], padding=field.extrapolation, dims=x, stack_dim=None)
            vx_dy = math.spatial_gradient(field.values.vector[0], dx=field.dx.vector[1], padding=field.extrapolation, dims=y, stack_dim=None)
            c = vy_dx - vx_dy
            return field.with_values(c)
    elif isinstance(field, StaggeredGrid) and field.spatial_rank == 2:
        if type == CenteredGrid:
            values = bake_extrapolation(field).values
            x_padded = math.pad(values.vector['x'], {'y': (1, 1)}, field.extrapolation)
            y_padded = math.pad(values.vector['y'], {'x': (1, 1)}, field.extrapolation)
            vx_dy = math.spatial_gradient(x_padded, field.dx, 'forward', None, dims='y', stack_dim=None)
            vy_dx = math.spatial_gradient(y_padded, field.dx, 'forward', None, dims='x', stack_dim=None)
            result = vy_dx - vx_dy
            return CenteredGrid(result, field.extrapolation.spatial_gradient(), bounds=field.bounds)
    raise NotImplementedError()
def divergence(field: phi.field._grid.Grid, order=2, implicit: phiml.math._optimize.Solve = None) ‑> phi.field._grid.CenteredGrid

Computes the divergence of a grid using finite differences.

This function can operate in two modes depending on the type of field:

  • CenteredGrid approximates the divergence at cell centers using central differences
  • StaggeredGrid exactly computes the divergence at cell centers

Args

field
vector field as CenteredGrid or StaggeredGrid
order
Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
When a Solve object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.

Returns

Divergence field as CenteredGrid

Expand source code
def divergence(field: Grid, order=2, implicit: Solve = None) -> CenteredGrid:
    """
    Computes the divergence of a grid using finite differences.

    This function can operate in two modes depending on the type of `field`:

    * `CenteredGrid` approximates the divergence at cell centers using central differences
    * `StaggeredGrid` exactly computes the divergence at cell centers

    Args:
        field: vector field as `CenteredGrid` or `StaggeredGrid`
        order: Spatial order of accuracy.
            Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution.
            Supported: 2 explicit, 4 explicit, 6 implicit.
        implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances.
            Otherwise, an explicit stencil is used.

    Returns:
        Divergence field as `CenteredGrid`
    """

    extrap_map = {}
    if not implicit:
        if order == 2:
            if isinstance(field, CenteredGrid):
                values, needed_shifts = [-1 / 2, 1 / 2], (-1, 1)
            else:
                values, needed_shifts = [-1, 1], (0, 1)

        elif order == 4:
            if isinstance(field, CenteredGrid):
                values, needed_shifts = [1 / 12, -2 / 3, 2 / 3, -1 / 12], (-2, -1, 1, 2)
            else:
                values, needed_shifts = [1 / 24, -27 / 24, 27 / 24, -1 / 24], (-1, 0, 1, 2)
    else:
        extrap_map_rhs = {}
        if order == 6:
            extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC)
            extrap_map_rhs['symmetric'] = combine_by_direction(ANTIREFLECT, ANTISYMMETRIC)

            if isinstance(field, CenteredGrid):
                values, needed_shifts = [-1 / 36, -14 / 18, 14 / 18, 1 / 36], (-2, -1, 1, 2)
                values_rhs, needed_shifts_rhs = [1 / 3, 1, 1 / 3], (-1, 0, 1)

            else:
                values, needed_shifts = [-17 / 186, -63 / 62, 63 / 62, 17 / 186], (-1, 0, 1, 2)
                values_rhs, needed_shifts_rhs = [9 / 62, 1, 9 / 62], (-1, 0, 1)
    base_widths = (abs(min(needed_shifts)), max(needed_shifts))
    field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation))  # ToDo does this line do anything?
    spatial_dims = field.shape.spatial.names
    if isinstance(field, StaggeredGrid):
        base_widths = (base_widths[0]+1, base_widths[1])
        padded_components = []
        for dim, component in zip(field.shape.spatial.names, unstack(field, 'vector')):
            border_valid = field.extrapolation.valid_outer_faces(dim)
            padding_widths = (base_widths[0] - border_valid[0], base_widths[1] - border_valid[1])
            padded_components.append(pad(component, {dim: padding_widths}))
    elif isinstance(field, CenteredGrid):
        padded_components = [pad(component, {dim: base_widths}) for dim, component in zip(spatial_dims, unstack(field, 'vector'))]
        if field.extrapolation == math.extrapolation.NONE:
            padded_components = [pad(component, {dim_: (0, 0) if dim_ == dim else (-1, -1) for dim_ in spatial_dims}) for dim, component in zip(spatial_dims, padded_components)]
    shifted_components = [shift(padded_component, needed_shifts, None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, spatial_dims)]
    result_components = [sum([value * shift for value, shift in zip(values, shifted_component)]) / field.dx.vector[dim] for shifted_component, dim in zip(shifted_components, spatial_dims)]
    if implicit:
        result_components = stack(result_components, channel('vector'))
        result_components.with_values(result_components.values._cache())
        implicit.x0 = field
        result_components = solve_linear(_lhs_for_implicit_scheme, result_components, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=channel('vector'))
        result_components = unstack(result_components, 'vector')
    result_components = [component.with_bounds(field.bounds) for component in result_components]
    result = sum(result_components)
    if field.extrapolation == math.extrapolation.NONE and isinstance(field, CenteredGrid):
        result = result.with_bounds(Box(field.bounds.lower + field.dx, field.bounds.upper - field.dx))
    return result
def downsample2x(grid: phi.field._grid.Grid) ‑> ~GridType

Reduces the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.

See Also: upsample2x().

Args

grid
CenteredGrid or StaggeredGrid.

Returns

Grid of same type as grid.

Expand source code
def downsample2x(grid: Grid) -> GridType:
    """
    Reduces the number of sample points by a factor of 2 in each spatial dimension.
    The new values are determined via linear interpolation.

    See Also:
        `upsample2x()`.

    Args:
        grid: `CenteredGrid` or `StaggeredGrid`.

    Returns:
        `Grid` of same type as `grid`.
    """
    if isinstance(grid, CenteredGrid):
        values = math.downsample2x(grid.values, grid.extrapolation)
        return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation)
    elif isinstance(grid, StaggeredGrid):
        values = []
        for dim, centered_grid in zip(grid.shape.spatial.names, unstack(grid, 'vector')):
            odd_discarded = centered_grid.values[{dim: slice(None, None, 2)}]
            others_interpolated = math.downsample2x(odd_discarded, grid.extrapolation, dims=grid.shape.spatial.without(dim))
            values.append(others_interpolated)
        return StaggeredGrid(math.stack(values, channel('vector')), bounds=grid.bounds, extrapolation=grid.extrapolation)
    else:
        raise ValueError(type(grid))
def exp(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes exp(x) of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def exp(x: TensorOrTree) -> TensorOrTree:
    """ Computes *exp(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.exp)
def finite_fill(grid: ~GridType, distance=1, diagonal=True) ‑> ~GridType

Extrapolates values of grid which are marked by nonzero values in valid using `phiml.math.masked_fill(). If values is a StaggeredGrid, its components get extrapolated independently.

Args

grid
Grid holding the values for extrapolation and possible non-finite values to be filled.
distance
Number of extrapolation steps, i.e. how far a cell can be from the closest finite value to get filled.
diagonal
Whether to extrapolate values to their diagonal neighbors per step.

Returns

grid
Grid with extrapolated values.
valid
binary Grid marking all valid values after extrapolation.
Expand source code
def finite_fill(grid: GridType, distance=1, diagonal=True) -> GridType:
    """
    Extrapolates values of `grid` which are marked by nonzero values in `valid` using `phiml.math.masked_fill().
    If `values` is a StaggeredGrid, its components get extrapolated independently.

    Args:
        grid: Grid holding the values for extrapolation and possible non-finite values to be filled.
        distance: Number of extrapolation steps, i.e. how far a cell can be from the closest finite value to get filled.
        diagonal: Whether to extrapolate values to their diagonal neighbors per step.

    Returns:
        grid: Grid with extrapolated values.
        valid: binary Grid marking all valid values after extrapolation.
    """
    if isinstance(grid, CenteredGrid):
        new_values = math.finite_fill(grid.values, distance=distance, diagonal=diagonal, padding=grid.extrapolation)
        return grid.with_values(new_values)
    elif isinstance(grid, StaggeredGrid):
        new_values = [finite_fill(c, distance=distance, diagonal=diagonal).values for c in grid.vector]
        return grid.with_values(math.stack(new_values, channel(grid)))
    else:
        raise ValueError(grid)
def floor(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes ⌊x⌋ of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def floor(x: TensorOrTree) -> TensorOrTree:
    """ Computes *⌊x⌋* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.floor)
def fourier_laplace(grid: ~GridType, times=1) ‑> ~GridType

See phiml.math.fourier_laplace()

Expand source code
def fourier_laplace(grid: GridType, times=1) -> GridType:
    """ See `phiml.math.fourier_laplace()` """
    assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC
    values = math.fourier_laplace(grid.values, dx=grid.dx, times=times)
    return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
def fourier_poisson(grid: ~GridType, times=1) ‑> ~GridType

See phiml.math.fourier_poisson()

Expand source code
def fourier_poisson(grid: GridType, times=1) -> GridType:
    """ See `phiml.math.fourier_poisson()` """
    assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC
    values = math.fourier_poisson(grid.values, dx=grid.dx, times=times)
    return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-05, ignore_mean=False, n=2) ‑> phiml.math._tensors.Tensor

Penalizes the squared values in frequency (Fourier) space. Lower frequencies are weighted more strongly then higher frequencies, depending on frequency_falloff.

Args

x
Tensor or phiml.math.magic.PhiTreeNode Values to penalize, typically actual - target.
frequency_falloff
Large values put more emphasis on lower frequencies, 1.0 weights all frequencies equally. Note: The total loss is not normalized. Varying the value will result in losses of different magnitudes.
threshold
Frequency amplitudes below this value are ignored. Setting this to zero may cause infinities or NaN values during backpropagation.
ignore_mean
If True, does not penalize the mean value (frequency=0 component).

Returns

Scalar loss value

Expand source code
def frequency_loss(x,
                   frequency_falloff: float = 100,
                   threshold=1e-5,
                   ignore_mean=False,
                   n=2) -> Tensor:
    """
    Penalizes the squared `values` in frequency (Fourier) space.
    Lower frequencies are weighted more strongly then higher frequencies, depending on `frequency_falloff`.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` Values to penalize, typically `actual - target`.
        frequency_falloff: Large values put more emphasis on lower frequencies, 1.0 weights all frequencies equally.
            *Note*: The total loss is not normalized. Varying the value will result in losses of different magnitudes.
        threshold: Frequency amplitudes below this value are ignored.
            Setting this to zero may cause infinities or NaN values during backpropagation.
        ignore_mean: If `True`, does not penalize the mean value (frequency=0 component).

    Returns:
      Scalar loss value
    """
    assert n in (1, 2)
    if isinstance(x, Tensor):
        if ignore_mean:
            x -= math.mean(x, x.shape.non_batch)
        k_squared = vec_squared(math.fftfreq(x.shape.spatial))
        weights = math.exp(-0.5 * k_squared * frequency_falloff ** 2)

        diff_fft = abs_square(math.fft(x) * weights)
        diff_fft = math.sqrt(math.maximum(diff_fft, threshold))
        return l2_loss(diff_fft) if n == 2 else l1_loss(diff_fft)
    elif isinstance(x, PhiTreeNode):
        losses = [frequency_loss(getattr(x, a), frequency_falloff, threshold, ignore_mean, n) for a in variable_values(x)]
        return sum(losses)
    else:
        raise ValueError(x)
def functional_gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable

Creates a function which computes the gradient of f.

Example:

def loss_function(x, y):
    prediction = f(x)
    loss = math.l2_loss(prediction - y)
    return loss, prediction

dx = gradient(loss_function, 'x', get_output=False)(x, y)

(loss, prediction), (dx, dy) = gradient(loss_function,
                                        'x,y', get_output=True)(x, y)

Functional gradients are implemented for the following backends:

When the gradient function is invoked, f is called with tensors that track the gradient. For PyTorch, arg.requires_grad = True for all positional arguments of f.

Args

f
Function to be differentiated. f must return a floating point Tensor with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if return_values=True. All arguments for which the gradient is computed must be of dtype float or complex.
get_output
Whether the gradient function should also return the return values of f.
wrt
Comma-separated parameter names of f with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

Returns

Function with the same arguments as f that returns the value of f, auxiliary data and gradient of f if get_output=True, else just the gradient of f.

Expand source code
def gradient(f: Callable, wrt: str = None, get_output=True) -> Callable:
    """
    Creates a function which computes the gradient of `f`.

    Example:
    ```python
    def loss_function(x, y):
        prediction = f(x)
        loss = math.l2_loss(prediction - y)
        return loss, prediction

    dx = gradient(loss_function, 'x', get_output=False)(x, y)

    (loss, prediction), (dx, dy) = gradient(loss_function,
                                            'x,y', get_output=True)(x, y)
    ```

    Functional gradients are implemented for the following backends:

    * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward)
    * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape)
    * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad)

    When the gradient function is invoked, `f` is called with tensors that track the gradient.
    For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`.

    Args:
        f: Function to be differentiated.
            `f` must return a floating point `Tensor` with rank zero.
            It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`.
            All arguments for which the gradient is computed must be of dtype float or complex.
        get_output: Whether the gradient function should also return the return values of `f`.
        wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed.
            If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

    Returns:
        Function with the same arguments as `f` that returns the value of `f`, auxiliary data and gradient of `f` if `get_output=True`, else just the gradient of `f`.
    """
    f_params, wrt = simplify_wrt(f, wrt)
    return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=True)
def gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable

Creates a function which computes the gradient of f.

Example:

def loss_function(x, y):
    prediction = f(x)
    loss = math.l2_loss(prediction - y)
    return loss, prediction

dx = gradient(loss_function, 'x', get_output=False)(x, y)

(loss, prediction), (dx, dy) = gradient(loss_function,
                                        'x,y', get_output=True)(x, y)

Functional gradients are implemented for the following backends:

When the gradient function is invoked, f is called with tensors that track the gradient. For PyTorch, arg.requires_grad = True for all positional arguments of f.

Args

f
Function to be differentiated. f must return a floating point Tensor with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if return_values=True. All arguments for which the gradient is computed must be of dtype float or complex.
get_output
Whether the gradient function should also return the return values of f.
wrt
Comma-separated parameter names of f with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

Returns

Function with the same arguments as f that returns the value of f, auxiliary data and gradient of f if get_output=True, else just the gradient of f.

Expand source code
def gradient(f: Callable, wrt: str = None, get_output=True) -> Callable:
    """
    Creates a function which computes the gradient of `f`.

    Example:
    ```python
    def loss_function(x, y):
        prediction = f(x)
        loss = math.l2_loss(prediction - y)
        return loss, prediction

    dx = gradient(loss_function, 'x', get_output=False)(x, y)

    (loss, prediction), (dx, dy) = gradient(loss_function,
                                            'x,y', get_output=True)(x, y)
    ```

    Functional gradients are implemented for the following backends:

    * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward)
    * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape)
    * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad)

    When the gradient function is invoked, `f` is called with tensors that track the gradient.
    For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`.

    Args:
        f: Function to be differentiated.
            `f` must return a floating point `Tensor` with rank zero.
            It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`.
            All arguments for which the gradient is computed must be of dtype float or complex.
        get_output: Whether the gradient function should also return the return values of `f`.
        wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed.
            If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

    Returns:
        Function with the same arguments as `f` that returns the value of `f`, auxiliary data and gradient of `f` if `get_output=True`, else just the gradient of `f`.
    """
    f_params, wrt = simplify_wrt(f, wrt)
    return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=True)
def imag(x: ~TensorOrTree) ‑> ~TensorOrTree

Returns the imaginary part of x. If x does not store complex numbers, returns a zero tensor with the same shape and dtype as this tensor.

See Also: real(), conjugate().

Args

x
Tensor or phiml.math.magic.PhiTreeNode or native tensor.

Returns

Imaginary component of x if x is complex, zeros otherwise.

Expand source code
def imag(x: TensorOrTree) -> TensorOrTree:
    """
    Returns the imaginary part of `x`.
    If `x` does not store complex numbers, returns a zero tensor with the same shape and dtype as this tensor.

    See Also:
        `real()`, `conjugate()`.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` or native tensor.

    Returns:
        Imaginary component of `x` if `x` is complex, zeros otherwise.
    """
    return _backend_op1(x, Backend.imag)
def integrate(field: phi.field._field.Field, region: phi.geom._geom.Geometry, **kwargs) ‑> phiml.math._tensors.Tensor

Computes R f(x) dxd , where f denotes the Field, R the region and d the number of spatial dimensions (d=field.shape.spatial_rank). Depending on the sample() implementation for field, the integral may be a rough approximation.

This method is currently only implemented for CenteredGrid.

Args

field
Field to integrate.
region
Region to integrate over.
**kwargs
Specify numerical scheme.

Returns

Integral as phi.Tensor

Expand source code
def integrate(field: Field, region: Geometry, **kwargs) -> Tensor:
    """
    Computes *∫<sub>R</sub> f(x) dx<sup>d</sup>* , where *f* denotes the `Field`, *R* the `region` and *d* the number of spatial dimensions (`d=field.shape.spatial_rank`).
    Depending on the `sample` implementation for `field`, the integral may be a rough approximation.

    This method is currently only implemented for `CenteredGrid`.

    Args:
        field: `Field` to integrate.
        region: Region to integrate over.
        **kwargs: Specify numerical scheme.

    Returns:
        Integral as `phi.Tensor`
    """
    if not isinstance(field, CenteredGrid):
        raise NotImplementedError()
    return field._sample(region, **kwargs) * region.volume
def is_finite(x: ~TensorOrTree) ‑> ~TensorOrTree

Returns a Tensor or phiml.math.magic.PhiTreeNode matching x with values True where x has a finite value and False otherwise.

Expand source code
def is_finite(x: TensorOrTree) -> TensorOrTree:
    """ Returns a `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """
    return _backend_op1(x, Backend.isfinite)
def isfinite(x: ~TensorOrTree) ‑> ~TensorOrTree

Returns a Tensor or phiml.math.magic.PhiTreeNode matching x with values True where x has a finite value and False otherwise.

Expand source code
def is_finite(x: TensorOrTree) -> TensorOrTree:
    """ Returns a `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """
    return _backend_op1(x, Backend.isfinite)
def jacobian(f: Callable, wrt: str = None, get_output=True) ‑> Callable

Creates a function which computes the Jacobian matrix of f. For scalar functions, consider using gradient() instead.

Example:

def f(x, y):
    prediction = f(x)
    loss = math.l2_loss(prediction - y)
    return loss, prediction

dx = jacobian(loss_function, wrt='x', get_output=False)(x, y)

(loss, prediction), (dx, dy) = jacobian(loss_function,
                                    wrt='x,y', get_output=True)(x, y)

Functional gradients are implemented for the following backends:

When the gradient function is invoked, f is called with tensors that track the gradient. For PyTorch, arg.requires_grad = True for all positional arguments of f.

Args

f
Function to be differentiated. f must return a floating point Tensor with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if return_values=True. All arguments for which the gradient is computed must be of dtype float or complex.
get_output
Whether the gradient function should also return the return values of f.
wrt
Comma-separated parameter names of f with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

Returns

Function with the same arguments as f that returns the value of f, auxiliary data and Jacobian of f if get_output=True, else just the Jacobian of f.

Expand source code
def jacobian(f: Callable, wrt: str = None, get_output=True) -> Callable:
    """
    Creates a function which computes the Jacobian matrix of `f`.
    For scalar functions, consider using `gradient()` instead.

    Example:
    ```python
    def f(x, y):
        prediction = f(x)
        loss = math.l2_loss(prediction - y)
        return loss, prediction

    dx = jacobian(loss_function, wrt='x', get_output=False)(x, y)

    (loss, prediction), (dx, dy) = jacobian(loss_function,
                                        wrt='x,y', get_output=True)(x, y)
    ```

    Functional gradients are implemented for the following backends:

    * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward)
    * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape)
    * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad)

    When the gradient function is invoked, `f` is called with tensors that track the gradient.
    For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`.

    Args:
        f: Function to be differentiated.
            `f` must return a floating point `Tensor` with rank zero.
            It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`.
            All arguments for which the gradient is computed must be of dtype float or complex.
        get_output: Whether the gradient function should also return the return values of `f`.
        wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed.
            If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged).

    Returns:
        Function with the same arguments as `f` that returns the value of `f`, auxiliary data and Jacobian of `f` if `get_output=True`, else just the Jacobian of `f`.
    """
    f_params, wrt = simplify_wrt(f, wrt)
    return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=False)
def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) ‑> Callable

Compiles a graph based on the function f. The graph compilation is performed just-in-time (jit), e.g. when the returned function is called for the first time.

The traced function will compute the same result as f but may run much faster. Some checks may be disabled in the compiled function.

Can be used as a decorator:

@math.jit_compile
def my_function(x: math.Tensor) -> math.Tensor:

Invoking the returned function may invoke re-tracing / re-compiling f after the first call if either

  • it is called with a different number of arguments,
  • the tensor arguments have different dimension names or types (the dimension order also counts),
  • any Tensor arguments require a different backend than previous invocations,
  • phiml.math.magic.PhiTreeNode positional arguments do not match in non-variable properties.

Compilation is implemented for the following backends:

Jit-compilations cannot be nested, i.e. you cannot call jit_compile() while another function is being compiled. An exception to this is jit_compile_linear() which can be called from within a jit-compiled function.

See Also: jit_compile_linear()

Args

f
Function to be traced. All positional arguments must be of type Tensor or phiml.math.magic.PhiTreeNode returning a single Tensor or phiml.math.magic.PhiTreeNode.
auxiliary_args
Comma-separated parameter names of arguments that are not relevant to backpropagation.
forget_traces
If True, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.

Returns

Function with similar signature and return values as f.

Expand source code
def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) -> Callable:
    """
    Compiles a graph based on the function `f`.
    The graph compilation is performed just-in-time (jit), e.g. when the returned function is called for the first time.

    The traced function will compute the same result as `f` but may run much faster.
    Some checks may be disabled in the compiled function.

    Can be used as a decorator:
    ```python
    @math.jit_compile
    def my_function(x: math.Tensor) -> math.Tensor:
    ```

    Invoking the returned function may invoke re-tracing / re-compiling `f` after the first call if either

    * it is called with a different number of arguments,
    * the tensor arguments have different dimension names or types (the dimension order also counts),
    * any `Tensor` arguments require a different backend than previous invocations,
    * `phiml.math.magic.PhiTreeNode` positional arguments do not match in non-variable properties.

    Compilation is implemented for the following backends:

    * PyTorch: [`torch.jit.trace`](https://pytorch.org/docs/stable/jit.html)
    * TensorFlow: [`tf.function`](https://www.tensorflow.org/guide/function)
    * Jax: [`jax.jit`](https://jax.readthedocs.io/en/latest/notebooks/quickstart.html#using-jit-to-speed-up-functions)

    Jit-compilations cannot be nested, i.e. you cannot call `jit_compile()` while another function is being compiled.
    An exception to this is `jit_compile_linear()` which can be called from within a jit-compiled function.

    See Also:
        `jit_compile_linear()`

    Args:
        f: Function to be traced.
            All positional arguments must be of type `Tensor` or `phiml.math.magic.PhiTreeNode` returning a single `Tensor` or `phiml.math.magic.PhiTreeNode`.
        auxiliary_args: Comma-separated parameter names of arguments that are not relevant to backpropagation.
        forget_traces: If `True`, only remembers the most recent compiled instance of this function.
            Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.

    Returns:
        Function with similar signature and return values as `f`.
    """
    if f is None:
        kwargs = {k: v for k, v in locals().items() if v is not None}
        return partial(jit_compile, **kwargs)
    auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip())
    return f if isinstance(f, (JitFunction, LinearFunction)) and f.auxiliary_args == auxiliary_args else JitFunction(f, auxiliary_args, forget_traces or False)
def jit_compile_linear(f: Callable[[~X], ~Y] = None, auxiliary_args: str = None, forget_traces: bool = None) ‑> phiml.math._functional.LinearFunction[~X, ~Y]

Compile an optimized representation of the linear function f. For backends that support sparse tensors, a sparse matrix will be constructed for f.

Can be used as a decorator:

@math.jit_compile_linear
def my_linear_function(x: math.Tensor) -> math.Tensor:

Unlike jit_compile(), jit_compile_linear() can be called during a regular jit compilation.

See Also: jit_compile()

Args

f
Function that is linear in its positional arguments. All positional arguments must be of type Tensor and f must return a Tensor.
auxiliary_args
Which parameters f is not linear in. These arguments are treated as conditioning arguments and will cause re-tracing on change.
forget_traces
If True, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.

Returns

LinearFunction with similar signature and return values as f.

Expand source code
def jit_compile_linear(f: Callable[[X], Y] = None, auxiliary_args: str = None, forget_traces: bool = None) -> 'LinearFunction[X, Y]':
    """
    Compile an optimized representation of the linear function `f`.
    For backends that support sparse tensors, a sparse matrix will be constructed for `f`.

    Can be used as a decorator:
    ```python
    @math.jit_compile_linear
    def my_linear_function(x: math.Tensor) -> math.Tensor:
    ```

    Unlike `jit_compile()`, `jit_compile_linear()` can be called during a regular jit compilation.

    See Also:
        `jit_compile()`

    Args:
        f: Function that is linear in its positional arguments.
            All positional arguments must be of type `Tensor` and `f` must return a `Tensor`.
        auxiliary_args: Which parameters `f` is not linear in. These arguments are treated as conditioning arguments and will cause re-tracing on change.
        forget_traces: If `True`, only remembers the most recent compiled instance of this function.
            Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.

    Returns:
        `LinearFunction` with similar signature and return values as `f`.
    """
    if f is None:
        kwargs = {k: v for k, v in locals().items() if v is not None}
        return partial(jit_compile_linear, **kwargs)
    if isinstance(f, JitFunction):
        f = f.f  # cannot trace linear function from jitted version
    if isinstance(auxiliary_args, str):
        auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip())
    else:
        assert auxiliary_args is None
        f_params = function_parameters(f)
        auxiliary_args = f_params[1:]
    return f if isinstance(f, LinearFunction) and f.auxiliary_args == auxiliary_args else LinearFunction(f, auxiliary_args, forget_traces or False)
def l1_loss(x, reduce: Union[str, tuple, list, set, phiml.math._shape.Shape, Callable] = <function non_batch>) ‑> phiml.math._tensors.Tensor

Computes i ||xi||1, summing over all non-batch dimensions.

Args

x
Tensor or phiml.math.magic.PhiTreeNode or 0D or 1D native tensor. For phiml.math.magic.PhiTreeNode objects, only value the sum over all value attributes is computed.
reduce
Dimensions to reduce as DimFilter.

Returns

loss
Tensor
Expand source code
def l1_loss(x, reduce: DimFilter = math.non_batch) -> Tensor:
    """
    Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>1</sub>*, summing over all non-batch dimensions.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` or 0D or 1D native tensor.
            For `phiml.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed.
        reduce: Dimensions to reduce as `DimFilter`.

    Returns:
        loss: `Tensor`
    """
    if isinstance(x, Tensor):
        return math.sum_(abs(x), reduce)
    elif isinstance(x, PhiTreeNode):
        return sum([l1_loss(getattr(x, a), reduce) for a in variable_values(x)])
    else:
        try:
            backend = math.choose_backend(x)
            shape = backend.staticshape(x)
            if len(shape) == 0:
                return abs(x)
            elif len(shape) == 1:
                return backend.sum(abs(x))
            else:
                raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-ML tensors.")
        except math.NoBackendFound:
            raise ValueError(x)
def l2_loss(x, reduce: Union[str, tuple, list, set, phiml.math._shape.Shape, Callable] = <function non_batch>) ‑> phiml.math._tensors.Tensor

Computes i ||xi||22 / 2, summing over all non-batch dimensions.

Args

x
Tensor or phiml.math.magic.PhiTreeNode or 0D or 1D native tensor. For phiml.math.magic.PhiTreeNode objects, only value the sum over all value attributes is computed.
reduce
Dimensions to reduce as DimFilter.

Returns

loss
Tensor
Expand source code
def l2_loss(x, reduce: DimFilter = math.non_batch) -> Tensor:
    """
    Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>2</sub><sup>2</sup> / 2*, summing over all non-batch dimensions.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` or 0D or 1D native tensor.
            For `phiml.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed.
        reduce: Dimensions to reduce as `DimFilter`.

    Returns:
        loss: `Tensor`
    """
    if isinstance(x, Tensor):
        if x.dtype.kind == complex:
            x = abs(x)
        return math.sum_(x ** 2, reduce) * 0.5
    elif isinstance(x, PhiTreeNode):
        return sum([l2_loss(getattr(x, a), reduce) for a in variable_values(x)])
    else:
        try:
            backend = math.choose_backend(x)
            shape = backend.staticshape(x)
            if len(shape) == 0:
                return x ** 2 * 0.5
            elif len(shape) == 1:
                return backend.sum(x ** 2) * 0.5
            else:
                raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-ML tensors.")
        except math.NoBackendFound:
            raise ValueError(x)
def laplace(field: ~GridType, axes=<function spatial>, order=2, implicit: phiml.math._optimize.Solve = None, weights: Union[phiml.math._tensors.Tensor, phi.field._field.Field] = None) ‑> ~GridType

Spatial Laplace operator for scalar grid. If a vector grid is passed, it is assumed to be centered and the laplace is computed component-wise.

Args

field
n-dimensional CenteredGrid
axes
The second derivative along these dimensions is summed over
weights
(Optional) Multiply the axis terms by these factors before summation. Must be a phiml.math.Tensor or Field with a single channel dimension that lists all laplace axes by name.
order
Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
When a Solve object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.

Returns

laplacian field as CenteredGrid

Expand source code
def laplace(field: GridType,
            axes=spatial,
            order=2,
            implicit: math.Solve = None,
            weights: Union[Tensor, Field] = None) -> GridType:
    """
    Spatial Laplace operator for scalar grid.
    If a vector grid is passed, it is assumed to be centered and the laplace is computed component-wise.

    Args:
        field: n-dimensional `CenteredGrid`
        axes: The second derivative along these dimensions is summed over
        weights: (Optional) Multiply the axis terms by these factors before summation.
            Must be a `phiml.math.Tensor` or `phi.field.Field` with a single channel dimension that lists all laplace axes by name.
        order: Spatial order of accuracy.
            Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution.
            Supported: 2 explicit, 4 explicit, 6 implicit.
        implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances.
            Otherwise, an explicit stencil is used.

    Returns:
        laplacian field as `CenteredGrid`
    """
    if isinstance(weights, Field):
        weights = weights.at(field).values
    axes_names = field.shape.only(axes).names
    extrap_map = {}
    if not implicit:
        if order == 2:
                values, needed_shifts = [1, -2, 1], (-1, 0, 1)

        elif order == 4:
                values, needed_shifts = [-1/12, 4/3, -5/2, 4/3, -1/12], (-2, -1, 0, 1, 2)
    else:
        extrap_map_rhs = {}
        if order == 6:
            values, needed_shifts = [3/44, 12/11, -51/22, 12/11, 3/44], (-2, -1, 0, 1, 2)
            extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC)
            values_rhs, needed_shifts_rhs = [2/11, 1, 2/11], (-1, 0, 1)
            extrap_map_rhs['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC)
    base_widths = (abs(min(needed_shifts)), max(needed_shifts))
    field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation))
    padded_components = [pad(field, {dim: base_widths}) for dim in axes_names]
    shifted_components = [shift(padded_component, needed_shifts, None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, axes_names)]
    result_components = [sum([value * shift_ for value, shift_ in zip(values, shifted_component)]) / field.dx.vector[dim]**2 for shifted_component, dim in zip(shifted_components, axes_names)]
    if implicit:
        result_components = stack(result_components, channel('laplacian'))
        result_components.with_values(result_components.values._cache())
        result_components = result_components.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map_rhs), field.extrapolation))
        implicit.x0 = result_components
        result_components = solve_linear(_lhs_for_implicit_scheme, result_components, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=channel('laplacian'))
        result_components = unstack(result_components, 'laplacian')
        extrap_map = extrap_map_rhs
    result_components = [component.with_bounds(field.bounds) for component in result_components]
    if weights is not None:
        assert channel(weights).rank == 1 and channel(weights).item_names is not None, f"weights must have one channel dimension listing the laplace dims but got {shape(weights)}"
        assert set(channel(weights).item_names[0]) >= set(axes_names), f"the channel dim of weights must contain all laplace dims {axes_names} but only has {channel(weights).item_names}"
        result_components = [c * weights[ax] for c, ax in zip(result_components, axes_names)]
    result = sum(result_components)
    result = result.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation))
    return result
def mask(obj: Union[~SampledFieldType, phi.geom._geom.Geometry]) ‑> ~SampledFieldType

Returns a Field that masks the inside (or non-zero values when obj is a grid) of a physical object. The mask takes the value 1 inside the object and 0 outside. For CenteredGrid and StaggeredGrid, the mask labels non-zero non-NaN entries as 1 and all other values as 0

Returns

Grid type or PointCloud

Expand source code
def mask(obj: Union[SampledFieldType, Geometry]) -> SampledFieldType:
    """
    Returns a `Field` that masks the inside (or non-zero values when `obj` is a grid) of a physical object.
    The mask takes the value 1 inside the object and 0 outside.
    For `CenteredGrid` and `StaggeredGrid`, the mask labels non-zero non-NaN entries as 1 and all other values as 0

    Returns:
        `Grid` type or `PointCloud`
    """
    if isinstance(obj, PointCloud):
        return PointCloud(obj.elements, 1, math.extrapolation.remove_constant_offset(obj.extrapolation), bounds=obj.bounds)
    elif isinstance(obj, Geometry):
        return PointCloud(obj, 1, 0)
    elif isinstance(obj, CenteredGrid):
        values = math.cast(obj.values != 0, int)
        return obj.with_values(values)
    else:
        raise ValueError(obj)
def maximum(f1: Union[phi.field._field.Field, phi.geom._geom.Geometry, float], f2: Union[phi.field._field.Field, phi.geom._geom.Geometry, float])

Element-wise maximum. One of the given fields needs to be an instance of SampledField and the the result will be sampled at the corresponding points. If both are SampledFields but have different points, f1 takes priority.

Args

f1
Field or Geometry or constant.
f2
Field or Geometry or constant.

Returns

SampledField

Expand source code
def maximum(f1: Union[Field, Geometry, float], f2: Union[Field, Geometry, float]):
    """
    Element-wise maximum.
    One of the given fields needs to be an instance of `SampledField` and the the result will be sampled at the corresponding points.
    If both are `SampledFields` but have different points, `f1` takes priority.

    Args:
        f1: `Field` or `Geometry` or constant.
        f2: `Field` or `Geometry` or constant.

    Returns:
        `SampledField`
    """
    f1, f2 = _auto_resample(f1, f2)
    return f1.with_values(math.maximum(f1.values, f2.values))
def mean(field: phi.field._field.SampledField) ‑> phiml.math._tensors.Tensor

Computes the mean value by reducing all spatial / instance dimensions.

Args

field
SampledField

Returns

phi.Tensor

Expand source code
def mean(field: SampledField) -> Tensor:
    """
    Computes the mean value by reducing all spatial / instance dimensions.

    Args:
        field: `SampledField`

    Returns:
        `phi.Tensor`
    """
    return math.mean(field.values, field.shape.non_channel.non_batch)
def minimize(f: Callable[[~X], ~Y], solve: phiml.math._optimize.Solve[~X, ~Y]) ‑> ~X

Finds a minimum of the scalar function f(x). The method argument of solve determines which optimizer is used. All optimizers supported by scipy.optimize.minimize are supported, see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html . Additionally a gradient descent solver with adaptive step size can be used with method='GD'.

math.minimize() is limited to backends that support jacobian(), i.e. PyTorch, TensorFlow and Jax.

To obtain additional information about the performed solve, use a SolveTape.

See Also: solve_nonlinear().

Args

f
Function whose output is subject to minimization. All positional arguments of f are optimized and must be Tensor or phiml.math.magic.PhiTreeNode. If solve.x0 is a tuple or list, it will be passed to f as varargs, f(*x0). To minimize a subset of the positional arguments, define a new (lambda) function depending only on those. The first return value of f must be a scalar float Tensor or phiml.math.magic.PhiTreeNode.
solve
Solve object to specify method type, parameters and initial guess for x.

Returns

x
solution, the minimum point x.

Raises

NotConverged
If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
If the optimization failed prematurely.
Expand source code
def minimize(f: Callable[[X], Y], solve: Solve[X, Y]) -> X:
    """
    Finds a minimum of the scalar function *f(x)*.
    The `method` argument of `solve` determines which optimizer is used.
    All optimizers supported by `scipy.optimize.minimize` are supported,
    see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html .
    Additionally a gradient descent solver with adaptive step size can be used with `method='GD'`.

    `math.minimize()` is limited to backends that support `jacobian()`, i.e. PyTorch, TensorFlow and Jax.

    To obtain additional information about the performed solve, use a `SolveTape`.

    See Also:
        `solve_nonlinear()`.

    Args:
        f: Function whose output is subject to minimization.
            All positional arguments of `f` are optimized and must be `Tensor` or `phiml.math.magic.PhiTreeNode`.
            If `solve.x0` is a `tuple` or `list`, it will be passed to *f* as varargs, `f(*x0)`.
            To minimize a subset of the positional arguments, define a new (lambda) function depending only on those.
            The first return value of `f` must be a scalar float `Tensor` or `phiml.math.magic.PhiTreeNode`.
        solve: `Solve` object to specify method type, parameters and initial guess for `x`.

    Returns:
        x: solution, the minimum point `x`.

    Raises:
        NotConverged: If the desired accuracy was not be reached within the maximum number of iterations.
        Diverged: If the optimization failed prematurely.
    """
    solve = solve.with_defaults('optimization')
    assert (solve.rel_tol == 0).all, f"rel_tol must be zero for minimize() but got {solve.rel_tol}"
    assert solve.preprocess_y is None, "minimize() does not allow preprocess_y"
    x0_nest, x0_tensors = disassemble_tree(solve.x0)
    x0_tensors = [to_float(t) for t in x0_tensors]
    backend = choose_backend_t(*x0_tensors, prefer_default=True)
    batch_dims = merge_shapes(*[t.shape for t in x0_tensors]).batch
    x0_natives = []
    for t in x0_tensors:
        t._expand()
        assert t.shape.is_uniform
        x0_natives.append(reshaped_native(t, [batch_dims, t.shape.non_batch]))
    x0_flat = backend.concat(x0_natives, -1)

    def unflatten_assemble(x_flat, additional_dims: Shape = EMPTY_SHAPE, convert=True):
        i = 0
        x_tensors = []
        for x0_native, x0_tensor in zip(x0_natives, x0_tensors):
            vol = backend.shape(x0_native)[-1]
            flat_native = x_flat[..., i:i + vol]
            x_tensors.append(reshaped_tensor(flat_native, [*additional_dims, batch_dims, x0_tensor.shape.non_batch], convert=convert))
            i += vol
        x = assemble_tree(x0_nest, x_tensors)
        return x

    def native_function(x_flat):
        x = unflatten_assemble(x_flat)
        if isinstance(x, (tuple, list)):
            y = f(*x)
        else:
            y = f(x)
        _, y_tensors = disassemble_tree(y)
        assert not non_batch(y_tensors[0]), f"Failed to minimize '{f.__name__}' because it returned a non-scalar output {shape(y_tensors[0])}. Reduce all non-batch dimensions, e.g. using math.l2_loss()"
        try:
            loss_native = reshaped_native(y_tensors[0], [batch_dims], force_expand=False)
        except AssertionError:
            raise AssertionError(f"Failed to minimize '{f.__name__}' because its output loss {shape(y_tensors[0])} has more batch dimensions than the initial guess {batch_dims}.")
        return y_tensors[0].sum, (loss_native,)

    atol = backend.to_float(reshaped_native(solve.abs_tol, [batch_dims]))
    maxi = reshaped_numpy(solve.max_iterations, [batch_dims])
    trj = _SOLVE_TAPES and any(t.should_record_trajectory_for(solve) for t in _SOLVE_TAPES)
    t = time.perf_counter()
    ret = backend.minimize(solve.method, native_function, x0_flat, atol, maxi, trj)
    t = time.perf_counter() - t
    if not trj:
        assert isinstance(ret, SolveResult)
        converged = reshaped_tensor(ret.converged, [batch_dims])
        diverged = reshaped_tensor(ret.diverged, [batch_dims])
        x = unflatten_assemble(ret.x)
        iterations = reshaped_tensor(ret.iterations, [batch_dims])
        function_evaluations = reshaped_tensor(ret.function_evaluations, [batch_dims])
        residual = reshaped_tensor(ret.residual, [batch_dims])
        result = SolveInfo(solve, x, residual, iterations, function_evaluations, converged, diverged, ret.method, ret.message, t)
    else:  # trajectory
        assert isinstance(ret, (tuple, list)) and all(isinstance(r, SolveResult) for r in ret)
        converged = reshaped_tensor(ret[-1].converged, [batch_dims])
        diverged = reshaped_tensor(ret[-1].diverged, [batch_dims])
        x = unflatten_assemble(ret[-1].x)
        x_ = unflatten_assemble(numpy.stack([r.x for r in ret]), additional_dims=batch('trajectory'), convert=False)
        residual = stack([reshaped_tensor(r.residual, [batch_dims]) for r in ret], batch('trajectory'))
        iterations = reshaped_tensor(ret[-1].iterations, [batch_dims])
        function_evaluations = stack([reshaped_tensor(r.function_evaluations, [batch_dims]) for r in ret], batch('trajectory'))
        result = SolveInfo(solve, x_, residual, iterations, function_evaluations, converged, diverged, ret[-1].method, ret[-1].message, t)
    for tape in _SOLVE_TAPES:
        tape._add(solve, trj, result)
    result.convergence_check(False)  # raises ConvergenceException
    return x
def minimum(f1: Union[phi.field._field.Field, phi.geom._geom.Geometry, float], f2: Union[phi.field._field.Field, phi.geom._geom.Geometry, float])

Element-wise minimum. One of the given fields needs to be an instance of SampledField and the the result will be sampled at the corresponding points. If both are SampledFields but have different points, f1 takes priority.

Args

f1
Field or Geometry or constant.
f2
Field or Geometry or constant.

Returns

SampledField

Expand source code
def minimum(f1: Union[Field, Geometry, float], f2: Union[Field, Geometry, float]):
    """
    Element-wise minimum.
    One of the given fields needs to be an instance of `SampledField` and the the result will be sampled at the corresponding points.
    If both are `SampledFields` but have different points, `f1` takes priority.

    Args:
        f1: `Field` or `Geometry` or constant.
        f2: `Field` or `Geometry` or constant.

    Returns:
        `SampledField`
    """
    f1, f2 = _auto_resample(f1, f2)
    return f1.with_values(math.minimum(f1.values, f2.values))
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) ‑> Union[phi.field._field.SampledField, phiml.math._tensors.Tensor]

Similar to phiml.math.native_call().

Args

f
Function to be called on native tensors of inputs.values. The function output must have the same dimension layout as the inputs and the batch size must be identical.
*inputs
SampledField or phi.Tensor instances.
extrapolation
(Optional) Extrapolation of the output field. If None, uses the extrapolation of the first input field.

Returns

SampledField matching the first SampledField in inputs.

Expand source code
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) -> Union[SampledField, Tensor]:
    """
    Similar to `phiml.math.native_call()`.

    Args:
        f: Function to be called on native tensors of `inputs.values`.
            The function output must have the same dimension layout as the inputs and the batch size must be identical.
        *inputs: `SampledField` or `phi.Tensor` instances.
        extrapolation: (Optional) Extrapolation of the output field. If `None`, uses the extrapolation of the first input field.

    Returns:
        `SampledField` matching the first `SampledField` in `inputs`.
    """
    input_tensors = [i.values if isinstance(i, SampledField) else tensor(i) for i in inputs]
    values = math.native_call(f, *input_tensors, channels_last=channels_last, channel_dim=channel_dim)
    for i in inputs:
        if isinstance(i, SampledField):
            result = i.with_values(values=values)
            if extrapolation is not None:
                result = result.with_extrapolation(extrapolation)
            return result
    else:
        raise AssertionError("At least one input must be a SampledField.")
def normalize(field: phi.field._field.SampledField, norm: phi.field._field.SampledField, epsilon=1e-05)

Multiplies the values of field so that its sum matches the source.

Expand source code
def normalize(field: SampledField, norm: SampledField, epsilon=1e-5):
    """ Multiplies the values of `field` so that its sum matches the source. """
    data = math.normalize_to(field.values, norm.values, epsilon)
    return field.with_values(data)
def pack_dims(field: ~SampledFieldType, dims: Union[phiml.math._shape.Shape, tuple, list, str], packed_dim: phiml.math._shape.Shape, pos: Optional[int] = None) ‑> ~SampledFieldType

Currently only supports grids and non-spatial dimensions.

See Also: phiml.math.pack_dims().

Args

field
SampledField

Returns

SampledField of same type as field.

Expand source code
def pack_dims(field: SampledFieldType,
              dims: Union[Shape, tuple, list, str],
              packed_dim: Shape,
              pos: Union[int, None] = None) -> SampledFieldType:
    """
    Currently only supports grids and non-spatial dimensions.

    See Also:
        `phiml.math.pack_dims()`.

    Args:
        field: `SampledField`

    Returns:
        `SampledField` of same type as `field`.
    """
    if isinstance(field, Grid):
        if spatial(field.shape.only(dims)):
            raise NotImplementedError("Packing spatial dimensions not supported for grids")
        return field.with_values(math.pack_dims(field.values, dims, packed_dim, pos))
    else:
        raise NotImplementedError()
def pad(grid: ~GridType, widths: Union[int, tuple, list, dict]) ‑> ~GridType

Pads a Grid using its extrapolation.

Unlike phiml.math.pad(), this function also affects the bounds of the grid, changing its size and origin depending on widths.

Args

grid
CenteredGrid or StaggeredGrid
widths
Either int or (lower, upper) to pad the same number of cells in all spatial dimensions or dict mapping dimension names to (lower, upper).

Returns

Grid of the same type as grid

Expand source code
def pad(grid: GridType, widths: Union[int, tuple, list, dict]) -> GridType:
    """
    Pads a `Grid` using its extrapolation.

    Unlike `phiml.math.pad()`, this function also affects the `bounds` of the grid, changing its size and origin depending on `widths`.

    Args:
        grid: `CenteredGrid` or `StaggeredGrid`
        widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions
            or `dict` mapping dimension names to `(lower, upper)`.

    Returns:
        `Grid` of the same type as `grid`
    """
    if isinstance(widths, int):
        widths = {axis: (widths, widths) for axis in grid.shape.spatial.names}
    elif isinstance(widths, (tuple, list)):
        widths = {axis: (width if isinstance(width, (tuple, list)) else (width, width)) for axis, width in zip(grid.shape.spatial.names, widths)}
    else:
        assert isinstance(widths, dict)
    widths_list = [widths[axis] if axis in widths.keys() else (0, 0) for axis in grid.shape.spatial.names]
    if isinstance(grid, Grid):
        data = math.pad(grid.values, widths, grid.extrapolation, bounds=grid.bounds)
        w_lower = math.wrap([w[0] for w in widths_list])
        w_upper = math.wrap([w[1] for w in widths_list])
        bounds = Box(grid.box.lower - w_lower * grid.dx, grid.box.upper + w_upper * grid.dx)
        return type(grid)(values=data, bounds=bounds, extrapolation=grid.extrapolation)
    raise NotImplementedError(f"{type(grid)} not supported. Only Grid instances allowed.")
def read(file: Union[str, phiml.math._tensors.Tensor], convert_to_backend=True) ‑> phi.field._field.SampledField

Loads a previously saved SampledField from disc.

See Also: write().

Args

file
Single file as str or Tensor of string type. If file is a tensor, all contained files are loaded an stacked according to the dimensions of file.
convert_to_backend
Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

Returns

Loaded SampledField.

Expand source code
def read(file: Union[str, Tensor], convert_to_backend=True) -> SampledField:
    """
    Loads a previously saved `SampledField` from disc.

    See Also:
        `write()`.

    Args:
        file: Single file as `str` or `Tensor` of string type.
            If `file` is a tensor, all contained files are loaded an stacked according to the dimensions of `file`.
        convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

    Returns:
        Loaded `SampledField`.
    """
    if isinstance(file, str):
        return read_single_field(file, convert_to_backend=convert_to_backend)
    if isinstance(file, Tensor):
        if file.rank == 0:
            return read_single_field(file.native(), convert_to_backend=convert_to_backend)
        else:
            dim = file.shape[0]
            files = unstack(file, dim)
            fields = [read(file_, convert_to_backend=convert_to_backend) for file_ in files]
            return stack(fields, dim)
    else:
        raise ValueError(file)
def real(x: ~TensorOrTree) ‑> ~TensorOrTree

See Also: imag(), conjugate().

Args

x
Tensor or phiml.math.magic.PhiTreeNode or native tensor.

Returns

Real component of x.

Expand source code
def real(x: TensorOrTree) -> TensorOrTree:
    """
    See Also:
        `imag()`, `conjugate()`.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` or native tensor.

    Returns:
        Real component of `x`.
    """
    return _backend_op1(x, Backend.real)
def reduce_sample(field: Union[phi.field._field.Field, phi.geom._geom.Geometry], geometry: Union[phi.geom._geom.Geometry, phi.field._field.SampledField, phiml.math._tensors.Tensor], dim=(vectorᶜ=None), **kwargs) ‑> phiml.math._tensors.Tensor

Similar to sample(), but matches channel dimensions of geometry with channel dimensions of this field. Currently, geometry may have at most one channel dimension.

See Also: sample(), Field.at(), Resampling overview.

Args

field
Source Field to sample.
geometry
Single or batched Geometry or SampledField or location Tensor. When passing a SampledField, its elements are used as sample points. When passing a vector-valued Tensor, a Point geometry will be created.
dim
Dimension of result, resulting from reduction of channel dimensions.
**kwargs
Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.

Returns

Sampled values as a phiml.math.Tensor

Expand source code
def reduce_sample(field: Union[Field, Geometry],
                  geometry: Union[Geometry, SampledField, Tensor],
                  dim=channel('vector'),
                  **kwargs) -> math.Tensor:
    """
    Similar to `sample()`, but matches channel dimensions of `geometry` with channel dimensions of this field.
    Currently, `geometry` may have at most one channel dimension.

    See Also:
        `sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields).

    Args:
        field: Source `Field` to sample.
        geometry: Single or batched `phi.geom.Geometry` or `SampledField` or location `Tensor`.
            When passing a `SampledField`, its `elements` are used as sample points.
            When passing a vector-valued `Tensor`, a `Point` geometry will be created.
        dim: Dimension of result, resulting from reduction of channel dimensions.
        **kwargs: Sampling arguments, e.g. to specify the numerical scheme.
            By default, linear interpolation is used.
            Grids also support 6th order implicit sampling at mid-points.

    Returns:
        Sampled values as a `phiml.math.Tensor`
    """
    geometry = _get_geometry(geometry)
    if isinstance(field, Geometry):
        from ._field_math import mask
        field = mask(field)
    if isinstance(field, SampledField) and field.elements.shallow_equals(geometry):
        return field.values
    if channel(geometry).without('vector'):  # Reduce this dimension
        geom_ch = channel(geometry).without('vector')
        assert geom_ch.rank == 1, "Only single-dimension reduction supported."
        if field.shape.channel.volume > 1:
            assert field.shape.channel.volume == geom_ch.volume, f"Cannot sample field with channels {field.shape.channel} at elements with channels {geometry.shape.channel}."
            components = math.unstack(field, field.shape.channel.name)
            sampled = [c._sample(p, **kwargs) for c, p in zip(components, geometry.unstack(geom_ch.name))]
        else:
            sampled = [field._sample(p, **kwargs) for p in geometry.unstack(channel(geometry).without('vector').name)]
        dim = dim.with_size(geometry.shape.channel.item_names[0])
        return math.stack(sampled, dim)
    else:  # Nothing to reduce
        return field._sample(geometry, **kwargs)
def resample(value: Union[phi.field._field.Field, phi.geom._geom.Geometry, phiml.math._tensors.Tensor, float], to: phi.field._field.SampledField, keep_extrapolation=False, **kwargs)

Samples a Field, Geometry or value at the sample points of the field to. The result will approximate value on the data structure of to. Unlike sample(), this method returns a Field object, not a Tensor.

Aliases

value.at(to), (and the deprecated value @ to).

See Also: sample(), reduce_sample(), Field.at(), Resampling overview.

Args

value
Object containing values to resample. This can be
to
SampledField (CenteredGrid, StaggeredGrid or PointCloud) object defining the sample points. The current values of to are ignored.
keep_extrapolation
Only available if self is a SampledField. If True, the resampled field will inherit the extrapolation from self instead of representation. This can result in non-compatible value tensors for staggered grids where the tensor size depends on the extrapolation type.
**kwargs
Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.

Returns

Field object of same type as representation

Examples

>>> grid = CenteredGrid(x=64, y=32)
>>> field.resample(Noise(), to=grid)
CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
>>> field.resample(1, to=grid)
CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
>>> field.resample(Box(x=1, y=2), to=grid)
CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
>>> field.resample(grid, to=grid) == grid
True
Expand source code
def resample(value: Union[Field, Geometry, Tensor, float], to: SampledField, keep_extrapolation=False, **kwargs):
    """
    Samples a `Field`, `Geometry` or value at the sample points of the field `to`.
    The result will approximate `value` on the data structure of `to`.
    Unlike `sample()`, this method returns a `Field` object, not a `Tensor`.

    Aliases:
        `value.at(to)`, (and the deprecated `value @ to`).

    See Also:
        `sample()`, `reduce_sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields).

    Args:
        value: Object containing values to resample.
            This can be
        to: `SampledField` (`CenteredGrid`, `StaggeredGrid` or `PointCloud`) object defining the sample points.
            The current values of `to` are ignored.
        keep_extrapolation: Only available if `self` is a `SampledField`.
            If True, the resampled field will inherit the extrapolation from `self` instead of `representation`.
            This can result in non-compatible value tensors for staggered grids where the tensor size depends on the extrapolation type.
        **kwargs: Sampling arguments, e.g. to specify the numerical scheme.
            By default, linear interpolation is used.
            Grids also support 6th order implicit sampling at mid-points.

    Returns:
        Field object of same type as `representation`

    Examples:
        >>> grid = CenteredGrid(x=64, y=32)
        >>> field.resample(Noise(), to=grid)
        CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
        >>> field.resample(1, to=grid)
        CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
        >>> field.resample(Box(x=1, y=2), to=grid)
        CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0]
        >>> field.resample(grid, to=grid) == grid
        True
    """
    if not isinstance(value, (Field, Geometry)):
        return to.with_values(value)
    resampled = reduce_sample(value, to.elements, **kwargs)
    extrap = value.extrapolation if isinstance(value, SampledField) and keep_extrapolation else to.extrapolation
    return to.with_values(resampled).with_extrapolation(extrap)
def round(x: ~TensorOrTree) ‑> ~TensorOrTree

Rounds the Tensor or phiml.math.magic.PhiTreeNode x to the closest integer.

Expand source code
def round_(x: TensorOrTree) -> TensorOrTree:
    """ Rounds the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to the closest integer. """
    return _backend_op1(x, Backend.round)
def sample(field: Union[phi.field._field.Field, phi.geom._geom.Geometry], geometry: Union[phi.geom._geom.Geometry, phi.field._field.SampledField, phiml.math._tensors.Tensor], **kwargs) ‑> phiml.math._tensors.Tensor

Computes the field value inside the volume of the (batched) geometry.

The field value may be determined by integrating over the volume, sampling the central value or any other way.

The batch dimensions of geometry are matched with this field. The geometry must not share any channel dimensions with this field. Spatial dimensions of geometry can be used to sample a grid of geometries.

See Also: reduce_sample(), Field.at(), Resampling overview.

Args

field
Source Field to sample.
geometry
Single or batched Geometry or SampledField or location Tensor. When passing a SampledField, its elements are used as sample points. When passing a vector-valued Tensor, a Point geometry will be created.
**kwargs
Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.

Returns

Sampled values as a phiml.math.Tensor

Expand source code
def sample(field: Union[Field, Geometry],
           geometry: Union[Geometry, SampledField, Tensor],
           **kwargs) -> math.Tensor:
    """
    Computes the field value inside the volume of the (batched) `geometry`.

    The field value may be determined by integrating over the volume, sampling the central value or any other way.

    The batch dimensions of `geometry` are matched with this field.
    The `geometry` must not share any channel dimensions with this field.
    Spatial dimensions of `geometry` can be used to sample a grid of geometries.

    See Also:
        `reduce_sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields).

    Args:
        field: Source `Field` to sample.
        geometry: Single or batched `phi.geom.Geometry` or `SampledField` or location `Tensor`.
            When passing a `SampledField`, its `elements` are used as sample points.
            When passing a vector-valued `Tensor`, a `Point` geometry will be created.
        **kwargs: Sampling arguments, e.g. to specify the numerical scheme.
            By default, linear interpolation is used.
            Grids also support 6th order implicit sampling at mid-points.

    Returns:
        Sampled values as a `phiml.math.Tensor`
    """
    geometry = _get_geometry(geometry)
    if isinstance(field, Geometry):
        from ._field_math import mask
        field = mask(field)
    geom_ch = channel(geometry).without('vector')
    assert all(dim not in field.shape for dim in geom_ch)
    if isinstance(field, SampledField) and field.elements.shallow_equals(geometry) and not geom_ch:
        return field.values
    if geom_ch:
        sampled = [field._sample(p, **kwargs) for p in geometry.unstack(geom_ch.name)]
        return math.stack(sampled, geom_ch)
    else:
        return field._sample(geometry, **kwargs)
def shift(grid: phi.field._grid.CenteredGrid, offsets: tuple, stack_dim: Optional[phiml.math._shape.Shape] = (shiftᶜ=None), dims=<function spatial>, pad=True)

Wraps :func:math.shift for CenteredGrid.

Args

grid
CenteredGrid:
offsets
tuple:
stack_dim
(Default value = 'shift')
Expand source code
def shift(grid: CenteredGrid, offsets: tuple, stack_dim: Optional[Shape] = channel('shift'), dims=spatial, pad=True):
    """
    Wraps :func:`math.shift` for CenteredGrid.

    Args:
      grid: CenteredGrid: 
      offsets: tuple: 
      stack_dim:  (Default value = 'shift')
    """
    if pad:
        padding = grid.extrapolation
        new_bounds = grid.bounds
    else:
        padding = None
        max_lower_shift = min(offsets) if min(offsets) < 0 else 0
        max_upper_shift = max(offsets) if max(offsets) > 0 else 0
        w_lower = math.wrap([max_lower_shift if dim in dims else 0 for dim in grid.shape.spatial.names])
        w_upper = math.wrap([max_upper_shift if dim in dims else 0 for dim in grid.shape.spatial.names])
        new_bounds = Box(grid.box.lower - w_lower * grid.dx, grid.box.upper - w_upper * grid.dx)
    data = math.shift(grid.values, offsets, dims=dims, padding=padding, stack_dim=stack_dim)
    return [type(grid)(data[i], bounds=new_bounds, extrapolation=grid.extrapolation) for i in range(len(offsets))]
def sign(x: ~TensorOrTree) ‑> ~TensorOrTree

The sign of positive numbers is 1 and -1 for negative numbers. The sign of 0 is undefined.

Args

x
Tensor or phiml.math.magic.PhiTreeNode

Returns

Tensor or phiml.math.magic.PhiTreeNode matching x.

Expand source code
def sign(x: TensorOrTree) -> TensorOrTree:
    """
    The sign of positive numbers is 1 and -1 for negative numbers.
    The sign of 0 is undefined.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode`

    Returns:
        `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x`.
    """
    return _backend_op1(x, Backend.sign)
def sin(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes sin(x) of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def sin(x: TensorOrTree) -> TensorOrTree:
    """ Computes *sin(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.sin)
def solve_linear(f: Union[Callable[[~X], ~Y], phiml.math._tensors.Tensor], y: ~Y, solve: phiml.math._optimize.Solve[~X, ~Y], *f_args, grad_for_f=False, f_kwargs: dict = None, **f_kwargs_) ‑> ~X

Solves the system of linear equations f(x) = y and returns x. This method will use the solver specified in solve. The following method identifiers are supported by all backends:

  • 'auto': Automatically choose a solver
  • 'CG': Conjugate gradient, only for symmetric and positive definite matrices.
  • 'CG-adaptive': Conjugate gradient with adaptive step size, only for symmetric and positive definite matrices.
  • 'biCG' or 'biCG-stab(0)': Biconjugate gradient
  • 'biCG-stab' or 'biCG-stab(1)': Biconjugate gradient stabilized, first order
  • 'biCG-stab(2)', 'biCG-stab(4)', …: Biconjugate gradient stabilized, second or higher order
  • 'scipy-direct': SciPy direct solve always run oh the CPU using scipy.sparse.linalg.spsolve.
  • 'scipy-CG', 'scipy-GMres', 'scipy-biCG', 'scipy-biCG-stab', 'scipy-CGS', 'scipy-QMR', 'scipy-GCrotMK': SciPy iterative solvers always run oh the CPU.

Caution: SciPy solvers cannot be jit-compiled and should only be used for debugging purposes.

For maximum performance, compile f using jit_compile_linear() beforehand. Then, an optimized representation of f (such as a sparse matrix) will be used to solve the linear system.

To obtain additional information about the performed solve, perform the solve within a SolveTape context. The used implementation can be obtained as SolveInfo.method.

The gradient of this operation will perform another linear solve with the parameters specified by Solve.gradient_solve.

See Also: solve_nonlinear(), jit_compile_linear().

Args

f

One of the following:

  • Linear function with Tensor or phiml.math.magic.PhiTreeNode first parameter and return value. f can have additional auxiliary arguments and return auxiliary values.
  • Dense matrix (Tensor with at least one dual dimension)
  • Sparse matrix (Sparse Tensor with at least one dual dimension)
  • Native tensor (not yet supported)
y
Desired output of f(x) as Tensor or phiml.math.magic.PhiTreeNode.
solve
Solve object specifying optimization method, parameters and initial guess for x.
*f_args
Positional arguments to be passed to f after solve.x0. These arguments will not be solved for. Supports vararg mode or pass all arguments as a tuple.
f_kwargs
Additional keyword arguments to be passed to f. These arguments are treated as auxiliary arguments and can be of any type.

Returns

x
solution of the linear system of equations f(x) = y as Tensor or phiml.math.magic.PhiTreeNode.

Raises

NotConverged
If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
If the solve failed prematurely.
Expand source code
def solve_linear(f: Union[Callable[[X], Y], Tensor],
                 y: Y,
                 solve: Solve[X, Y],
                 *f_args,
                 grad_for_f=False,
                 f_kwargs: dict = None,
                 **f_kwargs_) -> X:
    """
    Solves the system of linear equations *f(x) = y* and returns *x*.
    This method will use the solver specified in `solve`.
    The following method identifiers are supported by all backends:

    * `'auto'`: Automatically choose a solver
    * `'CG'`: Conjugate gradient, only for symmetric and positive definite matrices.
    * `'CG-adaptive'`: Conjugate gradient with adaptive step size, only for symmetric and positive definite matrices.
    * `'biCG'` or `'biCG-stab(0)'`: Biconjugate gradient
    * `'biCG-stab'` or `'biCG-stab(1)'`: Biconjugate gradient stabilized, first order
    * `'biCG-stab(2)'`, `'biCG-stab(4)'`, ...: Biconjugate gradient stabilized, second or higher order
    * `'scipy-direct'`: SciPy direct solve always run oh the CPU using `scipy.sparse.linalg.spsolve`.
    * `'scipy-CG'`, `'scipy-GMres'`, `'scipy-biCG'`, `'scipy-biCG-stab'`, `'scipy-CGS'`, `'scipy-QMR'`, `'scipy-GCrotMK'`: SciPy iterative solvers always run oh the CPU.

    **Caution**: SciPy solvers cannot be jit-compiled and should only be used for debugging purposes.

    For maximum performance, compile `f` using `jit_compile_linear()` beforehand.
    Then, an optimized representation of `f` (such as a sparse matrix) will be used to solve the linear system.

    To obtain additional information about the performed solve, perform the solve within a `SolveTape` context.
    The used implementation can be obtained as `SolveInfo.method`.

    The gradient of this operation will perform another linear solve with the parameters specified by `Solve.gradient_solve`.

    See Also:
        `solve_nonlinear()`, `jit_compile_linear()`.

    Args:
        f: One of the following:

            * Linear function with `Tensor` or `phiml.math.magic.PhiTreeNode` first parameter and return value. `f` can have additional auxiliary arguments and return auxiliary values.
            * Dense matrix (`Tensor` with at least one dual dimension)
            * Sparse matrix (Sparse `Tensor` with at least one dual dimension)
            * Native tensor (not yet supported)

        y: Desired output of `f(x)` as `Tensor` or `phiml.math.magic.PhiTreeNode`.
        solve: `Solve` object specifying optimization method, parameters and initial guess for `x`.
        *f_args: Positional arguments to be passed to `f` after `solve.x0`. These arguments will not be solved for.
            Supports vararg mode or pass all arguments as a `tuple`.
        f_kwargs: Additional keyword arguments to be passed to `f`.
            These arguments are treated as auxiliary arguments and can be of any type.

    Returns:
        x: solution of the linear system of equations `f(x) = y` as `Tensor` or `phiml.math.magic.PhiTreeNode`.

    Raises:
        NotConverged: If the desired accuracy was not be reached within the maximum number of iterations.
        Diverged: If the solve failed prematurely.
    """
    # --- Handle parameters ---
    f_kwargs = f_kwargs or {}
    f_kwargs.update(f_kwargs_)
    f_args = f_args[0] if len(f_args) == 1 and isinstance(f_args[0], tuple) else f_args
    # --- Get input and output tensors ---
    y_tree, y_tensors = disassemble_tree(y)
    x0_tree, x0_tensors = disassemble_tree(solve.x0)
    assert solve.x0 is not None, "Please specify the initial guess as Solve(..., x0=initial_guess)"
    assert len(x0_tensors) == len(y_tensors) == 1, "Only single-tensor linear solves are currently supported"
    if y_tree == 'native' and x0_tree == 'native':
        if callable(f):  # assume batch + 1 dim
            rank = y_tensors[0].rank
            assert x0_tensors[0].rank == rank, f"y and x0 must have the same rank but got {y_tensors[0].shape.sizes} for y and {x0_tensors[0].shape.sizes} for x0"
            y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'))
            x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'))
            solve = copy_with(solve, x0=x0)
            solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_)
            return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector')
        else:
            b = choose_backend(y, solve.x0, f)
            f_dims = b.staticshape(f)
            y_dims = b.staticshape(y)
            x_dims = b.staticshape(solve.x0)
            rank = len(f_dims) - 2
            assert rank >= 0, f"f must be a matrix but got shape {f_dims}"
            f = wrap(f, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), dual('vector'))
            if len(x_dims) == len(f_dims):  # matrix solve
                assert len(x_dims) == len(f_dims)
                assert x_dims[-2] == f_dims[-1]
                assert y_dims[-2] == f_dims[-2]
                y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), batch('extra_batch'))
                x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), batch('extra_batch'))
                solve = copy_with(solve, x0=x0)
                solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_)
                return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector,extra_batch')
            else:
                assert len(x_dims) == len(f_dims) - 1
                assert x_dims[-1] == f_dims[-1]
                assert y_dims[-1] == f_dims[-2]
                y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'))
                x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'))
                solve = copy_with(solve, x0=x0)
                solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_)
                return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector')
    backend = choose_backend_t(*y_tensors, *x0_tensors)
    prefer_explicit = backend.supports(Backend.sparse_coo_tensor) or backend.supports(Backend.csr_matrix) or grad_for_f

    if isinstance(f, Tensor) or (isinstance(f, LinearFunction) and prefer_explicit):  # Matrix solve
        if isinstance(f, LinearFunction):
            matrix, bias = f.sparse_matrix_and_bias(solve.x0, *f_args, **f_kwargs)
        else:
            matrix = f
            bias = 0
        preconditioner = compute_preconditioner(solve.preconditioner, matrix, safe=False, target_backend=NUMPY if solve.method.startswith('scipy-') else backend, solver=solve.method) if solve.preconditioner is not None else None

        def _matrix_solve_forward(y, solve: Solve, matrix: Tensor, is_backprop=False):
            backend_matrix = native_matrix(matrix, choose_backend_t(*y_tensors, matrix))
            pattern_dims_in = dual(matrix).as_channel().names
            pattern_dims_out = non_dual(matrix).names  # batch dims can be sparse or batched matrices
            result = _linear_solve_forward(y, solve, backend_matrix, pattern_dims_in, pattern_dims_out, preconditioner, backend, is_backprop)
            return result  # must return exactly `x` so gradient isn't computed w.r.t. other quantities

        _matrix_solve = attach_gradient_solve(_matrix_solve_forward, auxiliary_args=f'is_backprop,solve{",matrix" if matrix.default_backend == NUMPY else ""}', matrix_adjoint=grad_for_f)
        return _matrix_solve(y - bias, solve, matrix)
    else:  # Matrix-free solve
        f_args = cached(f_args)
        solve = cached(solve)
        assert not grad_for_f, f"grad_for_f=True can only be used for math.jit_compile_linear functions but got '{f_name(f)}'. Please decorate the linear function with @jit_compile_linear"
        assert solve.preconditioner is None, f"Preconditioners not currently supported for matrix-free solves. Decorate '{f_name(f)}' with @math.jit_compile_linear to perform a matrix solve."

        def _function_solve_forward(y, solve: Solve, f_args: tuple, f_kwargs: dict = None, is_backprop=False):
            y_nest, (y_tensor,) = disassemble_tree(y)
            x0_nest, (x0_tensor,) = disassemble_tree(solve.x0)
            # active_dims = (y_tensor.shape & x0_tensor.shape).non_batch  # assumes batch dimensions are not active
            batches = (y_tensor.shape & x0_tensor.shape).batch

            def native_lin_f(native_x, batch_index=None):
                if batch_index is not None and batches.volume > 1:
                    native_x = backend.tile(backend.expand_dims(native_x), [batches.volume, 1])
                x = assemble_tree(x0_nest, [reshaped_tensor(native_x, [batches, non_batch(x0_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(x0_tensor)], convert=False)])
                y = f(x, *f_args, **f_kwargs)
                _, (y_tensor,) = disassemble_tree(y)
                y_native = reshaped_native(y_tensor, [batches, non_batch(y_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(y_tensor)])
                if batch_index is not None and batches.volume > 1:
                    y_native = y_native[batch_index]
                return y_native

            result = _linear_solve_forward(y, solve, native_lin_f, pattern_dims_in=non_batch(x0_tensor).names, pattern_dims_out=non_batch(y_tensor).names, preconditioner=None, backend=backend, is_backprop=is_backprop)
            return result  # must return exactly `x` so gradient isn't computed w.r.t. other quantities

        _function_solve = attach_gradient_solve(_function_solve_forward, auxiliary_args='is_backprop,f_kwargs,solve', matrix_adjoint=grad_for_f)
        return _function_solve(y, solve, f_args, f_kwargs=f_kwargs)
def solve_nonlinear(f: Callable, y, solve: phiml.math._optimize.Solve) ‑> phiml.math._tensors.Tensor

Solves the non-linear equation f(x) = y by minimizing the norm of the residual.

This method is limited to backends that support jacobian(), currently PyTorch, TensorFlow and Jax.

To obtain additional information about the performed solve, use a SolveTape.

See Also: minimize(), solve_linear().

Args

f
Function whose output is optimized to match y. All positional arguments of f are optimized and must be Tensor or phiml.math.magic.PhiTreeNode. The output of f must match y.
y
Desired output of f(x) as Tensor or phiml.math.magic.PhiTreeNode.
solve
Solve object specifying optimization method, parameters and initial guess for x.

Returns

x
Solution fulfilling f(x) = y within specified tolerance as Tensor or phiml.math.magic.PhiTreeNode.

Raises

NotConverged
If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
If the solve failed prematurely.
Expand source code
def solve_nonlinear(f: Callable, y, solve: Solve) -> Tensor:
    """
    Solves the non-linear equation *f(x) = y* by minimizing the norm of the residual.

    This method is limited to backends that support `jacobian()`, currently PyTorch, TensorFlow and Jax.

    To obtain additional information about the performed solve, use a `SolveTape`.

    See Also:
        `minimize()`, `solve_linear()`.

    Args:
        f: Function whose output is optimized to match `y`.
            All positional arguments of `f` are optimized and must be `Tensor` or `phiml.math.magic.PhiTreeNode`.
            The output of `f` must match `y`.
        y: Desired output of `f(x)` as `Tensor` or `phiml.math.magic.PhiTreeNode`.
        solve: `Solve` object specifying optimization method, parameters and initial guess for `x`.

    Returns:
        x: Solution fulfilling `f(x) = y` within specified tolerance as `Tensor` or `phiml.math.magic.PhiTreeNode`.

    Raises:
        NotConverged: If the desired accuracy was not be reached within the maximum number of iterations.
        Diverged: If the solve failed prematurely.
    """
    def min_func(x):
        diff = f(x) - y
        l2 = l2_loss(diff)
        return l2
    if solve.preprocess_y is not None:
        y = solve.preprocess_y(y)
    from ._nd import l2_loss
    solve = solve.with_defaults('solve')
    tol = math.maximum(solve.rel_tol * l2_loss(y), solve.abs_tol)
    min_solve = copy_with(solve, abs_tol=tol, rel_tol=0, preprocess_y=None)
    return minimize(min_func, min_solve)
def spatial_gradient(field: phi.field._grid.CenteredGrid, gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None, type: type = phi.field._grid.CenteredGrid, dims: Union[str, tuple, list, set, phiml.math._shape.Shape, Callable] = <function spatial>, stack_dim: phiml.math._shape.Shape = (vectorᶜ=None), order=2, implicit: phiml.math._optimize.Solve = None)

Finite difference spatial_gradient.

This function can operate in two modes:

  • type=CenteredGrid approximates the spatial_gradient at cell centers using central differences
  • type=StaggeredGrid computes the spatial_gradient at face centers of neighbouring cells

Args

field
centered grid of any number of dimensions (scalar field, vector field, tensor field)
gradient_extrapolation
Extrapolation of the output
type
either CenteredGrid or StaggeredGrid
dims
Along which dimensions to compute the spatial gradient. Only supported when type==CenteredGrid.
stack_dim
Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions. The field must not have a dimension of the same name.
order
Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
When a Solve object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.

Returns

spatial_gradient field of type type.

Expand source code
def spatial_gradient(field: CenteredGrid,
                     gradient_extrapolation: Extrapolation = None,
                     type: type = CenteredGrid,
                     dims: math.DimFilter = spatial,
                     stack_dim: Shape = channel('vector'),
                     order=2,
                     implicit: Solve = None):
    """
    Finite difference spatial_gradient.

    This function can operate in two modes:

    * `type=CenteredGrid` approximates the spatial_gradient at cell centers using central differences
    * `type=StaggeredGrid` computes the spatial_gradient at face centers of neighbouring cells

    Args:
        field: centered grid of any number of dimensions (scalar field, vector field, tensor field)
        gradient_extrapolation: Extrapolation of the output
        type: either `CenteredGrid` or `StaggeredGrid`
        dims: Along which dimensions to compute the spatial gradient. Only supported when `type==CenteredGrid`.
        stack_dim: Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions.
            The `field` must not have a dimension of the same name.
        order: Spatial order of accuracy.
            Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution.
            Supported: 2 explicit, 4 explicit, 6 implicit.
        implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances.
            Otherwise, an explicit stencil is used.

    Returns:
        spatial_gradient field of type `type`.
    """
    if gradient_extrapolation is None:
        gradient_extrapolation = field.extrapolation.spatial_gradient()
    extrap_map = {}
    if not implicit:
        if order == 2:
            if type == CenteredGrid:
                values, needed_shifts = [-1/2, 1/2], (-1, 1)
            else:
                values, needed_shifts = [-1, 1], (0, 1)
        elif order == 4:
            if type == CenteredGrid:
                values, needed_shifts = [1/12, -2/3, 2/3, -1/12], (-2, -1, 1, 2)
            else:
                values, needed_shifts = [1/24, -27/24, 27/24, -1/24], (-1, 0, 1, 2)
        else:
            raise NotImplementedError(f"explicit {order}th-order not supported")
    else:
        extrap_map_rhs = {}
        if order == 6:
            if type == CenteredGrid:
                values, needed_shifts = [-1/36, -14/18, 14/18, 1/36], (-2, -1, 1, 2)
                values_rhs, needed_shifts_rhs = [1/3, 1, 1/3], (-1, 0, 1)
            else:
                values, needed_shifts = [-17/186, -63/62, 63/62, 17/186], (-1, 0, 1, 2)
                extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC)
                values_rhs, needed_shifts_rhs = [9/62, 1, 9/62], (-1, 0, 1)
                extrap_map_rhs['symmetric'] = combine_by_direction(ANTIREFLECT, ANTISYMMETRIC)
        else:
            raise NotImplementedError(f"implicit {order}th-order not supported")
    base_widths = (abs(min(needed_shifts)), max(needed_shifts))
    field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation))  # ToDo does this line do anything?
    if implicit:
        gradient_extrapolation = extrapolation.map(_ex_map_f(extrap_map_rhs), gradient_extrapolation)
    spatial_dims = field.shape.only(dims).names
    stack_dim = stack_dim.with_size(spatial_dims)
    if type == CenteredGrid:
        # ToDo if extrapolation == math.extrapolation.NONE, extend size by 1
        # pad = 1 if extrapolation == math.extrapolation.NONE else 0
        # bounds = Box(field.bounds.lower - field.dx, field.bounds.upper + field.dx) if extrapolation == math.extrapolation.NONE else field.bounds
        std_widths = (0, 0)
        if gradient_extrapolation == math.extrapolation.NONE:
            base_widths = (abs(min(needed_shifts))+1, max(needed_shifts)+1)
            std_widths = (1, 1)
        padded_components = [pad(field, {dim_: base_widths if dim_ == dim else std_widths for dim_ in spatial_dims}) for dim in spatial_dims]
    elif type == StaggeredGrid:
        assert spatial_dims == field.shape.spatial.names, f"spatial_gradient with type=StaggeredGrid requires dims=spatial, i.e. dims='{','.join(field.shape.spatial.names)}'"
        base_widths = (base_widths[0], base_widths[1]-1)
        padded_components = pad_for_staggered_output(field, gradient_extrapolation, field.shape.spatial.names, base_widths)
    else:
        raise ValueError(type)
    shifted_components = [shift(padded_component, needed_shifts, stack_dim=None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, spatial_dims)]
    result_components = [sum([value * shift_ for value, shift_ in zip(values, shifted_component)]) / field.dx.vector[dim] for shifted_component, dim in zip(shifted_components, field.shape.spatial.names)]
    if type == CenteredGrid:
        result = stack(result_components, stack_dim)
    else:
        assert stack_dim.name == 'vector', f"spatial_gradient with type=StaggeredGrid requires stack_dim.name == 'vector' but got '{stack_dim.name}'"
        result = StaggeredGrid(math.stack([component.values for component in result_components], channel(vector=spatial_dims)), bounds=field.bounds, extrapolation=gradient_extrapolation)
    result = result.with_extrapolation(gradient_extrapolation)
    if implicit:
        implicit.x0 = result
        result = solve_linear(_lhs_for_implicit_scheme, result, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=stack_dim, staggered_output=type != CenteredGrid)
    if type == CenteredGrid and gradient_extrapolation == math.extrapolation.NONE:
        result = result.with_bounds(Box(field.bounds.lower - field.dx, field.bounds.upper + field.dx))
    else:
        result = result.with_bounds(field.bounds)
    return result
def sqrt(x: ~TensorOrTree) ‑> ~TensorOrTree

Computes sqrt(x) of the Tensor or phiml.math.magic.PhiTreeNode x.

Expand source code
def sqrt(x: TensorOrTree) -> TensorOrTree:
    """ Computes *sqrt(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """
    return _backend_op1(x, Backend.sqrt)
def stack(fields, dim: phiml.math._shape.Shape, dim_bounds: phi.geom._box.Box = None)

Stacks the given SampledFields along dim.

See Also: concat().

Args

fields
List of matching SampledField instances.
dim
Stack dimension as Shape. Size is ignored.
dim_bounds
Box defining the physical size for dim.

Returns

SampledField matching stacked fields.

Expand source code
def stack(fields, dim: Shape, dim_bounds: Box = None):
    """
    Stacks the given `SampledField`s along `dim`.

    See Also:
        `concat()`.

    Args:
        fields: List of matching `SampledField` instances.
        dim: Stack dimension as `Shape`. Size is ignored.
        dim_bounds: `Box` defining the physical size for `dim`.

    Returns:
        `SampledField` matching stacked fields.
    """
    assert all(isinstance(f, SampledField) for f in fields), f"All fields must be SampledFields of the same type but got {fields}"
    assert all(isinstance(f, type(fields[0])) for f in fields), f"All fields must be SampledFields of the same type but got {fields}"
    if any(f.extrapolation != fields[0].extrapolation for f in fields):
        raise NotImplementedError("Concatenating extrapolations not supported")
    if isinstance(fields[0], Grid):
        values = math.stack([f.values for f in fields], dim)
        if spatial(dim):
            if dim_bounds is None:
                dim_bounds = Box(**{dim.name: len(fields)})
            return type(fields[0])(values, extrapolation=fields[0].extrapolation, bounds=fields[0].bounds * dim_bounds)
        else:
            return fields[0].with_values(values)
    elif isinstance(fields[0], PointCloud):
        elements = geom.stack([f.elements for f in fields], dim)
        values = math.stack([f.values for f in fields], dim)
        return PointCloud(elements=elements, values=values, extrapolation=fields[0].extrapolation, add_overlapping=fields[0]._add_overlapping, bounds=fields[0]._bounds)
    raise NotImplementedError(type(fields[0]))
def stagger(field: phi.field._grid.CenteredGrid, face_function: Callable, extrapolation: Union[float, phiml.math.extrapolation.Extrapolation], type: type = phi.field._grid.StaggeredGrid)

Creates a new grid by evaluating face_function given two neighbouring cells. One layer of missing cells is inferred from the extrapolation.

This method returns a Field of type type which must be either StaggeredGrid or CenteredGrid. When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells. When returning a CenteredGrid, the new grid has the same resolution as field.

Args

field
centered grid
face_function
function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
extrapolation
extrapolation mode of the returned grid. Has no effect on the values.
type
one of (StaggeredGrid, CenteredGrid)
field
CenteredGrid:
face_function
Callable:
extrapolation
math.extrapolation.Extrapolation:
type
type: (Default value = StaggeredGrid)

Returns

grid of type matching the type argument

Expand source code
def stagger(field: CenteredGrid,
            face_function: Callable,
            extrapolation: Union[float, math.extrapolation.Extrapolation],
            type: type = StaggeredGrid):
    """
    Creates a new grid by evaluating `face_function` given two neighbouring cells.
    One layer of missing cells is inferred from the extrapolation.
    
    This method returns a Field of type `type` which must be either StaggeredGrid or CenteredGrid.
    When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells.
    When returning a CenteredGrid, the new grid has the same resolution as `field`.

    Args:
      field: centered grid
      face_function: function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
      extrapolation: extrapolation mode of the returned grid. Has no effect on the values.
      type: one of (StaggeredGrid, CenteredGrid)
      field: CenteredGrid: 
      face_function: Callable:
      extrapolation: math.extrapolation.Extrapolation: 
      type: type:  (Default value = StaggeredGrid)

    Returns:
      grid of type matching the `type` argument

    """
    extrapolation = as_extrapolation(extrapolation)
    all_lower = []
    all_upper = []
    if type == StaggeredGrid:
        for dim in field.resolution.names:
            valid_lo, valid_up = extrapolation.valid_outer_faces(dim)
            if valid_lo and valid_up:
                width_lower, width_upper = {dim: (1, 0)}, {dim: (0, 1)}
            elif valid_lo and not valid_up:
                width_lower, width_upper = {dim: (1, -1)}, {dim: (0, 0)}
            elif not valid_lo and valid_up:
                width_lower, width_upper = {dim: (0, 0)}, {dim: (-1, 1)}
            else:
                width_lower, width_upper = {dim: (0, -1)}, {dim: (-1, 0)}
            all_lower.append(math.pad(field.values, width_lower, field.extrapolation, bounds=field.bounds))
            all_upper.append(math.pad(field.values, width_upper, field.extrapolation, bounds=field.bounds))
        all_upper = math.stack(all_upper, channel('vector'))
        all_lower = math.stack(all_lower, channel('vector'))
        values = face_function(all_lower, all_upper)
        result = StaggeredGrid(values, bounds=field.bounds, extrapolation=extrapolation)
        assert result.shape.spatial == field.shape.spatial
        return result
    elif type == CenteredGrid:
        left, right = math.shift(field.values, (-1, 1), padding=field.extrapolation, stack_dim=channel('vector'))
        values = face_function(left, right)
        return CenteredGrid(values, bounds=field.bounds, extrapolation=extrapolation)
    else:
        raise ValueError(type)
def stop_gradient(x)

Disables gradients for the given tensor. This may switch off the gradients for x itself or create a copy of x with disabled gradients.

Implementations:

Args

x
Tensor or phiml.math.magic.PhiTreeNode for which gradients should be disabled.

Returns

Copy of x.

Expand source code
def stop_gradient(x):
    """
    Disables gradients for the given tensor.
    This may switch off the gradients for `x` itself or create a copy of `x` with disabled gradients.

    Implementations:

    * PyTorch: [`x.detach()`](https://pytorch.org/docs/stable/autograd.html#torch.Tensor.detach)
    * TensorFlow: [`tf.stop_gradient`](https://www.tensorflow.org/api_docs/python/tf/stop_gradient)
    * Jax: [`jax.lax.stop_gradient`](https://jax.readthedocs.io/en/latest/_autosummary/jax.lax.stop_gradient.html)

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` for which gradients should be disabled.

    Returns:
        Copy of `x`.
    """
    if isinstance(x, Tensor):
        return x._op1(lambda native: choose_backend(native).stop_gradient(native))
    elif isinstance(x, PhiTreeNode):
        nest, values = disassemble_tree(x)
        new_values = [stop_gradient(v) for v in values]
        return assemble_tree(nest, new_values)
    else:
        return wrap(choose_backend(x).stop_gradient(x))
def support(field: phi.field._field.SampledField, list_dim: Union[str, phiml.math._shape.Shape] = (nonzeroⁱ=None)) ‑> phiml.math._tensors.Tensor

Returns the points at which the field values are non-zero.

Args

field
SampledField
list_dim
Dimension to list the non-zero values.

Returns

Tensor with shape (list_dim, vector)

Expand source code
def support(field: SampledField, list_dim: Union[Shape, str] = instance('nonzero')) -> Tensor:
    """
    Returns the points at which the field values are non-zero.

    Args:
        field: `SampledField`
        list_dim: Dimension to list the non-zero values.

    Returns:
        `Tensor` with shape `(list_dim, vector)`
    """
    return field.points[math.nonzero(field.values, list_dim=list_dim)]
def to_float(x: ~TensorOrTree) ‑> ~TensorOrTree

Converts the given tensor to floating point format with the currently specified precision.

The precision can be set globally using math.set_global_precision() and locally using with math.precision().

See the phiml.math module documentation at https://tum-pbs.github.io/PhiML/Math.html

See Also: cast().

Args

x
Tensor or phiml.math.magic.PhiTreeNode to convert

Returns

Tensor or phiml.math.magic.PhiTreeNode matching x.

Expand source code
def to_float(x: TensorOrTree) -> TensorOrTree:
    """
    Converts the given tensor to floating point format with the currently specified precision.
    
    The precision can be set globally using `math.set_global_precision()` and locally using `with math.precision()`.
    
    See the `phiml.math` module documentation at https://tum-pbs.github.io/PhiML/Math.html

    See Also:
        `cast()`.

    Args:
        x: `Tensor` or `phiml.math.magic.PhiTreeNode` to convert

    Returns:
        `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x`.
    """
    return _backend_op1(x, Backend.to_float)
def to_int32(x: ~TensorOrTree) ‑> ~TensorOrTree

Converts the Tensor or phiml.math.magic.PhiTreeNode x to 32-bit integer.

Expand source code
def to_int32(x: TensorOrTree) -> TensorOrTree:
    """ Converts the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to 32-bit integer. """
    return _backend_op1(x, Backend.to_int32)
def to_int64(x: ~TensorOrTree) ‑> ~TensorOrTree

Converts the Tensor or phiml.math.magic.PhiTreeNode x to 64-bit integer.

Expand source code
def to_int64(x: TensorOrTree) -> TensorOrTree:
    """ Converts the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to 64-bit integer. """
    return _backend_op1(x, Backend.to_int64)
def unstack(value, dim: Union[str, tuple, list, set, phiml.math._shape.Shape, Callable])

Un-stacks a Sliceable along one or multiple dimensions.

If multiple dimensions are given, the order of elements will be according to the dimension order in dim, i.e. elements along the last dimension will be neighbors in the returned tuple.

See Also: phiml.math.slice.

Args

value
phiml.math.magic.Shapable, such as phiml.math.Tensor
dim
Dimensions as Shape or comma-separated str or dimension type, i.e. channel, spatial, instance, batch.

Returns

tuple of Tensor objects.

Examples

>>> unstack(expand(0, spatial(x=5)), 'x')
(0.0, 0.0, 0.0, 0.0, 0.0)
Expand source code
def unstack(value, dim: DimFilter):
    """
    Un-stacks a `Sliceable` along one or multiple dimensions.

    If multiple dimensions are given, the order of elements will be according to the dimension order in `dim`, i.e. elements along the last dimension will be neighbors in the returned `tuple`.

    See Also:
        `phiml.math.slice`.

    Args:
        value: `phiml.math.magic.Shapable`, such as `phiml.math.Tensor`
        dim: Dimensions as `Shape` or comma-separated `str` or dimension type, i.e. `channel`, `spatial`, `instance`, `batch`.

    Returns:
        `tuple` of `Tensor` objects.

    Examples:
        >>> unstack(expand(0, spatial(x=5)), 'x')
        (0.0, 0.0, 0.0, 0.0, 0.0)
    """
    assert isinstance(value, Sliceable) and isinstance(value, Shaped), f"Cannot unstack {type(value).__name__}. Must be Sliceable and Shaped, see https://tum-pbs.github.io/PhiML/phiml/math/magic.html"
    dims = shape(value).only(dim)
    assert dims.rank > 0, "unstack() requires at least one dimension"
    if dims.rank == 1:
        if hasattr(value, '__unstack__'):
            result = value.__unstack__(dims.names)
            if result is not NotImplemented:
                assert isinstance(result, tuple), f"__unstack__ must return a tuple but got {type(result)}"
                assert all([isinstance(item, Sliceable) for item in result]), f"__unstack__ must return a tuple of Sliceable objects but not all items were sliceable in {result}"
                return result
        return tuple([slice_(value, {dims.name: i}) for i in range(dims.size)])
    else:  # multiple dimensions
        if hasattr(value, '__pack_dims__'):
            packed_dim = batch('_unstack')
            value_packed = value.__pack_dims__(dims.names, packed_dim, pos=None)
            if value_packed is not NotImplemented:
                return unstack(value_packed, packed_dim)
        unstack_dim = _any_uniform_dim(dims)
        first_unstacked = unstack(value, unstack_dim)
        inner_unstacked = [unstack(v, dims.without(unstack_dim)) for v in first_unstacked]
        return sum(inner_unstacked, ())
def upsample2x(grid: ~GridType) ‑> ~GridType

Increases the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.

See Also: downsample2x().

Args

grid
CenteredGrid or StaggeredGrid.

Returns

Grid of same type as grid.

Expand source code
def upsample2x(grid: GridType) -> GridType:
    """
    Increases the number of sample points by a factor of 2 in each spatial dimension.
    The new values are determined via linear interpolation.

    See Also:
        `downsample2x()`.

    Args:
        grid: `CenteredGrid` or `StaggeredGrid`.

    Returns:
        `Grid` of same type as `grid`.
    """
    if isinstance(grid, CenteredGrid):
        values = math.upsample2x(grid.values, grid.extrapolation)
        return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation)
    elif isinstance(grid, StaggeredGrid):
        raise NotImplementedError()
    else:
        raise ValueError(type(grid))
def vec_abs(field: phi.field._field.SampledField)

See phiml.math.vec_abs()

Expand source code
def vec_length(field: SampledField):
    """ See `phiml.math.vec_abs()` """
    assert isinstance(field, SampledField), f"SampledField required but got {type(field).__name__}"
    if isinstance(field, StaggeredGrid):
        field = field.at_centers()
    return field.with_values(math.vec_abs(field.values))
def vec_length(field: phi.field._field.SampledField)

See phiml.math.vec_abs()

Expand source code
def vec_length(field: SampledField):
    """ See `phiml.math.vec_abs()` """
    assert isinstance(field, SampledField), f"SampledField required but got {type(field).__name__}"
    if isinstance(field, StaggeredGrid):
        field = field.at_centers()
    return field.with_values(math.vec_abs(field.values))
def vec_squared(field: phi.field._field.SampledField)

See phiml.math.vec_squared()

Expand source code
def vec_squared(field: SampledField):
    """ See `phiml.math.vec_squared()` """
    if isinstance(field, StaggeredGrid):
        field = field.at_centers()
    return field.with_values(math.vec_squared(field.values))
def where(mask: Union[phi.field._field.Field, phi.geom._geom.Geometry, float], field_true: Union[phi.field._field.Field, float], field_false: Union[phi.field._field.Field, float]) ‑> ~SampledFieldType

Element-wise where operation. Picks the value of field_true where mask=1 / True and the value of field_false where mask=0 / False.

The fields are automatically resampled if necessary, preferring the sample points of mask(). At least one of the arguments must be a SampledField.

Args

mask
Field or Geometry object.
field_true
Field
field_false
Field

Returns

SampledField

Expand source code
def where(mask: Union[Field, Geometry, float], field_true: Union[Field, float], field_false: Union[Field, float]) -> SampledFieldType:
    """
    Element-wise where operation.
    Picks the value of `field_true` where `mask=1 / True` and the value of `field_false` where `mask=0 / False`.

    The fields are automatically resampled if necessary, preferring the sample points of `mask`.
    At least one of the arguments must be a `SampledField`.

    Args:
        mask: `Field` or `Geometry` object.
        field_true: `Field`
        field_false: `Field`

    Returns:
        `SampledField`
    """
    field_true, field_false, mask = _auto_resample(field_true, field_false, mask)
    values = math.where(mask.values, field_true.values, field_false.values)
    return field_true.with_values(values)
def write(field: phi.field._field.SampledField, file: Union[str, phiml.math._tensors.Tensor])

Writes a field to disc using a NumPy file format. Depending on file, the data may be split up into multiple files.

All characteristics of the field are serialized so that it can be fully restored using read().

See Also: read()

Args

field
Field to be saved.
file
Single file as str or Tensor of string type. If file is a tensor, the dimensions of field are matched to the dimensions of file. Dimensions of file that are missing in field result in data duplication. Dimensions of field that are missing in file result in larger files.
Expand source code
def write(field: SampledField, file: Union[str, Tensor]):
    """
    Writes a field to disc using a NumPy file format.
    Depending on `file`, the data may be split up into multiple files.

    All characteristics of the field are serialized so that it can be fully restored using `read()`.

    See Also:
        `read()`

    Args:
        field: Field to be saved.
        file: Single file as `str` or `Tensor` of string type.
            If `file` is a tensor, the dimensions of `field` are matched to the dimensions of `file`.
            Dimensions of `file` that are missing in `field` result in data duplication.
            Dimensions of `field` that are missing in `file` result in larger files.
    """
    if isinstance(file, str):
        write_single_field(field, file)
    elif isinstance(file, Tensor):
        if file.rank == 0:
            write_single_field(field, file.native())
        else:
            dim = file.shape.names[0]
            files = unstack(file, dim)
            fields = field.dimension(dim).unstack(file.shape.get_size(dim))
            for field_, file_ in zip(fields, files):
                write(field_, file_)
    else:
        raise ValueError(file)

Classes

class AngularVelocity (location: Union[phiml.math._tensors.Tensor, tuple, list, numbers.Number], strength: Union[phiml.math._tensors.Tensor, numbers.Number] = 1.0, falloff: Callable = None, component: str = None)

Model of a single vortex or set of vortices. The falloff of the velocity magnitude can be controlled.

Without a specified falloff, the velocity increases linearly with the distance from the vortex center. This is the case with rotating rigid bodies, for example.

Expand source code
class AngularVelocity(Field):
    """
    Model of a single vortex or set of vortices.
    The falloff of the velocity magnitude can be controlled.

    Without a specified falloff, the velocity increases linearly with the distance from the vortex center.
    This is the case with rotating rigid bodies, for example.
    """

    def __init__(self,
                 location: Union[Tensor, tuple, list, Number],
                 strength: Union[Tensor, Number] = 1.0,
                 falloff: Callable = None,
                 component: str = None):
        location = wrap(location)
        strength = wrap(strength)
        assert location.shape.channel.names == ('vector',), "location must have a single channel dimension called 'vector'"
        assert location.shape.spatial.is_empty, "location tensor cannot have any spatial dimensions"
        assert not instance(location), "AngularVelocity does not support instance dimensions"
        self.location = location
        self.strength = strength
        self.falloff = falloff
        self.component = component
        spatial_names = location.vector.item_names
        assert spatial_names is not None, "location.vector must list spatial dimensions as item names"
        self._shape = location.shape & spatial(**{dim: 1 for dim in spatial_names})

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        points = geometry.center
        distances = points - self.location
        strength = self.strength if self.falloff is None else self.strength * self.falloff(distances)
        velocity = math.cross_product(strength, distances)
        velocity = math.sum(velocity, self.location.shape.batch.without(points.shape))
        if self.component:
            velocity = velocity.vector[self.component]
        return velocity

    @property
    def shape(self) -> Shape:
        return self._shape

    def __getitem__(self, item: dict):
        assert all(dim == 'vector' for dim in item), f"Cannot slice AngularVelocity with {item}"
        if 'vector' in item:
            assert item['vector'] == 0 or self.component is None
            component = self.shape.spatial.names[item['vector']]
            return AngularVelocity(self.location, self.strength, self.falloff, component)
        else:
            return self

Ancestors

  • phi.field._field.Field

Instance variables

var shape : phiml.math._shape.Shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self) -> Shape:
    return self._shape
class CenteredGrid (values: Any = 0.0, extrapolation: Union[float, phiml.math.extrapolation.Extrapolation, dict, phi.field._field.Field] = 0.0, bounds: Union[phi.geom._box.Box, float] = None, resolution: Union[int, phiml.math._shape.Shape] = None, **resolution_: Union[int, phiml.math._tensors.Tensor])

N-dimensional grid with values sampled at the cell centers. A centered grid is defined through its CenteredGrid.values phiml.math.Tensor, its CenteredGrid.bounds Box describing the physical size, and its CenteredGrid.extrapolation (phiml.math.extrapolation.Extrapolation).

Centered grids support batch, spatial and channel dimensions.

See Also: StaggeredGrid, Grid, SampledField, Field, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html

Args

values

Values to use for the grid. Has to be one of the following:

  • Geometry: sets inside values to 1, outside to 0
  • Field: resamples the Field to the staggered sample points
  • Number: uses the value for all sample points
  • tuple or list: interprets the sequence as vector, used for all sample points
  • phiml.math.Tensor compatible with grid dims: uses tensor values as grid values
  • Function values(x) where x is a phiml.math.Tensor representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function.
extrapolation
The grid extrapolation determines the value outside the values tensor. Allowed types: float, phiml.math.Tensor, phiml.math.extrapolation.Extrapolation.
bounds
Physical size and location of the grid as Box. If the resolution is determined through resolution of values, a float can be passed for bounds to create a unit box.
resolution
Grid resolution as purely spatial phiml.math.Shape. If bounds is given as a Box, the resolution may be specified as an int to be equal along all axes.
**resolution_
Spatial dimensions as keyword arguments. Typically either resolution or spatial_dims are specified.
Expand source code
class CenteredGrid(Grid, metaclass=deprecated_field_class('CenteredGrid', parent_metaclass=type(Grid))):
    """
    N-dimensional grid with values sampled at the cell centers.
    A centered grid is defined through its `CenteredGrid.values` `phiml.math.Tensor`, its `CenteredGrid.bounds` `phi.geom.Box` describing the physical size, and its `CenteredGrid.extrapolation` (`phiml.math.extrapolation.Extrapolation`).
    
    Centered grids support batch, spatial and channel dimensions.

    See Also:
        `StaggeredGrid`,
        `Grid`,
        `SampledField`,
        `Field`,
        module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 values: Any = 0.,
                 extrapolation: Union[float, Extrapolation, dict, Field] = 0.,
                 bounds: Union[Box, float] = None,
                 resolution: Union[int, Shape] = None,
                 **resolution_: Union[int, Tensor]):
        """
        Args:
            values: Values to use for the grid.
                Has to be one of the following:

                * `phi.geom.Geometry`: sets inside values to 1, outside to 0
                * `Field`: resamples the Field to the staggered sample points
                * `Number`: uses the value for all sample points
                * `tuple` or `list`: interprets the sequence as vector, used for all sample points
                * `phiml.math.Tensor` compatible with grid dims: uses tensor values as grid values
                * Function `values(x)` where `x` is a `phiml.math.Tensor` representing the physical location.
                    The spatial dimensions of the grid will be passed as batch dimensions to the function.

            extrapolation: The grid extrapolation determines the value outside the `values` tensor.
                Allowed types: `float`, `phiml.math.Tensor`, `phiml.math.extrapolation.Extrapolation`.
            bounds: Physical size and location of the grid as `phi.geom.Box`.
                If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box.
            resolution: Grid resolution as purely spatial `phiml.math.Shape`.
                If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes.
            **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified.
        """
        if resolution is None and not resolution_:
            assert isinstance(values, math.Tensor), "Grid resolution must be specified when 'values' is not a Tensor."
            resolution = values.shape.spatial
            bounds = _get_bounds(bounds, resolution)
            elements = GridCell(resolution, bounds)
        else:
            resolution = _get_resolution(resolution, resolution_, bounds)
            bounds = _get_bounds(bounds, resolution)
            elements = GridCell(resolution, bounds)
            if isinstance(values, math.Tensor):
                values = math.expand(values, resolution)
            elif isinstance(values, Geometry):
                values = reduce_sample(values, elements)
            elif isinstance(values, Field):
                values = reduce_sample(values, elements)
            elif callable(values):
                values = _sample_function(values, elements)
            else:
                if isinstance(values, (tuple, list)) and len(values) == resolution.rank:
                    values = math.tensor(values, channel(vector=resolution.names))
                values = math.expand(math.tensor(values), resolution)
        if values.dtype.kind not in (float, complex):
            values = math.to_float(values)
        assert resolution.spatial_rank == bounds.spatial_rank, f"Resolution {resolution} does not match bounds {bounds}"
        Grid.__init__(self, elements, values, extrapolation, values.shape.spatial, bounds)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        if not item:
            return self
        values = self._values[item]
        extrapolation = self._extrapolation[item]
        keep_dims = [dim for dim in self.resolution.names if dim not in item or not isinstance(item[dim], int)]
        bounds = self.elements[item].bounds[{'vector': keep_dims}]
        return CenteredGrid(values, bounds=bounds, extrapolation=extrapolation)

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        if geometry == self.bounds:
            return math.mean(self._values, self._resolution)
        if isinstance(geometry, GeometryStack):
            sampled = [self._sample(g, **kwargs) for g in geometry.geometries]
            return math.stack(sampled, geometry.geometries.shape)
        if isinstance(geometry, GridCell):
            if self.elements == geometry:
                return self.values
            elif math.close(self.dx, geometry.size):
                if all([math.close(offset, geometry.half_size) or math.close(offset, 0)
                        for offset in math.abs(self.bounds.lower - geometry.bounds.lower)]):
                    dyadic_interpolated = self._dyadic_interplate(geometry.resolution, geometry.bounds, **kwargs)
                    if dyadic_interpolated is not NotImplemented:
                        return dyadic_interpolated
                if 'order' in kwargs and kwargs['order'] != 2:
                    raise NotImplementedError(f"Only 6th-order implicit and 2nd-order resampling supported but got order={kwargs['order']}")
                fast_resampled = self._shift_resample(geometry.resolution, geometry.bounds)
                if fast_resampled is not NotImplemented:
                    return fast_resampled
        points = geometry.center
        local_points = self.box.global_to_local(points) * self.resolution - 0.5
        resampled_values = math.grid_sample(self.values, local_points, self.extrapolation, bounds=self.bounds)
        if isinstance(self._extrapolation, FieldEmbedding):
            if isinstance(geometry, GridCell) and ((geometry.bounds.upper <= self.bounds.upper).all or (geometry.bounds.lower >= self.bounds.lower).all):
                # geometry is a subgrid of self
                return resampled_values
            else:  # otherwise we also sample the extrapolation Field
                ext_values = self._extrapolation.field._sample(geometry, **kwargs)
                inside = self.bounds.lies_inside(points)
                return math.where(inside, resampled_values, ext_values)
        return resampled_values

    def _dyadic_interplate(self, resolution: Shape, bounds: Box, order=2, implicit: Solve = None):
        offsets = bounds.lower - self.bounds.lower
        interpolation_dirs = [0 if math.close(offset, 0) else int(math.sign(offset)) for offset in offsets]
        return _dyadic_interpolate(self.values, interpolation_dirs, self.extrapolation, order, implicit)

    def _shift_resample(self, resolution: Shape, bounds: Box, threshold=1e-5, max_padding=20):
        assert math.all_available(bounds.lower, bounds.upper), "Shift resampling requires 'bounds' to be available."
        lower = math.to_int32(math.ceil(math.maximum(0, self.box.lower - bounds.lower) / self.dx - threshold))
        upper = math.to_int32(math.ceil(math.maximum(0, bounds.upper - self.box.upper) / self.dx - threshold))
        total_padding = (math.sum(lower) + math.sum(upper)).numpy()
        if total_padding > max_padding and self.extrapolation.native_grid_sample_mode:
            return NotImplemented
        elif total_padding > 0:
            from phi.field import pad
            padded = pad(self, {dim: (int(lower[i]), int(upper[i])) for i, dim in enumerate(self.shape.spatial.names)})
            grid_box, grid_resolution, grid_values = padded.box, padded.resolution, padded.values
        else:
            grid_box, grid_resolution, grid_values = self.box, self.resolution, self.values
        origin_in_local = grid_box.global_to_local(bounds.lower) * grid_resolution
        data = math.sample_subgrid(grid_values, origin_in_local, resolution)
        return data

    def closest_values(self, points: Geometry):
        local_points = self.box.global_to_local(points.center) * self.resolution - 0.5
        return math.closest_grid_values(self.values, local_points, self.extrapolation)

Ancestors

  • phi.field._grid.Grid
  • phi.field._field.SampledField
  • phi.field._field.Field

Methods

def closest_values(self, points: phi.geom._geom.Geometry)

Sample the closest grid point values of this field at the world-space locations (in physical units) given by points. Points must have a single channel dimension named vector. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions.

Args

points
world-space locations

Returns

Closest grid point values as a Tensor. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in points, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions 'closest_<dim>' where <dim> refers to the name of a spatial dimension.

Expand source code
def closest_values(self, points: Geometry):
    local_points = self.box.global_to_local(points.center) * self.resolution - 0.5
    return math.closest_grid_values(self.values, local_points, self.extrapolation)
class Field

Base class for all fields.

Important implementations:

  • CenteredGrid
  • StaggeredGrid
  • PointCloud
  • Noise

See the phi.field module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html

Expand source code
class Field:
    """
    Base class for all fields.
    
    Important implementations:
    
    * CenteredGrid
    * StaggeredGrid
    * PointCloud
    * Noise
    
    See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    @property
    def shape(self) -> Shape:
        """
        Returns a shape with the following properties
        
        * The spatial dimension names match the dimensions of this Field
        * The batch dimensions match the batch dimensions of this Field
        * The channel dimensions match the channels of this Field
        """
        raise NotImplementedError

    @property
    def spatial_rank(self) -> int:
        """
        Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D).
        This is equal to the spatial rank of the `data`.
        """
        raise NotImplementedError

    @property
    def bounds(self) -> Box:
        """
        The bounds represent the area inside which the values of this `Field` are valid.
        The bounds will also be used as axis limits for plots.

        The bounds can be set manually in the constructor, otherwise default bounds will be generated.

        For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively.

        Fields whose spatial rank is determined only during sampling return an empty `Box`.
        """
        raise NotImplementedError

    def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor:
        """ For internal use only. Use `sample()` instead. """
        raise NotImplementedError(self)

    def at(self, representation: 'SampledField', keep_extrapolation=False, **kwargs) -> 'SampledFieldType':
        """
        Short for `resample(self, representation)`

        See Also
            `resample()`.

        Returns:
            Field object of same type as `representation`
        """
        return resample(self, representation, keep_extrapolation, **kwargs)

    def __matmul__(self, other: 'SampledField'):  # value @ representation
        # Deprecated. Use `resample(value, field)` instead.
        warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
        return self.at(other, keep_extrapolation=False)

    def __rmatmul__(self, other):  # values @ representation
        if not isinstance(self, SampledField):
            return NotImplemented
        if isinstance(other, (Geometry, Number, tuple, list)):
            return self.with_values(other)
        return NotImplemented

    def __rshift__(self, other):
        warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2)
        return self.at(other, keep_extrapolation=False)

    def __rrshift__(self, other):
        warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2)
        if not isinstance(self, SampledField):
            return NotImplemented
        if isinstance(other, (Geometry, float, int, complex, tuple, list)):
            return self.with_values(other)
        return NotImplemented

    def __getitem__(self, item) -> 'Field':
        """
        Access a slice of the Field.
        The returned `Field` may be of a different type than `self`.

        Args:
            item: `dict` mapping dimensions (`str`) to selections (`int` or `slice`) or other supported type, such as `int` or `str`.

        Returns:
            Sliced `Field`.
        """
        raise NotImplementedError(self)

    def __getattr__(self, name: str) -> BoundDim:
        return BoundDim(self, name)

    def dimension(self, name: str):
        """
        Returns a reference to one of the dimensions of this field.

        The dimension reference can be used the same way as a `Tensor` dimension reference.
        Notable properties and methods of a dimension reference are:
        indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`.

        A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`.

        Args:
            name: dimension name

        Returns:
            dimension reference

        """
        return BoundDim(self, name)

    def __repr__(self):
        return f"{self.__class__.__name__} {self.shape}"

    @property
    def is_grid(self):
        """ `isinstance(self, Grid)`. Added for forward compatibility with 2.5. """
        from ._grid import Grid
        return isinstance(self, Grid)

    @property
    def is_point_cloud(self):
        """ `isinstance(self, PointCloud)`. Added for forward compatibility with 2.5. """
        from ._point_cloud import PointCloud
        return isinstance(self, PointCloud)

    @property
    def is_staggered(self):
        """ Whether the field values are sampled between elements. Added for forward compatibility with 2.5. """
        from ._grid import StaggeredGrid
        return isinstance(self, StaggeredGrid)

    @property
    def is_centered(self):
        return not self.is_staggered

    @property
    def sampled_at(self):
        return 'face' if self.is_staggered else 'center'

Subclasses

  • phi.field._angular_velocity.AngularVelocity
  • phi.field._field.SampledField
  • phi.field._mask.HardGeometryMask
  • phi.field._noise.Noise

Instance variables

var bounds : phi.geom._box.Box

The bounds represent the area inside which the values of this Field are valid. The bounds will also be used as axis limits for plots.

The bounds can be set manually in the constructor, otherwise default bounds will be generated.

For fields that are valid without bounds, the lower and upper limit of bounds is set to -inf and inf, respectively.

Fields whose spatial rank is determined only during sampling return an empty Box.

Expand source code
@property
def bounds(self) -> Box:
    """
    The bounds represent the area inside which the values of this `Field` are valid.
    The bounds will also be used as axis limits for plots.

    The bounds can be set manually in the constructor, otherwise default bounds will be generated.

    For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively.

    Fields whose spatial rank is determined only during sampling return an empty `Box`.
    """
    raise NotImplementedError
var is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
var is_grid

isinstance(self, Grid). Added for forward compatibility with 2.5.

Expand source code
@property
def is_grid(self):
    """ `isinstance(self, Grid)`. Added for forward compatibility with 2.5. """
    from ._grid import Grid
    return isinstance(self, Grid)
var is_point_cloud

isinstance(self, PointCloud). Added for forward compatibility with 2.5.

Expand source code
@property
def is_point_cloud(self):
    """ `isinstance(self, PointCloud)`. Added for forward compatibility with 2.5. """
    from ._point_cloud import PointCloud
    return isinstance(self, PointCloud)
var is_staggered

Whether the field values are sampled between elements. Added for forward compatibility with 2.5.

Expand source code
@property
def is_staggered(self):
    """ Whether the field values are sampled between elements. Added for forward compatibility with 2.5. """
    from ._grid import StaggeredGrid
    return isinstance(self, StaggeredGrid)
var sampled_at
Expand source code
@property
def sampled_at(self):
    return 'face' if self.is_staggered else 'center'
var shape : phiml.math._shape.Shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self) -> Shape:
    """
    Returns a shape with the following properties
    
    * The spatial dimension names match the dimensions of this Field
    * The batch dimensions match the batch dimensions of this Field
    * The channel dimensions match the channels of this Field
    """
    raise NotImplementedError
var spatial_rank : int

Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the data.

Expand source code
@property
def spatial_rank(self) -> int:
    """
    Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D).
    This is equal to the spatial rank of the `data`.
    """
    raise NotImplementedError

Methods

def at(self, representation: SampledField, keep_extrapolation=False, **kwargs) ‑> ~SampledFieldType

Short for resample()(self, representation)

See Also resample().

Returns

Field object of same type as representation

Expand source code
def at(self, representation: 'SampledField', keep_extrapolation=False, **kwargs) -> 'SampledFieldType':
    """
    Short for `resample(self, representation)`

    See Also
        `resample()`.

    Returns:
        Field object of same type as `representation`
    """
    return resample(self, representation, keep_extrapolation, **kwargs)
def dimension(self, name: str)

Returns a reference to one of the dimensions of this field.

The dimension reference can be used the same way as a Tensor dimension reference. Notable properties and methods of a dimension reference are: indexing using [index], unstack(), size, exists, is_batch, is_spatial, is_channel.

A shortcut to calling this function is the syntax field.<dim_name> which calls field.dimension(<dim_name>).

Args

name
dimension name

Returns

dimension reference

Expand source code
def dimension(self, name: str):
    """
    Returns a reference to one of the dimensions of this field.

    The dimension reference can be used the same way as a `Tensor` dimension reference.
    Notable properties and methods of a dimension reference are:
    indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`.

    A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`.

    Args:
        name: dimension name

    Returns:
        dimension reference

    """
    return BoundDim(self, name)
class Grid

Base class for CenteredGrid and StaggeredGrid.

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class Grid(SampledField, metaclass=deprecated_field_class('Grid')):
    """
    Base class for `CenteredGrid` and `StaggeredGrid`.
    """

    def __init__(self, elements: Geometry, values: Tensor, extrapolation: Union[float, Extrapolation], resolution: Union[Shape, int], bounds: Union[Box, float]):
        assert isinstance(bounds, Box)
        assert isinstance(resolution, Shape)
        if bounds.size.vector.item_names is None:
            with NUMPY:
                bounds = bounds.shifted(math.zeros(channel(vector=spatial(values).names)))
        SampledField.__init__(self, elements, values, extrapolation, bounds)
        assert values.shape.spatial_rank == elements.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}"
        assert values.shape.spatial_rank == bounds.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}"
        assert values.shape.instance_rank == 0, f"Instance dimensions not supported for grids. Got values with shape {values.shape}"
        assert set(resolution.names) == set(bounds.vector.item_names), f"Resolution does not match bounds"
        self._resolution = resolution.only(bounds.vector.item_names, reorder=True)

    def closest_values(self, points: Geometry):
        """
        Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`.
        Points must have a single channel dimension named `vector`.
        It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions.

        Args:
            points: world-space locations

        Returns:
            Closest grid point values as a `Tensor`.
            For each dimension, the grid points immediately left and right of the sample points are evaluated.
            For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field.
            These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension.
        """
        raise NotImplementedError(self)

    def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor:
        raise NotImplementedError(self)

    def with_values(self, values):
        if isinstance(values, math.Tensor):
            assert set(spatial(values).names) == set(self.bounds.vector.item_names), f"StaggeredGrid.with_values() only accepts tensor with same spatial dimensiosn but got {spatial(values)} for {self}"
            return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds)
        else:
            return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds, resolution=self._resolution)

    def with_extrapolation(self, extrapolation: Extrapolation):
        return type(self)(self.values, extrapolation=extrapolation, bounds=self.bounds)

    def with_bounds(self, bounds: Box):
        return type(self)(self.values, extrapolation=self.extrapolation, bounds=bounds)

    def __value_attrs__(self):
        return '_values', '_extrapolation'

    def __variable_attrs__(self):
        return '_values',

    def __expand__(self, dims: Shape, **kwargs) -> 'Grid':
        return self.with_values(math.expand(self.values, dims, **kwargs))

    def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'Grid':
        for dim in dims:
            if dim in self._resolution:
                return NotImplemented
        values = math.rename_dims(self.values, dims, new_dims)
        extrapolation = math.rename_dims(self.extrapolation, dims, new_dims, **kwargs)
        bounds = math.rename_dims(self.bounds, dims, new_dims, **kwargs)
        return type(self)(values, extrapolation=extrapolation, bounds=bounds, resolution=self._resolution)


    def __eq__(self, other):
        if not type(self) == type(other):
            return False
        if not (self._bounds == other._bounds and self._resolution == other._resolution and self._extrapolation == other._extrapolation):
            return False
        if self.values is None:
            return other.values is None
        if other.values is None:
            return False
        if not math.all_available(self.values) or not math.all_available(other.values):  # tracers involved
            if math.all_available(self.values) != math.all_available(other.values):
                return False
            else:  # both tracers
                return self.values.shape == other.values.shape
        if self.values.shape != other.values.shape:
            return False
        return bool((self.values == other.values).all)

    def __getitem__(self, item) -> 'Grid':
        raise NotImplementedError(self)

    @property
    def shape(self):
        return self._resolution & self._values.shape.non_spatial

    @property
    def bounds(self) -> Box:
        return self._bounds

    @property
    def box(self) -> Box:
        return self._bounds

    @property
    def resolution(self) -> Shape:
        return self._resolution

    @property
    def dx(self) -> Tensor:
        return self.bounds.size / self.resolution

    def __repr__(self):
        if self._values is not None:
            return f"{self.__class__.__name__}[{self.shape.non_spatial & self.resolution}, size={self.box.size}, extrapolation={self._extrapolation}]"
        else:
            return f"{self.__class__.__name__}[{self.resolution}, size={self.box.size}, extrapolation={self._extrapolation}]"

    def uniform_values(self):
        """
        Returns a uniform tensor containing `values`.

        For periodic grids, which always have a uniform value tensor, `values' is returned directly.
        If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`.
        """
        return self.values

Ancestors

  • phi.field._field.SampledField
  • phi.field._field.Field

Subclasses

  • phi.field._grid.CenteredGrid
  • phi.field._grid.StaggeredGrid

Instance variables

var bounds : phi.geom._box.Box

The bounds represent the area inside which the values of this Field are valid. The bounds will also be used as axis limits for plots.

The bounds can be set manually in the constructor, otherwise default bounds will be generated.

For fields that are valid without bounds, the lower and upper limit of bounds is set to -inf and inf, respectively.

Fields whose spatial rank is determined only during sampling return an empty Box.

Expand source code
@property
def bounds(self) -> Box:
    return self._bounds
var box : phi.geom._box.Box
Expand source code
@property
def box(self) -> Box:
    return self._bounds
var dx : phiml.math._tensors.Tensor
Expand source code
@property
def dx(self) -> Tensor:
    return self.bounds.size / self.resolution
var resolution : phiml.math._shape.Shape
Expand source code
@property
def resolution(self) -> Shape:
    return self._resolution
var shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self):
    return self._resolution & self._values.shape.non_spatial

Methods

def closest_values(self, points: phi.geom._geom.Geometry)

Sample the closest grid point values of this field at the world-space locations (in physical units) given by points. Points must have a single channel dimension named vector. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions.

Args

points
world-space locations

Returns

Closest grid point values as a Tensor. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in points, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions 'closest_<dim>' where <dim> refers to the name of a spatial dimension.

Expand source code
def closest_values(self, points: Geometry):
    """
    Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`.
    Points must have a single channel dimension named `vector`.
    It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions.

    Args:
        points: world-space locations

    Returns:
        Closest grid point values as a `Tensor`.
        For each dimension, the grid points immediately left and right of the sample points are evaluated.
        For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field.
        These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension.
    """
    raise NotImplementedError(self)
def uniform_values(self)

Returns a uniform tensor containing values.

For periodic grids, which always have a uniform value tensor, `values' is returned directly. If values is not uniform, it is padded as in StaggeredGrid.staggered_tensor().

Expand source code
def uniform_values(self):
    """
    Returns a uniform tensor containing `values`.

    For periodic grids, which always have a uniform value tensor, `values' is returned directly.
    If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`.
    """
    return self.values
def with_bounds(self, bounds: phi.geom._box.Box)
Expand source code
def with_bounds(self, bounds: Box):
    return type(self)(self.values, extrapolation=self.extrapolation, bounds=bounds)
def with_extrapolation(self, extrapolation: phiml.math.extrapolation.Extrapolation)

Returns a copy of this field with values replaced.

Expand source code
def with_extrapolation(self, extrapolation: Extrapolation):
    return type(self)(self.values, extrapolation=extrapolation, bounds=self.bounds)
def with_values(self, values)

Returns a copy of this field with values replaced.

Expand source code
def with_values(self, values):
    if isinstance(values, math.Tensor):
        assert set(spatial(values).names) == set(self.bounds.vector.item_names), f"StaggeredGrid.with_values() only accepts tensor with same spatial dimensiosn but got {spatial(values)} for {self}"
        return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds)
    else:
        return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds, resolution=self._resolution)
class HardGeometryMask (geometry: phi.geom._geom.Geometry)

Deprecated since version 1.3. Use mask() or resample() instead.

Expand source code
class HardGeometryMask(Field):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """

    def __init__(self, geometry: Geometry):
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)
        assert isinstance(geometry, Geometry)
        self.geometry = geometry

    @property
    def shape(self):
        return self.geometry.shape.non_channel

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        return math.to_float(self.geometry.lies_inside(geometry.center))

    def __getitem__(self, item: dict):
        return HardGeometryMask(self.geometry[item])

Ancestors

  • phi.field._field.Field

Subclasses

  • phi.field._mask.SoftGeometryMask

Instance variables

var shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self):
    return self.geometry.shape.non_channel
class Noise (*shape: phiml.math._shape.Shape, scale=10.0, smoothness=1.0, **channel_dims)

Generates random noise fluctuations which can be configured in physical size and smoothness. Each time values are sampled from a Noise field, a new noise field is generated.

Noise is typically used as an initializer for CenteredGrids or StaggeredGrids.

Args

shape
Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid.
scale
Size of noise fluctuations in physical units.
smoothness
Determines how quickly high frequencies die out.
**dims
Additional dimensions, added to shape.
Expand source code
class Noise(Field):
    """
    Generates random noise fluctuations which can be configured in physical size and smoothness.
    Each time values are sampled from a Noise field, a new noise field is generated.

    Noise is typically used as an initializer for CenteredGrids or StaggeredGrids.
    """

    def __init__(self, *shape: math.Shape, scale=10., smoothness=1.0, **channel_dims):
        """
        Args:
          shape: Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid.
          scale: Size of noise fluctuations in physical units.
          smoothness: Determines how quickly high frequencies die out.
          **dims: Additional dimensions, added to `shape`.
        """
        self.scale = scale
        self.smoothness = smoothness
        self._shape = math.concat_shapes(*shape, channel(**channel_dims))

    @property
    def shape(self):
        return self._shape

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        if isinstance(geometry, GridCell):
            return self.grid_sample(geometry.resolution, geometry.grid_size)
        raise NotImplementedError(f"{type(geometry)} not supported. Only GridCell allowed.")

    def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None):
        shape = (self._shape if shape is None else shape) & resolution
        for dim in channel(self._shape):
            if dim.item_names[0] is None:
                warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning)
                shape &= channel(**{dim.name: resolution.names})
        rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape))  # Note: there is no complex32
        # --- Compute 1 / k^2 ---
        k_vec = math.fftfreq(resolution) * resolution / math.tensor(size) * math.tensor(self.scale)  # in physical units
        k2 = math.vec_squared(k_vec)
        lowest_frequency = 0.1
        weight_mask = math.to_float(k2 > lowest_frequency)
        inv_k2 = math.divide_no_nan(1, k2)
        # --- Compute result ---
        fft = rndj * inv_k2 ** self.smoothness * weight_mask
        array = math.real(math.ifft(fft))
        array /= math.std(array, dim=array.shape.non_batch)
        array -= math.mean(array, dim=array.shape.non_batch)
        array = math.to_float(array)
        return array

    def __getitem__(self, item: dict):
        new_shape = self.shape.after_gather(item)
        return Noise(new_shape, scale=self.scale, smoothness=self.smoothness)

    def __repr__(self):
        return f"{self._shape}, scale={self.scale}, smoothness={self.smoothness}"

Ancestors

  • phi.field._field.Field

Instance variables

var shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self):
    return self._shape

Methods

def grid_sample(self, resolution: phiml.math._shape.Shape, size, shape: phiml.math._shape.Shape = None)
Expand source code
def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None):
    shape = (self._shape if shape is None else shape) & resolution
    for dim in channel(self._shape):
        if dim.item_names[0] is None:
            warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning)
            shape &= channel(**{dim.name: resolution.names})
    rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape))  # Note: there is no complex32
    # --- Compute 1 / k^2 ---
    k_vec = math.fftfreq(resolution) * resolution / math.tensor(size) * math.tensor(self.scale)  # in physical units
    k2 = math.vec_squared(k_vec)
    lowest_frequency = 0.1
    weight_mask = math.to_float(k2 > lowest_frequency)
    inv_k2 = math.divide_no_nan(1, k2)
    # --- Compute result ---
    fft = rndj * inv_k2 ** self.smoothness * weight_mask
    array = math.real(math.ifft(fft))
    array /= math.std(array, dim=array.shape.non_batch)
    array -= math.mean(array, dim=array.shape.non_batch)
    array = math.to_float(array)
    return array
class PointCloud (elements: Union[phiml.math._tensors.Tensor, phi.geom._geom.Geometry], values: Any = 1.0, extrapolation: Union[float, phiml.math.extrapolation.Extrapolation] = 0.0, add_overlapping=False, bounds: phi.geom._box.Box = None)

A PointCloud comprises:

  • elements: a Geometry representing all points or volumes
  • values: a Tensor representing the values corresponding to elements
  • extrapolation: an Extrapolation defining the field value outside of values

The points / elements of the PointCloud are listed along instance or spatial dimensions of elements. These dimensions are automatically added to values if not already present.

When sampling or resampling a PointCloud, the following keyword arguments can be specified.

  • soft: default=False. If True, interpolates smoothly from 1 to 0 between the inside and outside of elements. If False, only the center position of the new representation elements is checked against the point cloud elements.
  • scatter: default=False. If True, scattering will be used to sample the point cloud onto grids. Then, each element of the point cloud can only affect a single cell. This is only recommended when the points are much smaller than the cells.
  • outside_handling: default='discard'. One of 'discard', 'clamp', 'undefined'.
  • balance: default=0.5. Only used when soft=True. See the description in Geometry.approximate_fraction_inside().

See the phi.field module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html

Args

elements
Tensor or Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
add_overlapping
True: values of overlapping geometries are summed. False: values between overlapping geometries are interpolated
bounds
(optional) size of the fixed domain in which the points should get visualized. None results in max and min coordinates of points.
Expand source code
class PointCloud(SampledField, metaclass=deprecated_field_class('PointCloud')):
    """
    A `PointCloud` comprises:

    * `elements`: a `Geometry` representing all points or volumes
    * `values`: a `Tensor` representing the values corresponding to `elements`
    * `extrapolation`: an `Extrapolation` defining the field value outside of `values`

    The points / elements of the `PointCloud` are listed along *instance* or *spatial* dimensions of `elements`.
    These dimensions are automatically added to `values` if not already present.

    When sampling or resampling a `PointCloud`, the following keyword arguments can be specified.

    * `soft`: default=False.
      If `True`, interpolates smoothly from 1 to 0 between the inside and outside of elements.
      If `False`, only the center position of the new representation elements is checked against the point cloud elements.
    * `scatter`: default=False.
      If `True`, scattering will be used to sample the point cloud onto grids. Then, each element of the point cloud can only affect a single cell. This is only recommended when the points are much smaller than the cells.
    * `outside_handling`: default='discard'. One of `'discard'`, `'clamp'`, `'undefined'`.
    * `balance`: default=0.5. Only used when `soft=True`.
      See the description in `phi.geom.Geometry.approximate_fraction_inside()`.

    See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 elements: Union[Tensor, Geometry],
                 values: Any = 1.,
                 extrapolation: Union[Extrapolation, float] = 0.,
                 add_overlapping=False,
                 bounds: Box = None):
        """
        Args:
          elements: `Tensor` or `Geometry` object specifying the sample points and sizes
          values: values corresponding to elements
          extrapolation: values outside elements
          add_overlapping: True: values of overlapping geometries are summed. False: values between overlapping geometries are interpolated
          bounds: (optional) size of the fixed domain in which the points should get visualized. None results in max and min coordinates of points.
        """
        SampledField.__init__(self, elements, expand(wrap(values), non_batch(elements).non_channel), extrapolation, bounds)
        assert self._extrapolation is PERIODIC or isinstance(self._extrapolation, ConstantExtrapolation), f"Unsupported extrapolation for PointCloud: {self._extrapolation}"
        self._add_overlapping = add_overlapping

    @property
    def shape(self):
        return self._elements.shape.without('vector') & self._values.shape

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        if not item:
            return self
        item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'}
        elements = self.elements[item_without_vec]
        values = self._values[item]
        extrapolation = self._extrapolation[item]
        bounds = self._bounds[item_without_vec] if self._bounds is not None else None
        return PointCloud(elements, values, extrapolation, self._add_overlapping, bounds)

    def with_elements(self, elements: Geometry):
        return PointCloud(elements=elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)

    def shifted(self, delta):
        return self.with_elements(self.elements.shifted(delta))

    def with_values(self, values):
        return PointCloud(elements=self.elements, values=values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)

    def with_extrapolation(self, extrapolation: Extrapolation):
        return PointCloud(elements=self.elements, values=self.values, extrapolation=extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)

    def with_bounds(self, bounds: Box):
        return PointCloud(elements=self.elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=bounds)

    def __value_attrs__(self):
        return '_values', '_extrapolation'

    def __variable_attrs__(self):
        return '_values', '_elements'

    def __expand__(self, dims: Shape, **kwargs) -> 'PointCloud':
        return self.with_values(expand(self.values, dims, **kwargs))

    def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'PointCloud':
        elements = math.rename_dims(self.elements, dims, new_dims)
        values = math.rename_dims(self.values, dims, new_dims)
        extrapolation = math.rename_dims(self.extrapolation, dims, new_dims, **kwargs)
        return PointCloud(elements, values, extrapolation, self._add_overlapping, self._bounds)

    def __eq__(self, other):
        if not type(self) == type(other):
            return False
        # Check everything but __variable_attrs__ (values): elements type, extrapolation, add_overlapping
        if type(self.elements) is not type(other.elements):
            return False
        if self.extrapolation != other.extrapolation:
            return False
        if self._add_overlapping != other._add_overlapping:
            return False
        if self.values is None:
            return other.values is None
        if other.values is None:
            return False
        if not math.all_available(self.values) or not math.all_available(other.values):  # tracers involved
            if math.all_available(self.values) != math.all_available(other.values):
                return False
            else:  # both tracers
                return self.values.shape == other.values.shape
        return bool((self.values == other.values).all)

    @property
    def bounds(self) -> Box:
        if self._bounds is not None:
            return self._bounds
        else:
            from phi.field._field_math import data_bounds
            bounds = data_bounds(self.elements.center)
            radius = math.max(self.elements.bounding_radius())
            return Box(bounds.lower - radius, bounds.upper + radius)

    def _sample(self, geometry: Geometry, soft=False, scatter=False, outside_handling='discard', balance=0.5) -> Tensor:
        if geometry == self.elements:
            return self.values
        if isinstance(geometry, GeometryStack):
            sampled = [self._sample(g, soft, scatter, outside_handling, balance) for g in geometry.geometries]
            return math.stack(sampled, geometry.geometries.shape)
        if self.extrapolation is extrapolation.PERIODIC:
            raise NotImplementedError("Periodic PointClouds not yet supported")
        if isinstance(geometry, GridCell) and scatter:
            assert not soft, "Cannot soft-sample when scatter=True"
            return self.grid_scatter(geometry.bounds, geometry.resolution, outside_handling)
        else:
            assert not isinstance(self._elements, Point), "Cannot sample Point-like elements with scatter=False"
            if may_vary_along(self._values, instance(self._values) & spatial(self._values)):
                raise NotImplementedError("Non-scatter resampling not yet supported for varying values")
            idx0 = (instance(self._values) & spatial(self._values)).first_index()
            outside = self._extrapolation.value if isinstance(self._extrapolation, ConstantExtrapolation) else 0
            if soft:
                frac_inside = self.elements.approximate_fraction_inside(geometry, balance)
                return frac_inside * self._values[idx0] + (1 - frac_inside) * outside
            else:
                return math.where(self.elements.lies_inside(geometry.center), self._values[idx0], outside)

    def grid_scatter(self, bounds: Box, resolution: math.Shape, outside_handling: str):
        """
        Approximately samples this field on a regular grid using math.scatter().

        Args:
            outside_handling: `str` passed to `phiml.math.scatter()`.
            bounds: physical dimensions of the grid
            resolution: grid resolution

        Returns:
            `CenteredGrid`
        """
        closest_index = bounds.global_to_local(self.points) * resolution - 0.5
        mode = 'add' if self._add_overlapping else 'mean'
        base = math.zeros(resolution)
        if isinstance(self._extrapolation, ConstantExtrapolation):
            base += self._extrapolation.value
        scattered = math.scatter(base, closest_index, self.values, mode=mode, outside_handling=outside_handling)
        return scattered

    def __repr__(self):
        try:
            return "PointCloud[%s]" % (self.shape,)
        except:
            return "PointCloud[invalid]"

    def __and__(self, other):
        assert isinstance(other, PointCloud)
        assert instance(self).rank == instance(other).rank == 1, f"Can only use & on PointClouds that have a single instance dimension but got shapes {self.shape} & {other.shape}"
        from ._field_math import concat
        return concat([self, other], instance(self))

Ancestors

  • phi.field._field.SampledField
  • phi.field._field.Field

Instance variables

var bounds : phi.geom._box.Box

The bounds represent the area inside which the values of this Field are valid. The bounds will also be used as axis limits for plots.

The bounds can be set manually in the constructor, otherwise default bounds will be generated.

For fields that are valid without bounds, the lower and upper limit of bounds is set to -inf and inf, respectively.

Fields whose spatial rank is determined only during sampling return an empty Box.

Expand source code
@property
def bounds(self) -> Box:
    if self._bounds is not None:
        return self._bounds
    else:
        from phi.field._field_math import data_bounds
        bounds = data_bounds(self.elements.center)
        radius = math.max(self.elements.bounding_radius())
        return Box(bounds.lower - radius, bounds.upper + radius)
var shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self):
    return self._elements.shape.without('vector') & self._values.shape

Methods

def grid_scatter(self, bounds: phi.geom._box.Box, resolution: phiml.math._shape.Shape, outside_handling: str)

Approximately samples this field on a regular grid using math.scatter().

Args

outside_handling
str passed to phiml.math.scatter().
bounds
physical dimensions of the grid
resolution
grid resolution

Returns

CenteredGrid

Expand source code
def grid_scatter(self, bounds: Box, resolution: math.Shape, outside_handling: str):
    """
    Approximately samples this field on a regular grid using math.scatter().

    Args:
        outside_handling: `str` passed to `phiml.math.scatter()`.
        bounds: physical dimensions of the grid
        resolution: grid resolution

    Returns:
        `CenteredGrid`
    """
    closest_index = bounds.global_to_local(self.points) * resolution - 0.5
    mode = 'add' if self._add_overlapping else 'mean'
    base = math.zeros(resolution)
    if isinstance(self._extrapolation, ConstantExtrapolation):
        base += self._extrapolation.value
    scattered = math.scatter(base, closest_index, self.values, mode=mode, outside_handling=outside_handling)
    return scattered
def shifted(self, delta)
Expand source code
def shifted(self, delta):
    return self.with_elements(self.elements.shifted(delta))
def with_bounds(self, bounds: phi.geom._box.Box)
Expand source code
def with_bounds(self, bounds: Box):
    return PointCloud(elements=self.elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=bounds)
def with_elements(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_elements(self, elements: Geometry):
    return PointCloud(elements=elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
def with_extrapolation(self, extrapolation: phiml.math.extrapolation.Extrapolation)

Returns a copy of this field with values replaced.

Expand source code
def with_extrapolation(self, extrapolation: Extrapolation):
    return PointCloud(elements=self.elements, values=self.values, extrapolation=extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
def with_values(self, values)

Returns a copy of this field with values replaced.

Expand source code
def with_values(self, values):
    return PointCloud(elements=self.elements, values=values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
class SampledField (elements: Union[phiml.math._tensors.Tensor, phi.geom._geom.Geometry], values: phiml.math._tensors.Tensor, extrapolation: Union[phiml.math.extrapolation.Extrapolation, float, phi.field._field.Field, None], bounds: Optional[phi.geom._box.Box])

Base class for fields that are sampled at specific locations such as grids or point clouds.

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class SampledField(Field):
    """
    Base class for fields that are sampled at specific locations such as grids or point clouds.
    """

    def __init__(self,
                 elements: Union[Geometry, Tensor],
                 values: Tensor,
                 extrapolation: Union[float, Extrapolation, Field, None],
                 bounds: Union[Box, None]):
        """
        Args:
          elements: Geometry object specifying the sample points and sizes
          values: values corresponding to elements
          extrapolation: values outside elements
        """
        if isinstance(elements, Tensor):
            elements = Point(elements)
        assert isinstance(elements, Geometry), elements
        assert isinstance(values, Tensor), f"Values must be a Tensor but got {values}."
        assert bounds is None or isinstance(bounds, Box), 'Invalid bounds.'
        self._bounds = bounds
        self._elements: Geometry = elements
        self._values: Tensor = values
        self._extrapolation: Extrapolation = as_extrapolation(extrapolation)

    @property
    def bounds(self) -> Box:
        raise NotImplementedError(self.__class__)

    def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor:
        raise NotImplementedError(self.__class__)

    def with_values(self, values):
        """ Returns a copy of this field with `values` replaced. """
        raise NotImplementedError(self)

    def with_extrapolation(self, extrapolation: Extrapolation):
        """ Returns a copy of this field with `values` replaced. """
        raise NotImplementedError(self)

    @property
    def shape(self):
        raise NotImplementedError()

    @property
    def spatial_rank(self) -> int:
        return self._elements.spatial_rank

    def __getitem__(self: 'FieldType', item) -> 'FieldType':
        raise NotImplementedError(self)

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'FieldType':
        from ._field_math import stack
        return stack(values, dim, kwargs.get('bounds', None))

    @staticmethod
    def __concat__(values: tuple, dim: str, **kwargs) -> 'FieldType':
        from ._field_math import concat
        return concat(values, dim)

    @property
    def elements(self) -> Geometry:
        """
        Returns a geometrical representation of the discrete volume elements.
        The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions.
        
        For grids, the geometries are boxes while particle fields may be represented as spheres.
        
        If this Field has no discrete points, this method returns an empty geometry.
        """
        return self._elements

    @property
    def points(self) -> Tensor:
        return self.elements.center

    @property
    def values(self) -> Tensor:
        return self._values

    data = values

    @property
    def extrapolation(self) -> Extrapolation:
        return self._extrapolation

    def __mul__(self, other):
        return self._op2(other, lambda d1, d2: d1 * d2)

    __rmul__ = __mul__

    def __truediv__(self, other):
        return self._op2(other, lambda d1, d2: d1 / d2)

    def __rtruediv__(self, other):
        return self._op2(other, lambda d1, d2: d2 / d1)

    def __sub__(self, other):
        return self._op2(other, lambda d1, d2: d1 - d2)

    def __rsub__(self, other):
        return self._op2(other, lambda d1, d2: d2 - d1)

    def __add__(self, other):
        return self._op2(other, lambda d1, d2: d1 + d2)

    __radd__ = __add__

    def __pow__(self, power, modulo=None):
        return self._op2(power, lambda f, p: f ** p)

    def __neg__(self):
        return self._op1(lambda x: -x)

    def __gt__(self, other):
        return self._op2(other, lambda x, y: x > y)

    def __ge__(self, other):
        return self._op2(other, lambda x, y: x >= y)

    def __lt__(self, other):
        return self._op2(other, lambda x, y: x < y)

    def __le__(self, other):
        return self._op2(other, lambda x, y: x <= y)

    def __abs__(self):
        return self._op1(lambda x: abs(x))

    def _op1(self: 'SampledFieldType', operator: Callable) -> 'SampledFieldType':
        """
        Perform an operation on the data of this field.

        Args:
          operator: function that accepts tensors and extrapolations and returns objects of the same type and dimensions

        Returns:
          Field of same type
        """
        values = operator(self.values)
        extrapolation_ = operator(self._extrapolation)
        return self.with_values(values).with_extrapolation(extrapolation_)

    def _op2(self, other, operator) -> 'SampledField':
        if isinstance(other, Geometry):
            raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}")
        if isinstance(other, Field):
            other_values = reduce_sample(other, self._elements)
            values = operator(self._values, other_values)
            extrapolation_ = operator(self._extrapolation, other.extrapolation)
            return self.with_values(values).with_extrapolation(extrapolation_)
        else:
            if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank:
                other = math.wrap(other, self.points.shape['vector'])
            else:
                other = math.wrap(other)
            values = operator(self._values, other)
            return self.with_values(values)

Ancestors

  • phi.field._field.Field

Subclasses

  • phi.field._grid.Grid
  • phi.field._mesh.Mesh
  • phi.field._point_cloud.PointCloud

Instance variables

var bounds : phi.geom._box.Box

The bounds represent the area inside which the values of this Field are valid. The bounds will also be used as axis limits for plots.

The bounds can be set manually in the constructor, otherwise default bounds will be generated.

For fields that are valid without bounds, the lower and upper limit of bounds is set to -inf and inf, respectively.

Fields whose spatial rank is determined only during sampling return an empty Box.

Expand source code
@property
def bounds(self) -> Box:
    raise NotImplementedError(self.__class__)
var data : phiml.math._tensors.Tensor
Expand source code
@property
def values(self) -> Tensor:
    return self._values
var elements : phi.geom._geom.Geometry

Returns a geometrical representation of the discrete volume elements. The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions.

For grids, the geometries are boxes while particle fields may be represented as spheres.

If this Field has no discrete points, this method returns an empty geometry.

Expand source code
@property
def elements(self) -> Geometry:
    """
    Returns a geometrical representation of the discrete volume elements.
    The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions.
    
    For grids, the geometries are boxes while particle fields may be represented as spheres.
    
    If this Field has no discrete points, this method returns an empty geometry.
    """
    return self._elements
var extrapolation : phiml.math.extrapolation.Extrapolation
Expand source code
@property
def extrapolation(self) -> Extrapolation:
    return self._extrapolation
var points : phiml.math._tensors.Tensor
Expand source code
@property
def points(self) -> Tensor:
    return self.elements.center
var shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self):
    raise NotImplementedError()
var spatial_rank : int

Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the data.

Expand source code
@property
def spatial_rank(self) -> int:
    return self._elements.spatial_rank
var values : phiml.math._tensors.Tensor
Expand source code
@property
def values(self) -> Tensor:
    return self._values

Methods

def with_extrapolation(self, extrapolation: phiml.math.extrapolation.Extrapolation)

Returns a copy of this field with values replaced.

Expand source code
def with_extrapolation(self, extrapolation: Extrapolation):
    """ Returns a copy of this field with `values` replaced. """
    raise NotImplementedError(self)
def with_values(self, values)

Returns a copy of this field with values replaced.

Expand source code
def with_values(self, values):
    """ Returns a copy of this field with `values` replaced. """
    raise NotImplementedError(self)
class Scene

Provides methods for reading and writing simulation data.

See the format documentation at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .

All data of a Scene is located inside a single directory with name sim_xxxxxx where xxxxxx is the id. The data of the scene is organized into NumPy files by name and frame.

To create a new scene, use Scene.create(). To reference an existing scene, use Scene.at(). To list all scenes within a directory, use Scene.list().

Expand source code
class Scene:
    """
    Provides methods for reading and writing simulation data.

    See the format documentation at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .

    All data of a `Scene` is located inside a single directory with name `sim_xxxxxx` where `xxxxxx` is the `id`.
    The data of the scene is organized into NumPy files by *name* and *frame*.

    To create a new scene, use `Scene.create()`.
    To reference an existing scene, use `Scene.at()`.
    To list all scenes within a directory, use `Scene.list()`.
    """

    def __init__(self, paths: Union[str, math.Tensor]):
        self._paths = math.wrap(paths)
        self._properties: Union[dict, None] = None

    def __getitem__(self, item):
        return Scene(self._paths[item])

    def __getattr__(self, name: str) -> BoundDim:
        return BoundDim(self, name)

    def __variable_attrs__(self) -> Tuple[str, ...]:
        return 'paths',

    def __with_attrs__(self, **attrs):
        if 'paths' in attrs:
            return Scene(attrs['paths'])
        else:
            return Scene(self._paths)

    @property
    def shape(self):
        return self._paths.shape

    @property
    def is_batch(self):
        return self._paths.rank > 0

    @property
    def path(self) -> str:
        """
        Relative path of the scene directory.
        This property only exists for single scenes, not scene batches.
        """
        assert not self.is_batch, "Scene.path is not defined for scene batches."
        return self._paths.native()

    @property
    def paths(self) -> math.Tensor:
        return self._paths

    @staticmethod
    def stack(*scenes: 'Scene', dim: Shape = batch('batch')) -> 'Scene':
        return Scene(math.stack([s._paths for s in scenes], dim))

    @staticmethod
    def create(parent_directory: str,
               shape: math.Shape = math.EMPTY_SHAPE,
               name='sim',
               copy_calling_script=True,
               **dimensions) -> 'Scene':
        """
        Creates a new `Scene` or a batch of new scenes inside `parent_directory`.

        See Also:
            `Scene.at()`, `Scene.list()`.

        Args:
            parent_directory: Directory to hold the new `Scene`. If it doesn't exist, it will be created.
            shape: Determines number of scenes to create. Multiple scenes will be represented by a `Scene` with `is_batch=True`.
            name: Name of the directory (excluding index). Default is `'sim'`.
            copy_calling_script: Whether to copy the Python file that invoked this method into the `src` folder of all created scenes.
                See `Scene.copy_calling_script()`.
            dimensions: Additional batch dimensions

        Returns:
            Single `Scene` object representing the new scene(s).
        """
        shape = shape & math.batch(**dimensions)
        parent_directory = expanduser(parent_directory)
        abs_dir = abspath(parent_directory)
        if not isdir(abs_dir):
            os.makedirs(abs_dir)
            next_id = 0
        else:
            indices = [int(f[len(name)+1:]) for f in os.listdir(abs_dir) if f.startswith(f"{name}_")]
            next_id = max([-1] + indices) + 1
        ids = unpack_dim(wrap(tuple(range(next_id, next_id + shape.volume))), 'vector', shape)
        paths = math.map(lambda id_: join(parent_directory, f"{name}_{id_:06d}"), ids)
        scene = Scene(paths)
        scene.mkdir()
        if copy_calling_script:
            try:
                scene.copy_calling_script()
            except IOError as err:
                warnings.warn(f"Failed to copy calling script to scene during Scene.create(): {err}", RuntimeWarning)
        return scene

    @staticmethod
    def list(parent_directory: str,
             name='sim',
             include_other: bool = False,
             dim: Union[Shape, None] = None) -> Union['Scene', tuple]:
        """
        Lists all scenes inside the given directory.

        See Also:
            `Scene.at()`, `Scene.create()`.

        Args:
            parent_directory: Directory that contains scene folders.
            name: Name of the directory (excluding index). Default is `'sim'`.
            include_other: Whether folders that do not match the scene format should also be treated as scenes.
            dim: Stack dimension. If None, returns tuple of `Scene` objects. Otherwise, returns a scene batch with this dimension.

        Returns:
            `tuple` of scenes.
        """
        parent_directory = expanduser(parent_directory)
        abs_dir = abspath(parent_directory)
        if not isdir(abs_dir):
            return ()
        names = [sim for sim in os.listdir(abs_dir) if sim.startswith(f"{name}_") or (include_other and isdir(join(abs_dir, sim)))]
        names = list(sorted(names))
        if dim is None:
            return tuple(Scene(join(parent_directory, n)) for n in names)
        else:
            paths = math.wrap([join(parent_directory, n) for n in names], dim)
            return Scene(paths)

    @staticmethod
    def at(directory: Union[str, tuple, typing_list, math.Tensor, 'Scene'], id: Union[int, math.Tensor, None] = None) -> 'Scene':
        """
        Creates a `Scene` for an existing directory.

        See Also:
            `Scene.create()`, `Scene.list()`.

        Args:
            directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`.
            id: (Optional) Scene `id`, will be determined from `directory` if not specified.

        Returns:
            `Scene` object for existing scene.
        """
        if isinstance(directory, Scene):
            assert id is None, f"Got id={id} but directory is already a Scene."
            return directory
        if isinstance(directory, (tuple, list)):
            directory = math.wrap(directory, batch('scenes'))
        directory = math.map(lambda d: expanduser(d), math.wrap(directory))
        if isinstance(id, int) and id < 0:
            assert directory.shape.volume == 1
            scenes = Scene.list(directory.native())
            assert len(scenes) >= -id, f"Failed to get scene {id} at {directory}. {len(scenes)} scenes available in that directory."
            return scenes[id]
        if id is None:
            paths = directory
        else:
            id = math.wrap(id)
            paths = math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id)
        # test all exist
        for path in math.flatten(paths, flatten_batch=True):
            if not isdir(path):
                raise IOError(f"There is no scene at '{path}'")
        return Scene(paths)

    def subpath(self, name: str, create=False, create_parent=False) -> Union[str, tuple]:
        """
        Resolves the relative path `name` with this `Scene` as the root folder.

        Args:
            name: Relative path with this `Scene` as the root folder.
            create: Whether to create a directory of that name.
            create_parent: Whether to create the parent directory.

        Returns:
            Relative path including the path to this `Scene`.
            In batch mode, returns a `tuple`, else a `str`.
        """
        def single_subpath(path):
            path = join(path, name)
            if create_parent and not isdir(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            if create and not isdir(path):
                os.mkdir(path)
            return path

        result = math.map(single_subpath, self._paths)
        if result.rank == 0:
            return result.native()
        else:
            return result

    def _init_properties(self):
        if self._properties is not None:
            return

        def read_json(path: str) -> dict:
            json_file = join(path, "description.json")
            if isfile(json_file):
                with open(json_file) as stream:
                    props = json.load(stream)
                if '__tensors__' in props:
                    for key in props['__tensors__']:
                        props[key] = math.from_dict(props[key])
                return props
            else:
                return {}

        if self._paths.shape.volume == 1:
            self._properties = read_json(self._paths.native())
        else:
            self._properties = {}
            dicts = [read_json(p) for p in self._paths]
            keys = set(sum([tuple(d.keys()) for d in dicts], ()))
            for key in keys:
                assert all(key in d for d in dicts), f"Failed to create batched Scene because property '{key}' is present in some scenes but not all."
                if all([math.all(d[key] == dicts[0][key]) for d in dicts]):
                    self._properties[key] = dicts[0][key]
                else:
                    self._properties[key] = stack([d[key] for d in dicts], self._paths.shape)
        if '__tensors__' in self._properties:
            del self._properties['__tensors__']

    def exist_properties(self):
        """
        Checks whether the file `description.json` exists or has existed.
        """
        if self._properties is not None:
            return True  # must have been written or read
        else:
            json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json")
            return isfile(json_file)

    def exists_config(self):
        """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """
        if isinstance(self.path, str):
            return isfile(join(self.path, "description.json"))
        else:
            return any(isfile(join(p, "description.json")) for p in self.path)

    @property
    def properties(self):
        self._init_properties()
        return self._properties

    @properties.setter
    def properties(self, dict):
        self._properties = dict
        with open(join(self.path, "description.json"), "w") as out:
            json.dump(self._properties, out, indent=2)

    def put_property(self, key, value):
        """ See `Scene.put_properties()`. """
        self._init_properties()
        self._properties[key] = value
        self._write_properties()

    def put_properties(self, update: dict = None, **kw_updates):
        """
        Updates the properties dictionary and stores it in `description.json` of all scene folders.

        Args:
            update: new values, must be JSON serializable.
            kw_updates: additional update as keyword arguments. This overrides `update`.
        """
        self._init_properties()
        if update:
            self._properties.update(update)
        self._properties.update(kw_updates)
        for key, value in self._properties.items():
            if isinstance(value, (np.int64, np.int32)):
                value = int(value)
            elif isinstance(value, (np.float16, np.float32, np.float64, np.float16)) or (hasattr(np, 'float128') and isinstance(value, np.float128)):
                value = float(value)
            self._properties[key] = value
        self._write_properties()

    def _get_properties(self, index: dict):
        result = dict(self._properties)
        tensor_names = []
        for key, value in self._properties.items():
            if isinstance(value, math.Tensor):
                value = value[index]
                if value.rank == 0:
                    value = value.dtype.kind(value)
                else:
                    value = math.to_dict(value)
                    tensor_names.append(key)
                result[key] = value
        if tensor_names:
            result['__tensors__'] = tuple(tensor_names)
        return result

    def _write_properties(self):
        for instance in self.paths.shape.meshgrid():
            path = self.paths[instance].native()
            instance_properties = self._get_properties(instance)
            with open(join(path, "description.json"), "w") as out:
                json.dump(instance_properties, out, indent=2)

    def write(self, data: dict = None, frame=0, **kw_data):
        """
        Writes fields to this scene.
        One NumPy file will be created for each `phi.field.Field`

        See Also:
            `Scene.read()`.

        Args:
            data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`.
            kw_data: Additional data, overrides elements in `data`.
            frame: Frame number.
        """
        data = dict(data) if data else {}
        data.update(kw_data)
        for name, field in data.items():
            self.write_field(field, name, frame)

    def write_field(self, field: SampledField, name: str, frame: int):
        """
        Write a `SampledField` to a file.
        The filenames are created from the provided names and the frame index in accordance with the
        scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .

        Args:
            field: single field or structure of Fields to save.
            name: Base file name.
            frame: Frame number as `int`, typically time step index.
        """
        if not isinstance(field, SampledField):
            raise ValueError(f"Only SampledField instances can be saved but got {field}")
        name = _slugify_filename(name)
        files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
        write(field, files)

    def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField:
        """
        Reads a single `SampledField` from files contained in this `Scene` (batch).

        Args:
            name: Base file name.
            frame: Frame number as `int`, typically time step index.
            convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

        Returns:
            `SampledField`
        """
        name = _slugify_filename(name)
        files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
        return read(files, convert_to_backend=convert_to_backend)

    read_array = read_field

    def read(self, *names: str, frame=0, convert_to_backend=True):
        """
        Reads one or multiple fields from disc.

        See Also:
            `Scene.write()`.

        Args:
            names: Single field name or sequence of field names.
            frame: Frame number.
            convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

        Returns:
            Single `phi.field.Field` or sequence of fields, depending on the type of `names`.
        """
        if len(names) == 1 and isinstance(names[0], (tuple, list)):
            names = names[0]
        result = [self.read_array(name, frame, convert_to_backend) for name in names]
        return result[0] if len(names) == 1 else result

    @property
    def fieldnames(self) -> tuple:
        """ Determines all field names present in this `Scene`, independent of frame. """
        return get_fieldnames(self.path)

    @property
    def frames(self):
        """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """
        return get_frames(self.path, mode=set.union)

    @property
    def complete_frames(self):
        """
        Determines all frame number for which all existing fields are available.
        If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields.

        See Also:
            `Scene.frames`
        """
        return get_frames(self.path, mode=set.intersection)

    def __repr__(self):
        return f"{self.paths:no-dtype}"

    def __eq__(self, other):
        return isinstance(other, Scene) and (other._paths == self._paths).all

    def copy_calling_script(self, full_trace=False, include_context_information=True):
        """
        Copies the Python file that called this method into the `src` folder of this `Scene`.

        In batch mode, the script is copied to all scenes.

        Args:
            full_trace: Whether to include scripts that indirectly called this method.
            include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`.
        """
        script_paths = [frame.filename for frame in inspect.stack()]
        script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths))
        script_paths = set(script_paths) if full_trace else [script_paths[0]]
        self.subpath('src', create=True)
        for script_path in script_paths:
            if script_path.endswith('.py'):
                self.copy_src(script_path, only_external=False)
            elif 'ipython' in script_path:
                from IPython import get_ipython
                cells = get_ipython().user_ns['In']
                blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)]
                text = "\n\n".join(blocks)
                self.copy_src_text('ipython.py', text)
        if include_context_information:
            for path in math.flatten(self._paths, flatten_batch=True):
                with open(join(path, 'src', 'context.json'), 'w') as context_file:
                    json.dump({
                        'phi_version': phi_version,
                        'argv': sys.argv
                    }, context_file)

    def copy_src(self, script_path, only_external=True):
        for path in math.flatten(self._paths, flatten_batch=True):
            if not only_external or not _is_phi_file(script_path):
                shutil.copy(script_path, join(path, 'src', basename(script_path)))

    def copy_src_text(self, filename, text):
        for path in math.flatten(self._paths, flatten_batch=True):
            target = join(path, 'src', filename)
            with open(target, "w") as file:
                file.writelines(text)

    def mkdir(self):
        for path in math.flatten(self._paths, flatten_batch=True):
            isdir(path) or os.mkdir(path)

    def remove(self):
        """ Deletes the scene directory and all contained files. """
        for p in math.flatten(self._paths, flatten_batch=True):
            p = abspath(p)
            if isdir(p):
                shutil.rmtree(p)

    def rename(self, name: str):
        """ Deletes the scene directory and all contained files. """
        for p in math.flatten(self._paths, flatten_batch=True):
            p = abspath(p)
            if isdir(p):
                new_path = os.path.join(os.path.dirname(p), name)
                print(f"Renaming {p} to {new_path}")
                shutil.move(p, new_path)

Static methods

def at(directory: Union[str, tuple, list, phiml.math._tensors.Tensor, ForwardRef('Scene')], id: Union[int, phiml.math._tensors.Tensor, None] = None) ‑> phi.field._scene.Scene

Creates a Scene for an existing directory.

See Also: Scene.create(), Scene.list().

Args

directory
Either directory containing scene folder if id is given, or scene path if id=None.
id
(Optional) Scene id, will be determined from directory if not specified.

Returns

Scene object for existing scene.

Expand source code
@staticmethod
def at(directory: Union[str, tuple, typing_list, math.Tensor, 'Scene'], id: Union[int, math.Tensor, None] = None) -> 'Scene':
    """
    Creates a `Scene` for an existing directory.

    See Also:
        `Scene.create()`, `Scene.list()`.

    Args:
        directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`.
        id: (Optional) Scene `id`, will be determined from `directory` if not specified.

    Returns:
        `Scene` object for existing scene.
    """
    if isinstance(directory, Scene):
        assert id is None, f"Got id={id} but directory is already a Scene."
        return directory
    if isinstance(directory, (tuple, list)):
        directory = math.wrap(directory, batch('scenes'))
    directory = math.map(lambda d: expanduser(d), math.wrap(directory))
    if isinstance(id, int) and id < 0:
        assert directory.shape.volume == 1
        scenes = Scene.list(directory.native())
        assert len(scenes) >= -id, f"Failed to get scene {id} at {directory}. {len(scenes)} scenes available in that directory."
        return scenes[id]
    if id is None:
        paths = directory
    else:
        id = math.wrap(id)
        paths = math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id)
    # test all exist
    for path in math.flatten(paths, flatten_batch=True):
        if not isdir(path):
            raise IOError(f"There is no scene at '{path}'")
    return Scene(paths)
def create(parent_directory: str, shape: phiml.math._shape.Shape = (), name='sim', copy_calling_script=True, **dimensions) ‑> phi.field._scene.Scene

Creates a new Scene or a batch of new scenes inside parent_directory.

See Also: Scene.at(), Scene.list().

Args

parent_directory
Directory to hold the new Scene. If it doesn't exist, it will be created.
shape
Determines number of scenes to create. Multiple scenes will be represented by a Scene with is_batch=True.
name
Name of the directory (excluding index). Default is 'sim'.
copy_calling_script
Whether to copy the Python file that invoked this method into the src folder of all created scenes. See Scene.copy_calling_script().
dimensions
Additional batch dimensions

Returns

Single Scene object representing the new scene(s).

Expand source code
@staticmethod
def create(parent_directory: str,
           shape: math.Shape = math.EMPTY_SHAPE,
           name='sim',
           copy_calling_script=True,
           **dimensions) -> 'Scene':
    """
    Creates a new `Scene` or a batch of new scenes inside `parent_directory`.

    See Also:
        `Scene.at()`, `Scene.list()`.

    Args:
        parent_directory: Directory to hold the new `Scene`. If it doesn't exist, it will be created.
        shape: Determines number of scenes to create. Multiple scenes will be represented by a `Scene` with `is_batch=True`.
        name: Name of the directory (excluding index). Default is `'sim'`.
        copy_calling_script: Whether to copy the Python file that invoked this method into the `src` folder of all created scenes.
            See `Scene.copy_calling_script()`.
        dimensions: Additional batch dimensions

    Returns:
        Single `Scene` object representing the new scene(s).
    """
    shape = shape & math.batch(**dimensions)
    parent_directory = expanduser(parent_directory)
    abs_dir = abspath(parent_directory)
    if not isdir(abs_dir):
        os.makedirs(abs_dir)
        next_id = 0
    else:
        indices = [int(f[len(name)+1:]) for f in os.listdir(abs_dir) if f.startswith(f"{name}_")]
        next_id = max([-1] + indices) + 1
    ids = unpack_dim(wrap(tuple(range(next_id, next_id + shape.volume))), 'vector', shape)
    paths = math.map(lambda id_: join(parent_directory, f"{name}_{id_:06d}"), ids)
    scene = Scene(paths)
    scene.mkdir()
    if copy_calling_script:
        try:
            scene.copy_calling_script()
        except IOError as err:
            warnings.warn(f"Failed to copy calling script to scene during Scene.create(): {err}", RuntimeWarning)
    return scene
def list(parent_directory: str, name='sim', include_other: bool = False, dim: Optional[phiml.math._shape.Shape] = None) ‑> Union[phi.field._scene.Scene, tuple]

Lists all scenes inside the given directory.

See Also: Scene.at(), Scene.create().

Args

parent_directory
Directory that contains scene folders.
name
Name of the directory (excluding index). Default is 'sim'.
include_other
Whether folders that do not match the scene format should also be treated as scenes.
dim
Stack dimension. If None, returns tuple of Scene objects. Otherwise, returns a scene batch with this dimension.

Returns

tuple of scenes.

Expand source code
@staticmethod
def list(parent_directory: str,
         name='sim',
         include_other: bool = False,
         dim: Union[Shape, None] = None) -> Union['Scene', tuple]:
    """
    Lists all scenes inside the given directory.

    See Also:
        `Scene.at()`, `Scene.create()`.

    Args:
        parent_directory: Directory that contains scene folders.
        name: Name of the directory (excluding index). Default is `'sim'`.
        include_other: Whether folders that do not match the scene format should also be treated as scenes.
        dim: Stack dimension. If None, returns tuple of `Scene` objects. Otherwise, returns a scene batch with this dimension.

    Returns:
        `tuple` of scenes.
    """
    parent_directory = expanduser(parent_directory)
    abs_dir = abspath(parent_directory)
    if not isdir(abs_dir):
        return ()
    names = [sim for sim in os.listdir(abs_dir) if sim.startswith(f"{name}_") or (include_other and isdir(join(abs_dir, sim)))]
    names = list(sorted(names))
    if dim is None:
        return tuple(Scene(join(parent_directory, n)) for n in names)
    else:
        paths = math.wrap([join(parent_directory, n) for n in names], dim)
        return Scene(paths)
def stack(*scenes: Scene, dim: phiml.math._shape.Shape = (batchᵇ=None)) ‑> phi.field._scene.Scene
Expand source code
@staticmethod
def stack(*scenes: 'Scene', dim: Shape = batch('batch')) -> 'Scene':
    return Scene(math.stack([s._paths for s in scenes], dim))

Instance variables

var complete_frames

Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields.

See Also: Scene.frames

Expand source code
@property
def complete_frames(self):
    """
    Determines all frame number for which all existing fields are available.
    If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields.

    See Also:
        `Scene.frames`
    """
    return get_frames(self.path, mode=set.intersection)
var fieldnames : tuple

Determines all field names present in this Scene, independent of frame.

Expand source code
@property
def fieldnames(self) -> tuple:
    """ Determines all field names present in this `Scene`, independent of frame. """
    return get_fieldnames(self.path)
var frames

Determines all frame numbers present in this Scene, independent of field names. See Scene.complete_frames.

Expand source code
@property
def frames(self):
    """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """
    return get_frames(self.path, mode=set.union)
var is_batch
Expand source code
@property
def is_batch(self):
    return self._paths.rank > 0
var path : str

Relative path of the scene directory. This property only exists for single scenes, not scene batches.

Expand source code
@property
def path(self) -> str:
    """
    Relative path of the scene directory.
    This property only exists for single scenes, not scene batches.
    """
    assert not self.is_batch, "Scene.path is not defined for scene batches."
    return self._paths.native()
var paths : phiml.math._tensors.Tensor
Expand source code
@property
def paths(self) -> math.Tensor:
    return self._paths
var properties
Expand source code
@property
def properties(self):
    self._init_properties()
    return self._properties
var shape
Expand source code
@property
def shape(self):
    return self._paths.shape

Methods

def copy_calling_script(self, full_trace=False, include_context_information=True)

Copies the Python file that called this method into the src folder of this Scene.

In batch mode, the script is copied to all scenes.

Args

full_trace
Whether to include scripts that indirectly called this method.
include_context_information
If True, writes the phiflow version and sys.argv into context.json.
Expand source code
def copy_calling_script(self, full_trace=False, include_context_information=True):
    """
    Copies the Python file that called this method into the `src` folder of this `Scene`.

    In batch mode, the script is copied to all scenes.

    Args:
        full_trace: Whether to include scripts that indirectly called this method.
        include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`.
    """
    script_paths = [frame.filename for frame in inspect.stack()]
    script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths))
    script_paths = set(script_paths) if full_trace else [script_paths[0]]
    self.subpath('src', create=True)
    for script_path in script_paths:
        if script_path.endswith('.py'):
            self.copy_src(script_path, only_external=False)
        elif 'ipython' in script_path:
            from IPython import get_ipython
            cells = get_ipython().user_ns['In']
            blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)]
            text = "\n\n".join(blocks)
            self.copy_src_text('ipython.py', text)
    if include_context_information:
        for path in math.flatten(self._paths, flatten_batch=True):
            with open(join(path, 'src', 'context.json'), 'w') as context_file:
                json.dump({
                    'phi_version': phi_version,
                    'argv': sys.argv
                }, context_file)
def copy_src(self, script_path, only_external=True)
Expand source code
def copy_src(self, script_path, only_external=True):
    for path in math.flatten(self._paths, flatten_batch=True):
        if not only_external or not _is_phi_file(script_path):
            shutil.copy(script_path, join(path, 'src', basename(script_path)))
def copy_src_text(self, filename, text)
Expand source code
def copy_src_text(self, filename, text):
    for path in math.flatten(self._paths, flatten_batch=True):
        target = join(path, 'src', filename)
        with open(target, "w") as file:
            file.writelines(text)
def exist_properties(self)

Checks whether the file description.json exists or has existed.

Expand source code
def exist_properties(self):
    """
    Checks whether the file `description.json` exists or has existed.
    """
    if self._properties is not None:
        return True  # must have been written or read
    else:
        json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json")
        return isfile(json_file)
def exists_config(self)

Tests if the configuration file description.json exists. In batch mode, tests if any configuration exists.

Expand source code
def exists_config(self):
    """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """
    if isinstance(self.path, str):
        return isfile(join(self.path, "description.json"))
    else:
        return any(isfile(join(p, "description.json")) for p in self.path)
def mkdir(self)
Expand source code
def mkdir(self):
    for path in math.flatten(self._paths, flatten_batch=True):
        isdir(path) or os.mkdir(path)
def put_properties(self, update: dict = None, **kw_updates)

Updates the properties dictionary and stores it in description.json of all scene folders.

Args

update
new values, must be JSON serializable.
kw_updates
additional update as keyword arguments. This overrides update.
Expand source code
def put_properties(self, update: dict = None, **kw_updates):
    """
    Updates the properties dictionary and stores it in `description.json` of all scene folders.

    Args:
        update: new values, must be JSON serializable.
        kw_updates: additional update as keyword arguments. This overrides `update`.
    """
    self._init_properties()
    if update:
        self._properties.update(update)
    self._properties.update(kw_updates)
    for key, value in self._properties.items():
        if isinstance(value, (np.int64, np.int32)):
            value = int(value)
        elif isinstance(value, (np.float16, np.float32, np.float64, np.float16)) or (hasattr(np, 'float128') and isinstance(value, np.float128)):
            value = float(value)
        self._properties[key] = value
    self._write_properties()
def put_property(self, key, value)
Expand source code
def put_property(self, key, value):
    """ See `Scene.put_properties()`. """
    self._init_properties()
    self._properties[key] = value
    self._write_properties()
def read(self, *names: str, frame=0, convert_to_backend=True)

Reads one or multiple fields from disc.

See Also: Scene.write().

Args

names
Single field name or sequence of field names.
frame
Frame number.
convert_to_backend
Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

Returns

Single Field or sequence of fields, depending on the type of names.

Expand source code
def read(self, *names: str, frame=0, convert_to_backend=True):
    """
    Reads one or multiple fields from disc.

    See Also:
        `Scene.write()`.

    Args:
        names: Single field name or sequence of field names.
        frame: Frame number.
        convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

    Returns:
        Single `phi.field.Field` or sequence of fields, depending on the type of `names`.
    """
    if len(names) == 1 and isinstance(names[0], (tuple, list)):
        names = names[0]
    result = [self.read_array(name, frame, convert_to_backend) for name in names]
    return result[0] if len(names) == 1 else result
def read_array(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.SampledField

Reads a single SampledField from files contained in this Scene (batch).

Args

name
Base file name.
frame
Frame number as int, typically time step index.
convert_to_backend
Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

Returns

SampledField

Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField:
    """
    Reads a single `SampledField` from files contained in this `Scene` (batch).

    Args:
        name: Base file name.
        frame: Frame number as `int`, typically time step index.
        convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

    Returns:
        `SampledField`
    """
    name = _slugify_filename(name)
    files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
    return read(files, convert_to_backend=convert_to_backend)
def read_field(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.SampledField

Reads a single SampledField from files contained in this Scene (batch).

Args

name
Base file name.
frame
Frame number as int, typically time step index.
convert_to_backend
Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

Returns

SampledField

Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField:
    """
    Reads a single `SampledField` from files contained in this `Scene` (batch).

    Args:
        name: Base file name.
        frame: Frame number as `int`, typically time step index.
        convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.

    Returns:
        `SampledField`
    """
    name = _slugify_filename(name)
    files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
    return read(files, convert_to_backend=convert_to_backend)
def remove(self)

Deletes the scene directory and all contained files.

Expand source code
def remove(self):
    """ Deletes the scene directory and all contained files. """
    for p in math.flatten(self._paths, flatten_batch=True):
        p = abspath(p)
        if isdir(p):
            shutil.rmtree(p)
def rename(self, name: str)

Deletes the scene directory and all contained files.

Expand source code
def rename(self, name: str):
    """ Deletes the scene directory and all contained files. """
    for p in math.flatten(self._paths, flatten_batch=True):
        p = abspath(p)
        if isdir(p):
            new_path = os.path.join(os.path.dirname(p), name)
            print(f"Renaming {p} to {new_path}")
            shutil.move(p, new_path)
def subpath(self, name: str, create=False, create_parent=False) ‑> Union[str, tuple]

Resolves the relative path name with this Scene as the root folder.

Args

name
Relative path with this Scene as the root folder.
create
Whether to create a directory of that name.
create_parent
Whether to create the parent directory.

Returns

Relative path including the path to this Scene. In batch mode, returns a tuple, else a str.

Expand source code
def subpath(self, name: str, create=False, create_parent=False) -> Union[str, tuple]:
    """
    Resolves the relative path `name` with this `Scene` as the root folder.

    Args:
        name: Relative path with this `Scene` as the root folder.
        create: Whether to create a directory of that name.
        create_parent: Whether to create the parent directory.

    Returns:
        Relative path including the path to this `Scene`.
        In batch mode, returns a `tuple`, else a `str`.
    """
    def single_subpath(path):
        path = join(path, name)
        if create_parent and not isdir(os.path.dirname(path)):
            os.makedirs(os.path.dirname(path))
        if create and not isdir(path):
            os.mkdir(path)
        return path

    result = math.map(single_subpath, self._paths)
    if result.rank == 0:
        return result.native()
    else:
        return result
def write(self, data: dict = None, frame=0, **kw_data)

Writes fields to this scene. One NumPy file will be created for each Field

See Also: Scene.read().

Args

data
dict mapping field names to Field objects that can be written using write().
kw_data
Additional data, overrides elements in data.
frame
Frame number.
Expand source code
def write(self, data: dict = None, frame=0, **kw_data):
    """
    Writes fields to this scene.
    One NumPy file will be created for each `phi.field.Field`

    See Also:
        `Scene.read()`.

    Args:
        data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`.
        kw_data: Additional data, overrides elements in `data`.
        frame: Frame number.
    """
    data = dict(data) if data else {}
    data.update(kw_data)
    for name, field in data.items():
        self.write_field(field, name, frame)
def write_field(self, field: phi.field._field.SampledField, name: str, frame: int)

Write a SampledField to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .

Args

field
single field or structure of Fields to save.
name
Base file name.
frame
Frame number as int, typically time step index.
Expand source code
def write_field(self, field: SampledField, name: str, frame: int):
    """
    Write a `SampledField` to a file.
    The filenames are created from the provided names and the frame index in accordance with the
    scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .

    Args:
        field: single field or structure of Fields to save.
        name: Base file name.
        frame: Frame number as `int`, typically time step index.
    """
    if not isinstance(field, SampledField):
        raise ValueError(f"Only SampledField instances can be saved but got {field}")
    name = _slugify_filename(name)
    files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
    write(field, files)
class GeometryMask (geometry: phi.geom._geom.Geometry, balance: Union[phiml.math._tensors.Tensor, float] = 0.5)

Deprecated since version 1.3. Use mask() or resample() instead.

Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """
    def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5):
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)
        super().__init__(geometry)
        self.balance = balance

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        return self.geometry.approximate_fraction_inside(geometry, self.balance)

    def __getitem__(self, item: dict):
        return SoftGeometryMask(self.geometry[item], self.balance)

Ancestors

  • phi.field._mask.HardGeometryMask
  • phi.field._field.Field
class SoftGeometryMask (geometry: phi.geom._geom.Geometry, balance: Union[phiml.math._tensors.Tensor, float] = 0.5)

Deprecated since version 1.3. Use mask() or resample() instead.

Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """
    def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5):
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)
        super().__init__(geometry)
        self.balance = balance

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        return self.geometry.approximate_fraction_inside(geometry, self.balance)

    def __getitem__(self, item: dict):
        return SoftGeometryMask(self.geometry[item], self.balance)

Ancestors

  • phi.field._mask.HardGeometryMask
  • phi.field._field.Field
class StaggeredGrid (values: Any = 0.0, extrapolation: Union[float, phiml.math.extrapolation.Extrapolation, dict, phi.field._field.Field] = 0, bounds: Union[phi.geom._box.Box, float] = None, resolution: Union[int, phiml.math._shape.Shape] = None, **resolution_: Union[int, phiml.math._tensors.Tensor])

N-dimensional grid whose vector components are sampled at the respective face centers. A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation.

Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components.

See Also: CenteredGrid, Grid, SampledField, Field, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html

Args

values

Values to use for the grid. Has to be one of the following:

  • Geometry: sets inside values to 1, outside to 0
  • Field: resamples the Field to the staggered sample points
  • Number: uses the value for all sample points
  • tuple or list: interprets the sequence as vector, used for all sample points
  • phiml.math.Tensor with staggered shape: uses tensor values as grid values. Must contain a vector dimension with each slice consisting of one more element along the dimension they describe. Use phiml.math.stack() to manually create this non-uniform tensor.
  • Function values(x) where x is a phiml.math.Tensor representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function.
extrapolation
The grid extrapolation determines the value outside the values tensor. Allowed types: float, phiml.math.Tensor, phiml.math.extrapolation.Extrapolation.
bounds
Physical size and location of the grid as Box. If the resolution is determined through resolution of values, a float can be passed for bounds to create a unit box.
resolution
Grid resolution as purely spatial phiml.math.Shape. If bounds is given as a Box, the resolution may be specified as an int to be equal along all axes.
**resolution_
Spatial dimensions as keyword arguments. Typically either resolution or spatial_dims are specified.
Expand source code
class StaggeredGrid(Grid, metaclass=deprecated_field_class('StaggeredGrid', parent_metaclass=type(Grid))):
    """
    N-dimensional grid whose vector components are sampled at the respective face centers.
    A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation.
    
    Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components.

    See Also:
        `CenteredGrid`,
        `Grid`,
        `SampledField`,
        `Field`,
        module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 values: Any = 0.,
                 extrapolation: Union[float, Extrapolation, dict, Field] = 0,
                 bounds: Union[Box, float] = None,
                 resolution: Union[Shape, int] = None,
                 **resolution_: Union[int, Tensor]):
        """
        Args:
            values: Values to use for the grid.
                Has to be one of the following:

                * `phi.geom.Geometry`: sets inside values to 1, outside to 0
                * `Field`: resamples the Field to the staggered sample points
                * `Number`: uses the value for all sample points
                * `tuple` or `list`: interprets the sequence as vector, used for all sample points
                * `phiml.math.Tensor` with staggered shape: uses tensor values as grid values.
                  Must contain a `vector` dimension with each slice consisting of one more element along the dimension they describe.
                  Use `phiml.math.stack()` to manually create this non-uniform tensor.
                * Function `values(x)` where `x` is a `phiml.math.Tensor` representing the physical location.
                    The spatial dimensions of the grid will be passed as batch dimensions to the function.

            extrapolation: The grid extrapolation determines the value outside the `values` tensor.
                Allowed types: `float`, `phiml.math.Tensor`, `phiml.math.extrapolation.Extrapolation`.
            bounds: Physical size and location of the grid as `phi.geom.Box`.
                If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box.
            resolution: Grid resolution as purely spatial `phiml.math.Shape`.
                If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes.
            **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified.
        """
        extrapolation = as_extrapolation(extrapolation)
        if resolution is None and not resolution_:
            assert isinstance(values, Tensor), "Grid resolution must be specified when 'values' is not a Tensor."
            if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in spatial(values).names):  # non-uniform values required
                if values.shape.is_uniform:
                    values = unstack_staggered_tensor(values, extrapolation)
                resolution = resolution_from_staggered_tensor(values, extrapolation)
            else:
                resolution = spatial(values)
            bounds = _get_bounds(bounds, resolution)
            bounds = bounds or Box(math.const_vec(0, resolution), math.wrap(resolution, channel('vector')))
            elements = staggered_elements(resolution, bounds, extrapolation)
        else:
            resolution = _get_resolution(resolution, resolution_, bounds)
            bounds = _get_bounds(bounds, resolution)
            elements = staggered_elements(resolution, bounds, extrapolation)
            if isinstance(values, math.Tensor):
                if not spatial(values):
                    values = expand_staggered(values, resolution, extrapolation)
                if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in resolution.names):  # non-uniform values required
                    if values.shape.is_uniform:
                        values = unstack_staggered_tensor(values, extrapolation)
                    else:  # Keep dim order from data and check it matches resolution
                        assert set(resolution_from_staggered_tensor(values, extrapolation)) == set(resolution), f"Failed to create StaggeredGrid: values {values.shape} do not match given resolution {resolution} for extrapolation {extrapolation}. See https://tum-pbs.github.io/PhiFlow/Staggered_Grids.html"
            elif isinstance(values, Geometry):
                values = reduce_sample(values, elements)
            elif isinstance(values, Field):
                values = reduce_sample(values, elements)
            elif callable(values):
                values = _sample_function(values, elements)
                if elements.shape.shape.rank > 1:  # Different number of X and Y faces
                    assert isinstance(values, TensorStack), f"values function must return a staggered Tensor but returned {type(values)}"
                assert 'staggered_direction' in values.shape
                if 'vector' in values.shape:
                    values = math.stack([values.staggered_direction[i].vector[i] for i in range(resolution.rank)], channel(vector=resolution))
                else:
                    values = values.staggered_direction.as_channel('vector')
            else:
                values = expand_staggered(math.tensor(values), resolution, extrapolation)
        if values.dtype.kind not in (float, complex):
            values = math.to_float(values)
        assert resolution.spatial_rank == bounds.spatial_rank, f"Resolution {resolution} does not match bounds {bounds}"
        Grid.__init__(self, elements, values, extrapolation, resolution, bounds)

    @property
    def cells(self):
        return GridCell(self.resolution, self.bounds)

    def with_extrapolation(self, extrapolation: Extrapolation):
        extrapolation = as_extrapolation(extrapolation)
        if all([extrapolation.valid_outer_faces(dim) == self.extrapolation.valid_outer_faces(dim) for dim in self.resolution.names]):
            return StaggeredGrid(self.values, extrapolation=extrapolation, bounds=self.bounds)
        else:
            values = []
            for dim, component in zip(self.shape.spatial.names, self.values.vector):
                old_lo, old_hi = [int(v) for v in self.extrapolation.valid_outer_faces(dim)]
                new_lo, new_hi = [int(v) for v in extrapolation.valid_outer_faces(dim)]
                widths = (new_lo - old_lo, new_hi - old_hi)
                values.append(math.pad(component, {dim: widths}, self.extrapolation, bounds=self.bounds))
            values = math.stack(values, channel(vector=self.resolution))
            return StaggeredGrid(values, extrapolation, bounds=self.bounds)

    def _sample(self, geometry: Geometry, **kwargs) -> Tensor:
        channels = [sample(component, geometry, **kwargs) for component in self.vector.unstack()]
        return math.stack(channels, geometry.shape['vector'])

    def closest_values(self, points: Geometry):
        if 'staggered_direction' in points.shape:
            points_ = points.unstack('staggered_direction')
            channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())]
        else:
            channels = [component.closest_values(points) for component in self.vector.unstack()]
        return math.stack(channels, points.shape['vector'])

    def at_centers(self) -> CenteredGrid:
        """
        Interpolates the staggered values to the cell centers.

        Returns:
            `CenteredGrid` sampled at cell centers.
        """
        return CenteredGrid(self, resolution=self.resolution, bounds=self.bounds, extrapolation=self.extrapolation)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        if not item:
            return self
        if 'vector' in item:
            selection = item['vector']
            if isinstance(selection, int):
                item['vector'] = self.resolution.names[selection]
        values = self._values[{dim: sel for dim, sel in item.items() if dim not in self.shape.spatial}]
        for dim, sel in item.items():
            if dim in self.shape.spatial:
                raise AssertionError("Cannot slice StaggeredGrid along spatial dimensions.")
                # sel = slice(sel, sel + 1) if isinstance(sel, int) else sel
                # values = []
                # for vdim, val in zip(self.shape.spatial.names, self.values.unstack('vector')):
                #     if vdim == dim:
                #         values.append(val[{dim: slice(sel.start, sel.stop + 1)}])
                #     else:
                #         values.append(val[{dim: sel}])
                # values = math.stack(values, channel('vector'))
        extrapolation = self._extrapolation[item]
        bounds = GridCell(self._resolution, self._bounds)[item].bounds
        if 'vector' in item:
            selection = item['vector']
            if isinstance(selection, str) and ',' in selection:
                selection = parse_dim_order(selection)
            if isinstance(selection, str):  # single item name
                item_names = self.shape.get_item_names('vector', fallback_spatial=True)
                assert selection in item_names, f"Accessing field.vector['{selection}'] failed. Item names are {item_names}."
                selection = item_names.index(selection)
            if isinstance(selection, int):
                dim = self.resolution.names[selection]
                comp_cells = GridCell(self.resolution, bounds).stagger(dim, *self.extrapolation.valid_outer_faces(dim))
                return CenteredGrid(values, bounds=comp_cells.bounds, extrapolation=extrapolation)
            else:
                assert isinstance(selection, slice) and not selection.start and not selection.stop
        return StaggeredGrid(values, bounds=bounds, extrapolation=extrapolation)

    def uniform_values(self):
        if self.values.shape.is_uniform:
            return self.values
        else:
            return self.staggered_tensor()

    def staggered_tensor(self) -> Tensor:
        """
        Stacks all component grids into a single uniform `phiml.math.Tensor`.
        The individual components are padded to a common (larger) shape before being stacked.
        The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension.

        Returns:
            Uniform `phiml.math.Tensor`.
        """
        padded = []
        for dim, component in zip(self.resolution.names, math.unstack(self.values, 'vector')):
            widths = {d: (0, 1) for d in self.resolution.names}
            lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim)
            widths[dim] = (int(not lo_valid), int(not up_valid))
            padded.append(math.pad(component, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
        result = math.stack(padded, channel(vector=self.resolution))
        assert result.shape.is_uniform
        return result

    def _op2(self, other, operator):
        if isinstance(other, StaggeredGrid) and self.bounds == other.bounds and self.shape.spatial == other.shape.spatial:
            values = operator(self._values, other.values)
            extrapolation_ = operator(self._extrapolation, other.extrapolation)
            return StaggeredGrid(values=values, extrapolation=extrapolation_, bounds=self.bounds)
        else:
            return SampledField._op2(self, other, operator)

Ancestors

  • phi.field._grid.Grid
  • phi.field._field.SampledField
  • phi.field._field.Field

Instance variables

var cells
Expand source code
@property
def cells(self):
    return GridCell(self.resolution, self.bounds)

Methods

def at_centers(self) ‑> phi.field._grid.CenteredGrid

Interpolates the staggered values to the cell centers.

Returns

CenteredGrid sampled at cell centers.

Expand source code
def at_centers(self) -> CenteredGrid:
    """
    Interpolates the staggered values to the cell centers.

    Returns:
        `CenteredGrid` sampled at cell centers.
    """
    return CenteredGrid(self, resolution=self.resolution, bounds=self.bounds, extrapolation=self.extrapolation)
def closest_values(self, points: phi.geom._geom.Geometry)

Sample the closest grid point values of this field at the world-space locations (in physical units) given by points. Points must have a single channel dimension named vector. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions.

Args

points
world-space locations

Returns

Closest grid point values as a Tensor. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in points, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions 'closest_<dim>' where <dim> refers to the name of a spatial dimension.

Expand source code
def closest_values(self, points: Geometry):
    if 'staggered_direction' in points.shape:
        points_ = points.unstack('staggered_direction')
        channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())]
    else:
        channels = [component.closest_values(points) for component in self.vector.unstack()]
    return math.stack(channels, points.shape['vector'])
def staggered_tensor(self) ‑> phiml.math._tensors.Tensor

Stacks all component grids into a single uniform phiml.math.Tensor. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid resolution in every spatial dimension.

Returns

Uniform phiml.math.Tensor.

Expand source code
def staggered_tensor(self) -> Tensor:
    """
    Stacks all component grids into a single uniform `phiml.math.Tensor`.
    The individual components are padded to a common (larger) shape before being stacked.
    The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension.

    Returns:
        Uniform `phiml.math.Tensor`.
    """
    padded = []
    for dim, component in zip(self.resolution.names, math.unstack(self.values, 'vector')):
        widths = {d: (0, 1) for d in self.resolution.names}
        lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim)
        widths[dim] = (int(not lo_valid), int(not up_valid))
        padded.append(math.pad(component, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
    result = math.stack(padded, channel(vector=self.resolution))
    assert result.shape.is_uniform
    return result
def uniform_values(self)

Returns a uniform tensor containing values.

For periodic grids, which always have a uniform value tensor, `values' is returned directly. If values is not uniform, it is padded as in StaggeredGrid.staggered_tensor().

Expand source code
def uniform_values(self):
    if self.values.shape.is_uniform:
        return self.values
    else:
        return self.staggered_tensor()
def with_extrapolation(self, extrapolation: phiml.math.extrapolation.Extrapolation)

Returns a copy of this field with values replaced.

Expand source code
def with_extrapolation(self, extrapolation: Extrapolation):
    extrapolation = as_extrapolation(extrapolation)
    if all([extrapolation.valid_outer_faces(dim) == self.extrapolation.valid_outer_faces(dim) for dim in self.resolution.names]):
        return StaggeredGrid(self.values, extrapolation=extrapolation, bounds=self.bounds)
    else:
        values = []
        for dim, component in zip(self.shape.spatial.names, self.values.vector):
            old_lo, old_hi = [int(v) for v in self.extrapolation.valid_outer_faces(dim)]
            new_lo, new_hi = [int(v) for v in extrapolation.valid_outer_faces(dim)]
            widths = (new_lo - old_lo, new_hi - old_hi)
            values.append(math.pad(component, {dim: widths}, self.extrapolation, bounds=self.bounds))
        values = math.stack(values, channel(vector=self.resolution))
        return StaggeredGrid(values, extrapolation, bounds=self.bounds)