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

Functions

def CenteredGrid(values: Any = 0.0,
boundary: Any = 0.0,
bounds: phi.geom._box.Box = None,
resolution: int = None,
extrapolation: Any = None,
convert=True,
**resolution_: int) ‑> phi.field._field.Field
Expand source code
def CenteredGrid(values: Any = 0.,
                 boundary: Any = 0.,
                 bounds: Box or float = None,
                 resolution: int or Shape = None,
                 extrapolation: Any = None,
                 convert=True,
                 **resolution_: int or Tensor) -> Field:
    """
    Create an n-dimensional grid with values sampled at the cell centers.
    A centered grid is defined through its `CenteredGrid.values` `phi.math.Tensor`, its `CenteredGrid.bounds` `phi.geom.Box` describing the physical size, and its `CenteredGrid.extrapolation` (`phi.math.extrapolation.Extrapolation`).
    
    Centered grids support batch, spatial and channel dimensions.

    See Also:
        `StaggeredGrid`,
        `Grid`,
        `Field`,
        `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:

            * `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
            * `phi.math.Tensor` compatible with grid dims: uses tensor values as grid values
            * Function `values(x)` where `x` is a `phi.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`, `phi.math.Tensor`, `phi.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 `phi.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.
        convert: Whether to convert `values` to the default backend.
    """
    extrapolation = boundary if extrapolation is None else extrapolation
    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
        elements = UniformGrid(resolution, bounds)
    else:
        resolution = _get_resolution(resolution, resolution_, bounds)
        elements = UniformGrid(resolution, bounds)
        if isinstance(values, math.Tensor):
            values = math.expand(values, resolution)
        elif isinstance(values, (Field, FieldInitializer, Geometry)):
            values = sample(values, elements)
        elif callable(values):
            values = sample_function(values, elements, 'center', extrapolation)
        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, convert=convert), resolution)
    if values.dtype.kind not in (float, complex):
        values = math.to_float(values)
    assert resolution.spatial_rank == elements.bounds.spatial_rank, f"Resolution {resolution} does not match bounds {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 == elements.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}"
    return Field(elements, values, extrapolation)

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

Centered grids support batch, spatial and channel dimensions.

See Also: StaggeredGrid(), Field, Field, 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
  • phi.math.Tensor compatible with grid dims: uses tensor values as grid values
  • Function values(x) where x is a phi.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, phi.math.Tensor, phi.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 phi.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.
convert
Whether to convert values to the default backend.
def PointCloud(elements: phiml.math._tensors.Tensor | phi.geom._geom.Geometry | float,
values: Any = 1.0,
extrapolation: phiml.math.extrapolation.Extrapolation | float = 0.0,
bounds: phi.geom._box.Box = None,
variable_attrs=('values', 'geometry'),
value_attrs=('values',)) ‑> phi.field._field.Field
Expand source code
def PointCloud(elements: Union[Tensor, Geometry, float], values: Any = 1., extrapolation: Union[Extrapolation, float] = 0., bounds: Box = None, variable_attrs=('values', 'geometry'), value_attrs=('values',)) -> Field:
    """
    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

    Args:
        elements: `Tensor` or `Geometry` object specifying the sample points and sizes
        values: values corresponding to elements
        extrapolation: values outside elements
        bounds: Deprecated. Has no use since 2.5.
    """
    if bounds is not None:
        warnings.warn("bounds argument is deprecated since 2.5 and will be ignored.", stacklevel=2)
    # if dual(values):
    #     assert dual(values).rank == 1, f"PointCloud cannot convert values with more than 1 dual dimension."
    #     non_dual_name = dual(values).name[1:]
    #     indices = math.stored_indices(values)[non_dual_name]
    #     values = math.stored_values(values)
    #     elements = elements[{non_dual_name: indices}]
    if isinstance(elements, (int, float)) and elements == 0:
        assert 'vector' in shape(values), f"When constructing a PointCloud from the origin 0, values must have a 'vector' dimension"
        elements = values * 0
    if isinstance(elements, Tensor):
        elements = geom.Point(elements)
    result = Field(elements, values, extrapolation, variable_attrs, value_attrs)
    assert result.boundary is PERIODIC or isinstance(result.boundary, ConstantExtrapolation), f"Unsupported extrapolation for PointCloud: {result._boundary}"
    return result

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
bounds
Deprecated. Has no use since 2.5.
def StaggeredGrid(values: Any = 0.0,
boundary: float = 0,
bounds: phi.geom._box.Box = None,
resolution: phiml.math._shape.Shape = None,
extrapolation: float = None,
convert=True,
**resolution_: int) ‑> phi.field._field.Field
Expand source code
def StaggeredGrid(values: Any = 0.,
                  boundary: float or Extrapolation = 0,
                  bounds: Box or float = None,
                  resolution: Shape or int = None,
                  extrapolation: float or Extrapolation = None,
                  convert=True,
                  **resolution_: int or Tensor) -> Field:
    """
    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`,
        `Field`,
        `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:

            * `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
            * `phi.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 `phi.math.stack()` to manually create this non-uniform tensor.
            * Function `values(x)` where `x` is a `phi.math.Tensor` representing the physical location.
                The spatial dimensions of the grid will be passed as batch dimensions to the function.

        boundary: The grid extrapolation determines the value outside the `values` tensor.
            Allowed types: `float`, `phi.math.Tensor`, `phi.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 `phi.math.Shape`.
            If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes.
        convert: Whether to convert `values` to the default backend.
        **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified.
    """
    extrapolation = boundary if extrapolation is None else extrapolation
    extrapolation = as_boundary(extrapolation, UniformGrid)
    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 '~vector' not in values.shape:
                values = unstack_staggered_tensor(values, extrapolation)
            resolution = resolution_from_staggered_tensor(values, extrapolation)
        else:
            resolution = spatial(values)
        bounds = bounds or Box(math.const_vec(0, resolution), math.wrap(resolution, channel('vector')))
        elements = UniformGrid(resolution, bounds)
    else:
        resolution = _get_resolution(resolution, resolution_, bounds)
        elements = UniformGrid(resolution, bounds)
        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 '~vector' not in values.shape:  # legacy behavior: we are given a padded staggered tensor
                    values = unstack_staggered_tensor(values, extrapolation)
                    resolution = resolution_from_staggered_tensor(values, extrapolation)
                    elements = UniformGrid(resolution, bounds)
                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, Field, FieldInitializer)):
            values = sample(values, elements, at='face', boundary=extrapolation, dot_face_normal=elements)
        elif callable(values):
            values = sample_function(values, elements, 'face', extrapolation)
            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 '~vector' in values.shape
            if 'vector' in values.shape:
                values = math.stack([values[{'vector': i, '~vector': i}] for i in range(resolution.rank)], dual(vector=resolution))
        else:
            values = expand_staggered(math.tensor(values, convert=convert), resolution, extrapolation)
    if values.dtype.kind not in (float, complex):
        values = math.to_float(values)
    assert resolution.spatial_rank == elements.bounds.spatial_rank, f"Resolution {resolution} does not match bounds {elements.bounds}"
    if 'vector' in values.shape:
        values = rename_dims(values, 'vector', dual(vector=values.vector.item_names))
    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 == elements.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}"
    return Field(elements, values, extrapolation)

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(), Field, Field, 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
  • phi.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 phi.math.stack() to manually create this non-uniform tensor.
  • Function values(x) where x is a phi.math.Tensor representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function.
boundary
The grid extrapolation determines the value outside the values tensor. Allowed types: float, phi.math.Tensor, phi.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 phi.math.Shape. If bounds is given as a Box, the resolution may be specified as an int to be equal along all axes.
convert
Whether to convert values to the default backend.
**resolution_
Spatial dimensions as keyword arguments. Typically either resolution or spatial_dims are specified.
def abs(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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.

def as_boundary(obj: phiml.math.extrapolation.Extrapolation | phiml.math._tensors.Tensor | float | phi.field._field.Field | None) ‑> phiml.math.extrapolation.Extrapolation
Expand source code
def as_boundary(obj: Union[Extrapolation, Tensor, float, Field, None], _geometry=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`
    """
    return obj.as_boundary() if isinstance(obj, Field) else math.extrapolation.as_extrapolation(obj)

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

def assert_close(*fields: phi.field._field.Field,
rel_tolerance: float = 1e-05,
abs_tolerance: float = 0,
msg: str = '',
verbose: bool = True)
Expand source code
def assert_close(*fields: Field or Tensor or 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 `phi.math.assert_close()`. """
    f0 = next(filter(lambda t: isinstance(t, Field), fields))
    values = [(f @ f0).values if isinstance(f, Field) else math.wrap(f) for f in fields]
    math.assert_close(*values, rel_tolerance=rel_tolerance, abs_tolerance=abs_tolerance, msg=msg, verbose=verbose)

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

def bake_extrapolation(grid: phi.field._field.Field) ‑> phi.field._field.Field
Expand source code
def bake_extrapolation(grid: Field) -> Field:
    """
    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 `phi.math.extrapolation.NONE`.
    """
    if grid.extrapolation == math.extrapolation.NONE:
        return grid
    if grid.is_grid and grid.is_staggered:
        padded = []
        for dim in grid.vector.item_names:
            lower, upper = grid.extrapolation.valid_outer_faces(dim)
            value = grid.vector[dim].values
            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, dual(vector=grid.shape.spatial)), bounds=grid.bounds, extrapolation=math.extrapolation.NONE)
    elif grid.is_grid:
        return pad(grid, 1).with_extrapolation(math.extrapolation.NONE)
    else:
        raise ValueError(f"Not a valid grid: {grid}")

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 phi.math.extrapolation.NONE.

def cast(x: ~MagicType, dtype: phiml.backend._dtype.DType | type) ‑> ~OtherMagicType
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__}'")

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

def ceil(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

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

    Args:
        density: Scalar `Field`

    Returns:
        `Tensor` holding only batch dimensions.
    """
    assert 'vector' not in density.shape
    return mean(density.points * density) / mean(density)

Compute the center of mass of a density field.

Args

density
Scalar Field

Returns

Tensor holding only batch dimensions.

def concat(fields: Sequence[phi.field._field.Field], dim: str) ‑> phi.field._field.Field
Expand source code
def concat(fields: Sequence[Field], dim: str or Shape) -> Field:
    """
    Concatenates the given `Field`s along `dim`.

    See Also:
        `stack()`.

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

    Returns:
        `Field` matching concatenated fields.
    """
    assert all(isinstance(f, Field) 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 fields[0].is_grid:
        values = math.concat([f.values for f in fields], dim)
        return fields[0].with_values(values)
    elif fields[0].is_point_cloud or fields[0].is_graph:
        geometry = geom.concat([f.geometry for f in fields], dim)
        values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim)
        return PointCloud(elements=geometry, values=values, extrapolation=fields[0].extrapolation)
    elif fields[0].is_mesh:
        assert all([f.geometry == fields[0].geometry for f in fields])
        values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim)
        return Field(fields[0].geometry, values, fields[0].extrapolation)
    raise NotImplementedError(type(fields[0]))

Concatenates the given Fields along dim.

See Also: stack().

Args

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

Returns

Field matching concatenated fields.

def convert(x, backend: phiml.backend._backend.Backend = None, use_dlpack=True)
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 tree_map(convert, x, backend=backend, use_dlpack=use_dlpack)
    else:
        return b_convert(x, backend, use_dlpack=use_dlpack)

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.

def cos(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def curl(field: phi.field._field.Field, at='corner')
Expand source code
def curl(field: Field, at='corner'):
    """
    Computes the finite-difference curl of the give 2D `StaggeredGrid`.

    Args:
        field: `Field`
        at: Either `center` or `face`.
    """
    assert 'vector' in field.shape, f"curl requires a vector field but got {field}"
    assert field.spatial_rank in (2, 3), "curl is only defined in 2 and 3 spatial dimensions."
    if field.is_grid and field.is_staggered and field.spatial_rank == 2 and at == 'corner':
        x, y = field.vector.item_names
        values = field.with_boundary(None).values
        vx = math.pad(values.vector.dual[x], {y: (1, 1)}, field.extrapolation[{'vector': y}])
        vy = math.pad(values.vector.dual[y], {x: (1, 1)}, field.extrapolation[{'vector': x}])
        vy_dx = math.spatial_gradient(vy, dims=x, dx=field.dx[x], padding=None, stack_dim=None, difference='forward')
        vx_dy = math.spatial_gradient(vx, dims=y, dx=field.dx[y], padding=None, stack_dim=None, difference='forward')
        curl_val = vy_dx - vx_dy
        corners = UniformGrid(field.resolution + 1, Box(field.bounds.lower - field.dx / 2, field.bounds.upper + field.dx / 2))
        return Field(corners, curl_val, field.boundary.spatial_gradient())
    elif field.is_grid and field.is_centered and field.spatial_rank == 2 and at == 'corner':
        x, y = field.vector.item_names
        values = pad(field, 1).values
        diag_basis = wrap([(1, 1), (1, -1)], channel(diag='pos,neg'), dual(vector=[x, y]))
        diag_comp = diag_basis @ values
        ll = diag_comp[{x: slice(-1), y: slice(-1), 'diag': 'neg'}]
        ul = diag_comp[{x: slice(-1), y: slice(1, None), 'diag': 'pos'}]
        lr = diag_comp[{x: slice(1, None), y: slice(-1), 'diag': 'pos'}]
        ur = diag_comp[{x: slice(1, None), y: slice(1, None), 'diag': 'neg'}]
        curl_val = ll - ul + lr - ur
        corners = UniformGrid(field.resolution + 1, Box(field.bounds.lower - field.dx / 2, field.bounds.upper + field.dx / 2))
        return Field(corners, curl_val, field.boundary.spatial_gradient())
    # if field.is_grid and not field.is_staggered and field.spatial_rank == 2:
    #     if 'vector' not in field.shape and at == 'face':
    #         # 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 at == 'center':
    #         # 2D curl of vector field
    #         x, y = field.resolution.names
    #         vy_dx = math.spatial_gradient(field.values.vector[1], dx=field.dx[0], padding=field.extrapolation, dims=x, stack_dim=None)
    #         vx_dy = math.spatial_gradient(field.values.vector[0], dx=field.dx[1], padding=field.extrapolation, dims=y, stack_dim=None)
    #         c = vy_dx - vx_dy
    #         return field.with_values(c)
    # elif field.is_grid and field.is_staggered and field.spatial_rank == 2:
    #     if at == 'center':
    #         values = bake_extrapolation(field).values
    #         x_padded = math.pad(values.vector['x'], {'y': (1, 1)}, field.extrapolation[{'vector': 'x'}], bounds=field.bounds)
    #         y_padded = math.pad(values.vector['y'], {'x': (1, 1)}, field.extrapolation[{'vector': 'y'}], bounds=field.bounds)
    #         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(), field.bounds)
    raise NotImplementedError("Only 2D curl at corner currently supported")

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

Args

field
Field
at
Either center or face.
def divergence(field: phi.field._field.Field,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: phi.field._field.Field = None,
implicitness: int = None) ‑> CenteredGrid() at 0x7fe3745509a0>
Expand source code
def divergence(field: Field, order=2, implicit: Solve = None, upwind: Field = None,
                     implicitness: int = 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.
        implicitness: specifies the size of the implicit stencil in case an implicit treatment is used
        upwind: For unstructured meshes only. Whether to use upwind interpolation.

    Returns:
        Divergence field as `CenteredGrid`
    """

    if field.is_mesh:
        field = field.at_faces(boundary=NONE, order=order, upwind=upwind)
        div = field.geometry.integrate_flux(field.values, divide_volume=True)
        return Field(field.geometry, div, field.boundary.spatial_gradient())
    if order == 2:
        if field.is_staggered:
            field = bake_extrapolation(field)
            components = []
            for dim in field.shape.spatial.names:
                div_dim = math.spatial_gradient(field.vector[dim].values, field.dx, 'forward', None, dims=dim,
                                                stack_dim=None)
                components.append(div_dim)
            data = math.sum(components, dim='0')
            return CenteredGrid(data, bounds=field.bounds, extrapolation=field.extrapolation.spatial_gradient())
        elif field.is_centered:
            left, right = shift(field, (-1, 1), stack_dim=batch('div_'))
            grad = (right - left) / (field.dx * 2)
            components = [grad.vector[i].div_[i] for i in grad.div_.item_names]
            result = sum(components)
            return result

    else:
        components = [
            spatial_gradient(f, dims=dim, at='center', order=order, implicit=implicit, implicitness=implicitness,
                             stack_dim="sum:b").sum[0] for f, dim in zip(field.vector, field.shape.only(spatial).names)]

    return sum(components)

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.
implicitness
specifies the size of the implicit stencil in case an implicit treatment is used
upwind
For unstructured meshes only. Whether to use upwind interpolation.

Returns

Divergence field as CenteredGrid()

def downsample2x(grid: phi.field._field.Field) ‑> phi.field._field.Field
Expand source code
def downsample2x(grid: Field) -> Field:
    """
    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 grid.is_grid and grid.is_centered:
        values = math.downsample2x(grid.values, grid.extrapolation)
        return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation)
    elif grid.is_grid and grid.is_staggered:
        grid_ = grid.with_boundary(extrapolation.NONE)
        values = {}
        for dim in grid.vector.item_names:
            odd_discarded = grid_.values[{'~vector': dim, dim: slice(None, None, 2)}]
            others_interpolated = math.downsample2x(odd_discarded, grid.extrapolation, dims=grid.shape.spatial.without(dim))
            values[dim] = others_interpolated
        return StaggeredGrid(math.stack(values, dual('vector')), None, grid.bounds).with_extrapolation(grid.extrapolation)
    else:
        raise ValueError(grid)

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

Field of same type as grid.

def exp(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def finite_fill(grid: phi.field._field.Field, distance=1, diagonal=True) ‑> phi.field._field.Field
Expand source code
def finite_fill(grid: Field, distance=1, diagonal=True) -> Field:
    """
    Extrapolates values of `grid` which are marked by nonzero values in `valid` using `phi.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 grid.is_grid and grid.is_centered:
        new_values = math.finite_fill(grid.values, distance=distance, diagonal=diagonal, padding=grid.extrapolation)
        return grid.with_values(new_values)
    elif grid.is_grid and grid.is_staggered:
        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).as_dual()))
    else:
        raise ValueError(grid)

Extrapolates values of grid which are marked by nonzero values in valid using `phi.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.
def floor(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def fourier_laplace(grid: phi.field._field.Field, times=1) ‑> phi.field._field.Field
Expand source code
def fourier_laplace(grid: Field, times=1) -> Field:
    """ See `phi.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)

See phi.math.fourier_laplace()

def fourier_poisson(grid: phi.field._field.Field, times=1) ‑> phi.field._field.Field
Expand source code
def fourier_poisson(grid: Field, times=1) -> Field:
    """ See `phi.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)

See phi.math.fourier_poisson()

def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-05, ignore_mean=False, n=2) ‑> phiml.math._tensors.Tensor
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 = math.sum_(math.fftfreq(x.shape.spatial) ** 2, channel)
        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 value_attributes(x)]
        return sum(losses)
    else:
        raise ValueError(x)

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

def functional_gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
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)

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.

def gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
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)

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.

def imag(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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.

def integrate(field: phi.field._field.Field, region: phi.geom._geom.Geometry, **kwargs) ‑> phiml.math._tensors.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 field.is_grid and not field.is_staggered:
        raise NotImplementedError()
    return sample(field, region, **kwargs) * region.volume

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

def is_finite(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def isfinite(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def jacobian(f: Callable, wrt: str = None, get_output=True) ‑> Callable
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)

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.

def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) ‑> Callable
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)

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.

def jit_compile_linear(f: Callable[[~X], ~Y] = None,
auxiliary_args: str = None,
forget_traces: bool = None) ‑> LinearFunction[X, Y]
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)

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.

def l1_loss(x,
reduce: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function non_batch>) ‑> phiml.math._tensors.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 value_attributes(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)

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
def l2_loss(x,
reduce: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function non_batch>) ‑> phiml.math._tensors.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 value_attributes(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)

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
def laplace(u: phi.field._field.Field,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: phi.field._field.Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
implicitness: int = None,
weights: phiml.math._tensors.Tensor | phi.field._field.Field = None,
upwind: phi.field._field.Field = None,
correct_skew=True) ‑> phi.field._field.Field
Expand source code
def laplace(u: Field,
            axes: DimFilter = spatial,
            gradient: Field = None,
            order=2,
            implicit: math.Solve = None,
            implicitness: int = None,
            weights: Union[Tensor, Field] = None,
            upwind: Field = None,
            correct_skew=True) -> Field:
    """
    Spatial Laplace operator for scalar grid.

    For grids, uses a finite difference scheme specified by `order` and `implicit`.
    For unstructured meshes, the scheme is specified via `order` and `upwind`.

    Args:
        u: n-dimensional grid or mesh.
        axes: The second derivative along these dimensions is summed over
        weights: (Optional) Multiply the axis terms by these factors before summation.
            Must be a `phi.math.Tensor` or `phi.field.Field` with a single channel dimension that lists all laplace axes by name.
        gradient: Only used by FVM at the moment. Approximate gradient of `u`, e.g. ∇u of the previous time step.
            If `None`, approximates the gradient as `(u_neighbor - u_self) / distance`.
        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 (inherited from `phi.field.laplace()`).
            For FVM, the order is used when interpolating `v` and `prev_v` to cell faces if needed.
        implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances.
            Otherwise, an explicit stencil is used.
        implicitness: specifies the size of the implicit stencil in case an implicit treatment is used
        upwind: FVM only. Whether to use upwind interpolation.
        correct_skew: If `True`, adds a correction term for cell skewness. This requires `gradient` to be passed.

    Returns:
        laplacian field as `CenteredGrid`
    """

    if implicitness is None:
        implicitness = 0 if implicit is None else 2
    elif implicitness != 0:
        assert implicit is not None, "for implicit treatment a `Solve` is required"
    axes_names = u.shape.only(axes).names
    if isinstance(weights, Field):
        weights = weights.at(u).values
    if weights is not None:
        if channel(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}"
    # --- Mesh ---
    if u.is_mesh:
        if weights is not None and 'vector' in shape(weights):
            raise NotImplementedError(f"laplace on meshes is not yet supported with vector-valued weights")
        neighbor_val = u.mesh.pad_boundary(u.values, mode=u.boundary)
        nb_distances = u.mesh.neighbor_distances
        connecting_grad = (u.mesh.connectivity * neighbor_val - u.values) / nb_distances  # (T_N - T_P) / d_PN
        if correct_skew and gradient is not None:  # skewness correction
            assert dual(gradient).names == ('~vector',), f"gradient must contain one dual dim '~vector' listing the gradient components but got {gradient.shape}"
            gradient = gradient.at_faces(boundary=NONE, order=order, upwind=upwind).values
            nb_offsets = u.mesh.neighbor_offsets
            n1 = (u.face_normals.vector @ nb_offsets.vector) * nb_offsets / nb_distances ** 2  # (n·d_PN) d_PN / d_PN^2
            n2 = u.face_normals - n1
            ortho_correction = gradient @ n2
            grad = connecting_grad * math.vec_length(n1) + ortho_correction
        else:
            assert not correct_skew, f"FVM skew correction only available when gradient is specified. Pass gradient or set correct_skew=False"
            grad = connecting_grad
        laplace_values = u.mesh.integrate_surface(grad) / u.mesh.volume  # 1/V ∑_f ∇T ν A
        result = weights * laplace_values if weights is not None else laplace_values
        return Field(u.mesh, result, u.boundary - u.boundary)

    # --- Grid ---

    laplace_ext = u.extrapolation.spatial_gradient().spatial_gradient()
    laplace_dims = u.shape.only(axes).names

    if u.vector.exists and (u.is_centered or order > 2):
        fields = [f for f in u.vector]
    else:
        fields = [u]

    result = []
    for f in fields:
        if order == 2:
            result.append(math.map_d2c(math.laplace)(f.values, dx=f.dx, padding=f.extrapolation, dims=axes, weights=weights, padding_kwargs={'bounds': f.bounds}))  # uses ghost cells
        else:
            result_components = [perform_finite_difference_operation(f.values, dim, 2, f.dx.vector[dim], f.extrapolation,
                                                                         laplace_ext, 'center', order, implicit,
                                                                         implicitness) for dim in laplace_dims]
            if weights is not None:
                if channel(weights):
                    result_components = [c * weights[ax] for c, ax in zip(result_components, axes_names)]
                else:
                    result_components = [c * weights for c in result_components]

            result.append(sum(result_components))

    if u.vector.exists and (u.is_centered or order > 2):
        if u.is_staggered:
            result = math.stack(result, dual(vector=u.vector.item_names))
        else:
            result = math.stack(result, channel(vector=u.vector.item_names))
    else:
        result = result[0]

    return u.with_values(result).with_extrapolation(laplace_ext)

Spatial Laplace operator for scalar grid.

For grids, uses a finite difference scheme specified by order and implicit. For unstructured meshes, the scheme is specified via order and upwind.

Args

u
n-dimensional grid or mesh.
axes
The second derivative along these dimensions is summed over
weights
(Optional) Multiply the axis terms by these factors before summation. Must be a phi.math.Tensor or Field with a single channel dimension that lists all laplace axes by name.
gradient
Only used by FVM at the moment. Approximate gradient of u, e.g. ∇u of the previous time step. If None, approximates the gradient as (u_neighbor - u_self) / distance.
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 (inherited from laplace()). For FVM, the order is used when interpolating v and prev_v to cell faces if needed.
implicit
When a Solve object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.
implicitness
specifies the size of the implicit stencil in case an implicit treatment is used
upwind
FVM only. Whether to use upwind interpolation.
correct_skew
If True, adds a correction term for cell skewness. This requires gradient() to be passed.

Returns

laplacian field as CenteredGrid()

def mask(obj: phi.field._field.Field) ‑> phi.field._field.Field
Expand source code
def mask(obj: Field or Geometry) -> Field:
    """
    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, Geometry):
        return Field(obj, 1, 0)
    assert isinstance(obj, Field), f"obj must be a Geometry or Field but got {type(obj)}"
    if obj.is_grid and not obj.is_staggered:
        values = math.cast(obj.values != 0, int)
        return obj.with_values(values)
    elif obj.is_staggered:
        raise NotImplementedError
    else:
        return Field(obj.elements, 1, math.extrapolation.remove_constant_offset(obj.extrapolation))

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

Field type or PointCloud()

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

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

    Returns:
        `Field`
    """
    f1, f2 = _auto_resample(f1, f2)
    return f1.with_values(math.maximum(f1.values, f2.values))

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

Args

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

Returns

Field

def mean(field: phi.field._field.Field, dim=<function <lambda>>) ‑> phiml.math._tensors.Tensor
Expand source code
def mean(field: Field, dim=lambda s: s.non_channel.non_batch) -> Tensor:
    """
    Computes the mean value by reducing all spatial / instance dimensions.

    Args:
        field: `Field`

    Returns:
        `phi.Tensor`
    """
    result = math.mean(field.values, dim=dim)
    if (instance(field.geometry) & spatial(field.geometry)) in result.shape:
        return field.with_values(result)
    return result

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

Args

field
Field

Returns

phi.Tensor

def minimize(f: Callable[[~X], ~Y], solve: phiml.math._optimize.Solve[~X, ~Y]) ‑> ~X
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, cache=True, attr_type=variable_attributes)
    x0_tensors = [to_float(t) for t in x0_tensors]
    backend = choose_backend_t(*x0_tensors, prefer_default=True)
    batch_dims = merge_shapes(*[batch(t) for t in x0_tensors])
    x0_natives = []
    x0_native_shapes = []
    for t in x0_tensors:
        t = cached(t)
        if t.shape.is_uniform:
            x0_natives.append(reshaped_native(t, [batch_dims, t.shape.non_batch]))
            x0_native_shapes.append(t.shape.non_batch)
        else:
            for ut in unstack(t, t.shape.shape.without('dims')):
                x0_natives.append(reshaped_native(ut, [batch_dims, ut.shape.non_batch]))
                x0_native_shapes.append(ut.shape.non_batch)
    x0_flat = backend.concat(x0_natives, -1)

    def unflatten_assemble(x_flat, additional_dims: Shape = EMPTY_SHAPE, convert=True):
        partial_tensors = []
        i = 0
        for x0_native, t_shape in zip(x0_natives, x0_native_shapes):
            vol = backend.staticshape(x0_native)[-1]
            flat_native = x_flat[..., i:i + vol]
            partial_tensor = reshaped_tensor(flat_native, [*additional_dims, batch_dims, t_shape], convert=convert)
            partial_tensors.append(partial_tensor)
            i += vol
        # --- assemble non-uniform tensors ---
        x_tensors = []
        for t in x0_tensors:
            if t.shape.is_uniform:
                x_tensors.append(partial_tensors.pop(0))
            else:
                stack_dims = t.shape.shape.without('dims')
                x_tensors.append(stack(partial_tensors[:stack_dims.volume], stack_dims))
                partial_tensors = partial_tensors[stack_dims.volume:]
        x = assemble_tree(x0_nest, x_tensors, attr_type=variable_attributes)
        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, cache=False)
        loss_tensor = y_tensors[0]
        assert not non_batch(loss_tensor), f"Failed to minimize '{f.__name__}' because it returned a non-scalar output {shape(loss_tensor)}. Reduce all non-batch dimensions, e.g. using math.l2_loss()"
        extra_batch = loss_tensor.shape.without(batch_dims)
        if extra_batch:  # output added more batch dims. We should expand the initial guess
            if extra_batch.volume > 1:
                raise NewBatchDims(loss_tensor.shape, extra_batch)
            else:
                loss_tensor = loss_tensor[next(iter(extra_batch.meshgrid()))]
        loss_native = reshaped_native(loss_tensor, [batch_dims], force_expand=False)
        return loss_tensor.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()
    try:
        ret = backend.minimize(solve.method, native_function, x0_flat, atol, maxi, trj)
    except NewBatchDims as new_dims:  # try again with expanded initial guess
        warnings.warn(f"Function returned objective value with dims {new_dims.output_shape} but initial guess was missing {new_dims.missing}. Trying again with expanded initial guess.", RuntimeWarning, stacklevel=2)
        x0 = expand(solve.x0, new_dims.missing)
        solve = copy_with(solve, x0=x0)
        return minimize(f, solve)
    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

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.
def minimum(f1: phi.field._field.Field, f2: phi.field._field.Field)
Expand source code
def minimum(f1: Field or Geometry or float, f2: Field or Geometry or float):
    """
    Element-wise minimum.
    One of the given fields needs to be an instance of `Field` and the the result will be sampled at the corresponding points.
    If both are `Fields` but have different points, `f1` takes priority.

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

    Returns:
        `Field`
    """
    f1, f2 = _auto_resample(f1, f2)
    return f1.with_values(math.minimum(f1.values, f2.values))

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

Args

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

Returns

Field

def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) ‑> phiml.math._tensors.Tensor | phi.field._field.Field
Expand source code
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) -> Union[Field, Tensor]:
    """
    Similar to `phi.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: `Field` or `phi.Tensor` instances.
        extrapolation: (Optional) Extrapolation of the output field. If `None`, uses the extrapolation of the first input field.

    Returns:
        `Field` matching the first `Field` in `inputs`.
    """
    input_tensors = [i.uniform_values() if isinstance(i, Field) 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, Field):
            if not i.values.shape.is_uniform:
                values = unstack_staggered_tensor(values, i.boundary)
            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 Field.")

Similar to phi.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
Field or phi.Tensor instances.
extrapolation
(Optional) Extrapolation of the output field. If None, uses the extrapolation of the first input field.

Returns

Field matching the first Field in inputs.

def normalize(field: phi.field._field.Field, norm: phi.field._field.Field, epsilon=1e-05)
Expand source code
def normalize(field: Field, norm: Field, 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)

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

def pack_dims(field: phi.field._field.Field,
dims: phiml.math._shape.Shape,
packed_dim: phiml.math._shape.Shape,
pos: int = None) ‑> phi.field._field.Field
Expand source code
def pack_dims(field: Field,
              dims: Shape or tuple or list or str,
              packed_dim: Shape,
              pos: int or None = None) -> Field:
    """
    Currently only supports grids and non-spatial dimensions.

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

    Args:
        field: `Field`

    Returns:
        `Field` of same type as `field`.
    """
    if isinstance(field, Field):
        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()

Currently only supports grids and non-spatial dimensions.

See Also: phi.math.pack_dims().

Args

field
Field

Returns

Field of same type as field.

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

    Unlike `phi.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 grid.is_grid:
        if grid.is_staggered:
            data = math.pad(grid.values.vector.dual.as_channel(), widths, grid.extrapolation, bounds=grid.bounds).vector.as_dual()
        else:
            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.bounds.lower - w_lower * grid.dx, grid.bounds.upper + w_upper * grid.dx)
        return create_similar_grid(grid, data, grid.extrapolation, bounds)
    raise NotImplementedError(f"{type(grid)} not supported. Only Grid instances allowed.")

Pads a Field using its extrapolation.

Unlike phi.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

Field of the same type as grid

def read(file: str | phiml.math._tensors.Tensor, convert_to_backend=True) ‑> phi.field._field.Field
Expand source code
def read(file: Union[str, Tensor], convert_to_backend=True) -> Field:
    """
    Loads a previously saved `Field` 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 `Field`.
    """
    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 = math.unstack(file, dim.name)
            fields = [read(file_, convert_to_backend=convert_to_backend) for file_ in files]
            return stack(fields, dim)
    else:
        raise ValueError(file)

Loads a previously saved Field 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 Field.

def real(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

See Also: imag(), conjugate().

Args

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

Returns

Real component of x.

def reduce_sample(field: phi.field._field.Field | phi.geom._geom.Geometry | phi.field._field.FieldInitializer | Callable,
geometry: phi.geom._geom.Geometry,
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def reduce_sample(field: Union[Field, Geometry, FieldInitializer, Callable],
                  geometry: Geometry or Field or Tensor,
                  **kwargs) -> math.Tensor:
    """Alias for `sample()` with `dot_face_normal=field.geometry`."""
    can_reduce = dual(field.values) in geometry.shape
    at = 'face' if dual(geometry) else 'center'
    return sample(field, geometry, at, field.boundary, dot_face_normal=field.geometry if can_reduce else None, **kwargs)

Alias for sample() with dot_face_normal=field.geometry.

def resample(value: phi.field._field.Field | phi.geom._geom.Geometry | phiml.math._tensors.Tensor | float | phi.field._field.FieldInitializer,
to: phi.field._field.Field | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs)
Expand source code
def resample(value: Union[Field, Geometry, Tensor, float, FieldInitializer], to: Union[Field, Geometry], keep_boundary=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: `Field` (`CenteredGrid`, `StaggeredGrid` or `PointCloud`) object defining the sample points.
            The current values of `to` are ignored.
        keep_boundary: Only available if `self` is a `Field`.
            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
    """
    assert isinstance(to, (Field, Geometry)), f"'to' must be a Field or Geomoetry but got {to}"
    if not isinstance(value, (Field, Geometry, FieldInitializer)):
        return to.with_values(value)
    if isinstance(value, Field) and keep_boundary:
        extrap = value.extrapolation
    elif isinstance(to, Field) and not keep_boundary:
        extrap = to.extrapolation
    else:
        raise AssertionError(f"Boundary cannot be determined, keep_boundary={keep_boundary}, value: {type(value)}, to: {type(to)}")
    resampled = sample(value, to, at=to.sampled_at if isinstance(to, Field) else 'center', boundary=extrap, dot_face_normal=to.geometry, **kwargs)
    return Field(to.geometry if isinstance(to, Field) else to, resampled, extrap)

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
Field (CenteredGrid(), StaggeredGrid() or PointCloud()) object defining the sample points. The current values of to are ignored.
keep_boundary
Only available if self is a Field. 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
def round(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def safe_mul(x, y)
Expand source code
def safe_mul(x, y):
    """See `phiml.math.safe_mul()`"""
    x, y = _auto_resample(x, y)
    values = math.safe_mul(x.values, y.values)
    return x.with_values(values)

See phiml.math.safe_mul()

def sample(field: phi.field._field.Field | phi.geom._geom.Geometry | phi.field._field.FieldInitializer | Callable,
geometry: phi.geom._geom.Geometry,
at: str = 'center',
boundary: phiml.math.extrapolation.Extrapolation | phiml.math._tensors.Tensor | numbers.Number = None,
dot_face_normal: phi.geom._geom.Geometry | None = None,
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def sample(field: Union[Field, Geometry, FieldInitializer, Callable],
           geometry: Geometry or Field or Tensor,
           at: str = 'center',
           boundary: Union[Extrapolation, Tensor, Number] = None,
           dot_face_normal: Optional[Geometry] = None,
           **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:
        `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 `Field` or location `Tensor`.
            When passing a `Field`, its `elements` are used as sample points.
            When passing a vector-valued `Tensor`, a `Point` geometry will be created.
        at: One of 'center', 'face', 'vertex'
        boundary: Target extrapolation.
        dot_face_normal: If not `None` and , `field` is a vector field and `at=='face'`, the dot product between sampled field vectors and the face normals is returned instead.
        **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 `phi.math.Tensor`
    """
    # --- Process args ---
    assert at in ['center', 'face', 'vertex']
    if at == 'face':
        assert boundary is not None, "boundaries must be given when sampling at faces"
    geometry = _get_geometry(geometry)
    boundary = as_boundary(boundary, geometry) if boundary is not None else None
    if dot_face_normal is True:
        dot_face_normal = geometry
    if isinstance(field, Geometry):
        from ._field_math import mask
        field = mask(field)
    if isinstance(field, FieldInitializer):
        values = field._sample(geometry, at, boundary, **kwargs)
        if dot_face_normal is not None and at == 'face' and channel(values):
            if _are_axis_aligned(dot_face_normal.face_normals):
                values = math.stack([values[{'vector': i, '~vector': i}] for i in range(geometry.spatial_rank)], dual(**geometry.shape['vector'].untyped_dict))
            else:
                raise NotImplementedError
        field = Field(geometry, values, boundary)
    if callable(field):
        values = sample_function(field, geometry, at, boundary)
        field = Field(geometry, values, boundary)
    # --- Resample ---
    assert isinstance(field, Field), f"field must be a Field, Geometry or initializer but got {type(field)}"
    if at == 'center':
        if field.is_centered and field.sampled_elements.shallow_equals(geometry):
            return field.values
        if field.is_grid and not field.is_staggered:
            return sample_grid_at_centers(field, geometry, **kwargs)
        elif field.is_grid and field.is_staggered:
            return sample_staggered_grid(field, geometry, **kwargs)
        elif field.is_mesh and field.is_staggered:
            return math.finite_mean(field.values, dual)  # ToDo weigh by face areas?
        elif field.is_mesh:
            return sample_mesh(field, geometry.center, **kwargs)
        else:
            return scatter_to_centers(field, geometry, **kwargs)
    elif at == 'face':
        if field.is_staggered and field.geometry.shallow_equals(geometry) and field.geometry.face_shape == geometry.face_shape and field.geometry.shallow_equals(dot_face_normal):
            return field.values
        elif dot_face_normal is not None and channel(field):
            if _are_axis_aligned(dot_face_normal.face_normals):
                components = unstack(field, field.shape.channel.name)
                faces = math.unstack(slice_off_constant_faces(geometry.faces, geometry.boundary_faces, boundary), dual)
                sampled = [sample(c, p, **kwargs) for c, p in zip(components, faces)]
                return math.stack(sampled, dual(dot_face_normal.face_shape))
            else:
                raise NotImplementedError
        elif field.is_grid and field.is_centered:
            return sample_grid_at_faces(field, geometry, boundary, **kwargs)
        elif field.is_grid and field.is_staggered:
            faces = math.unstack(slice_off_constant_faces(geometry.faces, geometry.boundary_faces, boundary), dual)
            sampled = [sample(field, face, **kwargs) for face in faces]
            return math.stack(sampled, dual(geometry.face_shape))
        elif field.is_mesh and field.is_centered and field.geometry.shallow_equals(geometry):
            return centroid_to_faces(field, boundary, **kwargs)
        elif field.is_mesh and field.is_staggered and field.geometry.shallow_equals(geometry):
            return field.with_boundary(boundary)
        else:
            return scatter_to_faces(field, geometry, boundary, **kwargs)
        # geom_ch = channel(geometry).without('vector')
        # assert all(dim not in field.shape for dim in geom_ch)
        # if geom_ch:
        #     sampled = [field._sample(p, **kwargs) for p in geometry.unstack(geom_ch.name)]
        #     return math.stack(sampled, geom_ch)
    elif at == 'vertex':
        raise NotImplementedError

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: Field.at(), Resampling overview.

Args

field
Source Field to sample.
geometry
Single or batched Geometry or Field or location Tensor. When passing a Field, its elements are used as sample points. When passing a vector-valued Tensor, a Point geometry will be created.
at
One of 'center', 'face', 'vertex'
boundary
Target extrapolation.
dot_face_normal
If not None and , field is a vector field and at=='face', the dot product between sampled field vectors and the face normals is returned instead.
**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 phi.math.Tensor

def shift(grid: phi.field._field.Field,
offsets: tuple,
stack_dim: phiml.math._shape.Shape | None = (shiftᶜ=None),
dims=<function spatial>,
pad=True)
Expand source code
def shift(grid: Field, 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.bounds.lower - w_lower * grid.dx, grid.bounds.upper - w_upper * grid.dx)
    data = math.shift(grid.values, offsets, dims=dims, padding=padding, stack_dim=stack_dim)
    return [create_similar_grid(grid, data[i], grid.extrapolation, new_bounds) for i in range(len(offsets))]

Wraps :func:math.shift for CenteredGrid.

Args

grid
CenteredGrid:
offsets
tuple:
stack_dim
(Default value = 'shift')
def sign(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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.

def sin(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def solve_linear(f: 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
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, both in eager execution and JIT mode.

    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.

    **Caution:** The matrix construction may potentially be performed each time `solve_linear` is called if auxiliary arguments change.
    To prevent this, jit-compile the function that makes the call to `solve_linear`.

    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.
    """
    assert solve.x0 is not None, "Please specify the initial guess as Solve(..., x0=initial_guess)"
    # --- 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, cache=False, attr_type=value_attributes)
    x0_tree, x0_tensors = disassemble_tree(solve.x0, cache=False, attr_type=variable_attributes)
    assert len(x0_tensors) == len(y_tensors) == 1, "Only single-tensor linear solves are currently supported"
    if isinstance(y_tree, str) and y_tree == NATIVE_TENSOR and isinstance(x0_tree, str) and x0_tree == NATIVE_TENSOR:
        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"
            if rank == 0:
                y = wrap(y)
                x0 = wrap(solve.x0)
            else:
                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):
            x0 = math.convert(solve.x0, backend)
            matrix, bias = f.sparse_matrix_and_bias(x0, *f_args, **f_kwargs)
        else:
            matrix = f
            bias = 0
        m_rank = _stored_matrix_rank(matrix)
        if solve.rank_deficiency is None:
            if m_rank is not None:
                estimated_deficiency = dual(matrix).volume - m_rank
                if (estimated_deficiency > 0).any:
                    warnings.warn("Possible rank deficiency detected. Matrix might be singular which can lead to convergence problems. Please specify using Solve(rank_deficiency=...).")
                solve = copy_with(solve, rank_deficiency=0)
            else:
                solve = copy_with(solve, rank_deficiency=0)  # no info or user input, assume not rank-deficient
        preconditioner = compute_preconditioner(solve.preconditioner, matrix, rank_deficiency=solve.rank_deficiency, 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, cache=False, attr_type=value_attributes)
            x0_nest, (x0_tensor,) = disassemble_tree(solve.x0, cache=False, attr_type=variable_attributes)
            # 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)], attr_type=variable_attributes)
                y_ = f(x, *f_args, **f_kwargs)
                _, (y_tensor_,) = disassemble_tree(y_, cache=False, attr_type=value_attributes)
                assert set(non_batch(y_tensor_)) == set(non_batch(y_tensor)), f"Function returned dimensions {y_tensor_.shape} but right-hand-side has shape {y_tensor.shape}"
                y_native = reshaped_native(y_tensor_, [batches, non_batch(y_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(y_tensor)])  # order like right-hand-side
                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)

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, both in eager execution and JIT mode.

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.

Caution: The matrix construction may potentially be performed each time solve_linear() is called if auxiliary arguments change. To prevent this, jit-compile the function that makes the call to solve_linear().

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.
def solve_nonlinear(f: Callable, y, solve: phiml.math._optimize.Solve) ‑> phiml.math._tensors.Tensor
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)

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 spatial_gradient(field: phi.field._field.Field,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
implicitness: int = None,
scheme=None,
upwind: phi.field._field.Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)
Expand source code
def spatial_gradient(field: Field,
                     boundary: Extrapolation = None,
                     at: str = 'center',
                     dims: math.DimFilter = spatial,
                     stack_dim: Union[Shape, str] = channel('vector'),
                     order=2,
                     implicit: Solve = None,
                     implicitness: int = None,
                     scheme=None,
                     upwind: Field = None,
                     gradient_extrapolation: Extrapolation = 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)
        boundary: Boundary conditions of the gradient field.
        at: Either `'face'` or `'center'`
        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.
        implicitness: specifies the size of the implicit stencil in case an implicit treatment is used
        gradient_extrapolation: Alias for `boundary`.
        scheme: For unstructured meshes only. Currently only `'green-gauss'` is supported.
        upwind: For unstructured meshes only. Whether to use upwind interpolation.

    Returns:
        spatial_gradient field of type `type`.
    """

    assert at in ['face', 'center']
    stack_dim = auto(stack_dim)
    if gradient_extrapolation is not None:
        assert boundary is None, f"Cannot specify both boundary and gradient_extrapolation"
        boundary = gradient_extrapolation
    if boundary is None:
        boundary = field.extrapolation.spatial_gradient()
    if gradient_extrapolation is None:
        gradient_extrapolation = boundary
    if field.is_mesh and at == 'center':
        assert stack_dim not in field.shape, f"Gradient dimension is already part of field {field.shape}. Please use a different dimension"
        boundary = boundary or field.boundary.spatial_gradient()
        if scheme == 'green-gauss':
            return green_gauss_gradient(field, stack_dim=stack_dim, boundary=boundary, order=order, upwind=upwind)
        elif scheme == 'least-squares':
            return least_squares_gradient(field, stack_dim=stack_dim, boundary=boundary)
        raise NotImplementedError(scheme)


    if field.vector.exists:
        assert stack_dim.name != 'vector', "`stack_dim=vector` is inadmissible if the input is a vector grid"
        if field == StaggeredGrid:
            assert at == 'faces', "for a `StaggeredGrid` input only `type == StaggeredGrid` is possible"

    if at == 'faces':
        assert stack_dim.name == 'vector', f"spatial_gradient with type=StaggeredGrid requires stack_dim.name == 'vector' but got '{stack_dim.name}'"



    if gradient_extrapolation is None:
        gradient_extrapolation = field.extrapolation.spatial_gradient()

    if implicitness is None:
        implicitness = 0 if implicit is None else 2
    elif implicitness != 0:
        assert implicit is not None, "for implicit treatment a `Solve` is required"

    grad_dims = field.shape.only(dims).names

    if stack_dim is None:
        assert len(grad_dims) == 1, "`stack_dim` `None` is only possible with single `grad_dim`"
    else:
        stack_dim = stack_dim.with_size(grad_dims)

    if order == 2:
        if at == 'center':
            values = math.spatial_gradient(field.values, field.dx.vector.as_channel(name=stack_dim.name),
                                           difference='central', padding=field.extrapolation, stack_dim=stack_dim)
            return CenteredGrid(values, bounds=field.bounds, extrapolation=gradient_extrapolation)
        elif at == 'face':
            assert stack_dim.name == 'vector'
            return stagger(field, lambda lower, upper: (upper - lower) / field.dx.vector.as_dual(), gradient_extrapolation)

    result_components = [
        perform_finite_difference_operation(field.values, dim, 1, field.dx.vector[dim], field.extrapolation,
                                            gradient_extrapolation, at, order, implicit, implicitness)
        for dim in field.shape.only(grad_dims).names]

    if at == 'center':
        result = field.with_values(math.stack(result_components, stack_dim))
        result = result.with_extrapolation(gradient_extrapolation)
    else:
        result = StaggeredGrid(
            math.stack(result_components, stack_dim.as_dual()),
            bounds=field.bounds, extrapolation=gradient_extrapolation)

    if at == 'center' 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

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)
boundary
Boundary conditions of the gradient field.
at
Either 'face' or 'center'
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.
implicitness
specifies the size of the implicit stencil in case an implicit treatment is used
gradient_extrapolation
Alias for boundary.
scheme
For unstructured meshes only. Currently only 'green-gauss' is supported.
upwind
For unstructured meshes only. Whether to use upwind interpolation.

Returns

spatial_gradient field of type type.

def sqrt(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def stack(fields: Sequence[phi.field._field.Field],
dim: phiml.math._shape.Shape,
dim_bounds: phi.geom._box.Box = None)
Expand source code
def stack(fields: Sequence[Field], dim: Shape, dim_bounds: Box = None):
    """
    Stacks the given `Field`s along `dim`.

    See Also:
        `concat()`.

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

    Returns:
        `Field` matching stacked fields.
    """
    assert all(isinstance(f, Field) for f in fields), f"All fields must be Fields of the same type but got {fields}"
    assert all(isinstance(f, type(fields[0])) for f in fields), f"All fields must be Fields of the same type but got {fields}"
    if any([f.sampled_at != fields[0].sampled_at for f in fields]):
        return math.layout(fields, dim)
    if any(f.boundary != fields[0].boundary for f in fields):
        boundary = math.stack([f.boundary for f in fields], dim)
    else:
        boundary = fields[0].boundary
    if fields[0].is_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 grid(values, boundary, fields[0].bounds * dim_bounds)
        else:
            return fields[0].with_values(values).with_boundary(boundary)
    else:
        values = math.stack([f.values for f in fields], dim)
        geometry = fields[0].geometry if all(f.geometry == fields[0].geometry for f in fields) else math.stack([f.geometry for f in fields], dim)
        return Field(geometry, values, boundary)

Stacks the given Fields along dim.

See Also: concat().

Args

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

Returns

Field matching stacked fields.

def stagger(field: phi.field._field.Field,
face_function: Callable,
boundary: float,
at='face',
dims=<function spatial>)
Expand source code
def stagger(field: Field,
            face_function: Callable,
            boundary: float or math.extrapolation.Extrapolation,
            at='face',
            dims=spatial):
    """
    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: Grid
        face_function: function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
        boundary: extrapolation mode of the returned grid. Has no effect on the values.
        at: Where the result should be sampled, one of 'face', 'center'
        dims: Which dimensions to stagger. Defaults to all spatial axes.

    Returns:
        Grid sampled either at centers or faces depending on `at`.
    """
    boundary = as_boundary(boundary, field.geometry)
    all_lower = []
    all_upper = []
    dims = field.shape.only(dims, reorder=True).names
    if at == 'face':
        for dim in dims:
            valid_lo, valid_up = boundary.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)}
            comp = field.values.vector[dim] if 'vector' in channel(field) else field.values
            all_lower.append(math.pad(comp, width_lower, field.extrapolation, bounds=field.bounds))
            all_upper.append(math.pad(comp, width_upper, field.extrapolation, bounds=field.bounds))
        all_upper = math.stack(all_upper, dual(vector=dims))
        all_lower = math.stack(all_lower, dual(vector=dims))
        values = face_function(all_lower, all_upper)
        result = Field(field.geometry, values, boundary)
        assert result.resolution == field.resolution
        return result
    else:
        assert at == 'center', f"type must be 'face' or 'center' but got '{at}'"
        left, right = math.shift(field.values, (-1, 1), dims=dims, padding=field.extrapolation, stack_dim=channel('vector'))
        values = face_function(left, right)
        return CenteredGrid(values, boundary, field.bounds)

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
Grid
face_function
function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
boundary
extrapolation mode of the returned grid. Has no effect on the values.
at
Where the result should be sampled, one of 'face', 'center'
dims
Which dimensions to stagger. Defaults to all spatial axes.

Returns

Grid sampled either at centers or faces depending on at.

def stop_gradient(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, Shape):
        return x
    return _backend_op1(x, Backend.stop_gradient, attr_type=variable_attributes)

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.

def support(field: phi.field._field.Field,
list_dim: phiml.math._shape.Shape = (nonzeroⁱ=None)) ‑> phiml.math._tensors.Tensor
Expand source code
def support(field: Field, list_dim: Shape or str = instance('nonzero')) -> Tensor:
    """
    Returns the points at which the field values are non-zero.

    Args:
        field: `Field`
        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)]

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

Args

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

Returns

Tensor with shape (list_dim, vector)

def to_float(x: ~TensorOrTree) ‑> ~TensorOrTree
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 documentation at https://tum-pbs.github.io/PhiML/Data_Types.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)

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 documentation at https://tum-pbs.github.io/PhiML/Data_Types.html

See Also: cast().

Args

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

Returns

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

def to_int32(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def to_int64(x: ~TensorOrTree) ‑> ~TensorOrTree
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)

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

def unstack(value, dim: str | Sequence | set | ForwardRef('Shape') | Callable | None) ‑> tuple
Expand source code
def unstack(value, dim: DimFilter) -> tuple:
    """
    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`.
    If no dimension is given or none of the given dimensions exists on `value`, returns a list containing only `value`.

    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 objects matching the type of `value`.

    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)
    if dims.rank == 0:
        return value,
    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, ())

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. If no dimension is given or none of the given dimensions exists on value, returns a list containing only value.

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 objects matching the type of value.

Examples

>>> unstack(expand(0, spatial(x=5)), 'x')
(0.0, 0.0, 0.0, 0.0, 0.0)
def upsample2x(grid: phi.field._field.Field) ‑> phi.field._field.Field
Expand source code
def upsample2x(grid: Field) -> Field:
    """
    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`.
    """
    assert grid.is_grid, f"upsample2x only supported for grids but got {grid}"
    if grid.is_centered:
        values = math.upsample2x(grid.values, grid.extrapolation)
        return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation)
    elif grid.is_staggered:
        raise NotImplementedError()
    else:
        raise ValueError(type(grid))

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

Field of same type as grid.

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

See phi.math.vec_abs()

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

See phi.math.vec_abs()

def vec_squared(field: phi.field._field.Field)
Expand source code
def vec_squared(field: Field):
    """ See `phi.math.vec_squared()` """
    if field.is_grid and field.is_staggered:
        field = field.at_centers()
    return field.with_values(math.vec_squared(field.values))

See phi.math.vec_squared()

def where(mask: phi.field._field.Field,
field_true: phi.field._field.Field,
field_false: phi.field._field.Field) ‑> phi.field._field.Field
Expand source code
def where(mask: Field or Geometry or float, field_true: Field or float, field_false: Field or float) -> Field:
    """
    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 `Field`.

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

    Returns:
        `Field`
    """
    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)

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

Args

mask
Field or Geometry object.
field_true
Field
field_false
Field

Returns

Field

def write(field: phi.field._field.Field, file: str | phiml.math._tensors.Tensor)
Expand source code
def write(field: Field, 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 = math.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)

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.

Classes

class AngularVelocity (location: phiml.math._tensors.Tensor | tuple | list | numbers.Number,
strength: phiml.math._tensors.Tensor | numbers.Number = 1.0,
falloff: Callable = None)
Expand source code
class AngularVelocity(FieldInitializer):
    """
    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):
        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
        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, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor:
        points = get_sample_points(geometry, at, boundaries)
        distances = points - self.location
        strength = self.strength if self.falloff is None else self.strength * self.falloff(distances)
        velocity = cross(strength, distances)
        velocity = math.sum(velocity, self.location.shape.batch.without(points.shape))
        return velocity

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.

Ancestors

  • phi.field._field.FieldInitializer
class Field (geometry: phi.geom._geom.Geometry,
values: phiml.math._tensors.Tensor,
boundary: phiml.math.extrapolation.Extrapolation = 0.0,
variable_attrs: Tuple[str, ...] = ('values',),
value_attrs: Tuple[str, ...] = ('values',))
Expand source code
@dataclass(frozen=True)
class Field(metaclass=_FieldType):
    """
    A `Field` represents a discretized physical quantity (like temperature field or velocity field).
    The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`.
    The boundary conditions and values outside the geometry are determined by `boundary`.

    Examples:
        Create a periodic 2D grid, initialized via noise fluctuations.
        >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

        Create a field on an unstructured mesh loaded from a .gmsh file
        >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
        >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

        Create two cubes and compute a scalar values for each.
        >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

    geometry: Geometry
    """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """
    values: Tensor
    """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`."""
    boundary: Extrapolation = 0.
    """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """

    variable_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`"""
    value_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`"""

    def __post_init__(self):
        math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel)  # shape check

    @property
    def grid(self) -> UniformGrid:
        """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
        assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
        return self.geometry

    @property
    def mesh(self) -> Mesh:
        """Cast `self.geometry` to a `phi.geom.Mesh`."""
        assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def graph(self) -> Graph:
        """Cast `self.geometry` to a `phi.geom.Graph`."""
        assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def faces(self):
        return get_faces(self.geometry, self.boundary)

    @property
    def face_centers(self):
        return self.geometry.face_centers
        # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)

    @property
    def face_normals(self):
        return self.geometry.face_normals
        # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)

    @property
    def face_areas(self):
        return self.geometry.face_areas
        # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)

    @property
    def sampled_elements(self) -> Geometry:
        """
        If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
        If the values are sampled at the faces, returns `self.faces`.
        """
        return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

    @property
    def elements(self):
        # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
        warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
        return self.geometry

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

    @property
    def is_staggered(self):
        return is_staggered(self.values, self.geometry)

    @property
    def center(self) -> Tensor:
        """ Returns the center points of the `elements` of this `Field`. """
        all_points = self.geometry.get_points(self.sampled_at)
        boundary = self.geometry.get_boundary(self.sampled_at)
        return slice_off_constant_faces(all_points, boundary, self.extrapolation)

    @property
    def points(self):
        return self.center

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

    def numpy(self, order: DimFilter = None):
        """
        Return the field values as `NumPy` array(s).

        Args:
            order: Dimension order as `str` or `Shape`.

        Returns:
            A single NumPy array for uniform values, else a list of NumPy arrays.
        """
        if order is None and self.is_grid:
            axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
            order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
        if self.values.shape.is_uniform:
            return self.values.numpy(order)
        else:
            assert order is not None, f"order must be specified for non-uniform Field values"
            order = self.values.shape.only(order, reorder=True)
            stack_dims = order.non_uniform_shape
            inner_order = order.without(stack_dims)
            return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

    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()`.
        """
        if self.values.shape.is_uniform:
            return self.values
        else:
            return self.staggered_tensor()

    @property
    def extrapolation(self) -> Extrapolation:
        """ Returns the `Extrapolation` of this `Field`. """
        return self.boundary

    @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
        """
        if self.is_grid and '~vector' in self.values.shape:
            return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
        set_shape = self.geometry.sets[self.sampled_at]
        return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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

    @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`.
        """
        return self.geometry.spatial_rank

    @property
    def bounds(self) -> BaseBox:
        """
        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`.
        """
        if isinstance(self.geometry.bounds, BaseBox):
            return self.geometry.bounds
        extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
        points = self.geometry.center + extent
        lower = math.min(points, dim=points.shape.non_batch.non_channel)
        upper = math.max(points, dim=points.shape.non_batch.non_channel)
        return Box(lower, upper)

    box = bounds

    @property
    def is_grid(self):
        """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
        return isinstance(self.geometry, UniformGrid)

    @property
    def is_mesh(self):
        """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
        return isinstance(self.geometry, Mesh)

    @property
    def is_graph(self):
        """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
        return isinstance(self.geometry, Graph)

    @property
    def is_point_cloud(self):
        """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
        if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
            return False
        if isinstance(self.geometry, (BaseBox, Sphere, Point)):
            return True
        return True

    @property
    def dx(self) -> Tensor:
        assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
        return self.bounds.size / self.resolution

    @property
    def cells(self):
        assert isinstance(self.geometry, (UniformGrid, Mesh))
        return self.geometry

    def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
        resolution = resolution.spatial & spatial(**resolution_)
        if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
            return self
        bounds = self.bounds if bounds is None else bounds
        if not resolution:
            half_sizes = self.geometry.bounding_half_extent()
            if (half_sizes > 0).all:
                size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
            else:
                cell_count = non_batch(self.geometry).non_channel.non_dual.volume
                size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
            res = math.maximum(1, math.round(bounds.size / size))
            resolution = spatial(**res.vector)
        return Field(UniformGrid(resolution, bounds), self, self.boundary)

    def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud.
        This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_spheres()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Point` geometry.
        """
        points = self.sampled_elements.center
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
        return Field(Point(points), values, self.boundary)

    def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
        This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_points()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Sphere` geometry.
        """
        points = self.sampled_elements.center
        volumes = self.sampled_elements.volume
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
            volumes = pack_dims(volumes, dims, list_dim)
        return Field(Sphere(points, volume=volumes), values, self.boundary)

    def at_centers(self, **kwargs) -> 'Field':
        """
        Interpolates the values to the cell centers.

        See Also:
            `Field.at_faces()`, `Field.at()`, `resample`.

        Args:
            **kwargs: Sampling arguments.

        Returns:
            `CenteredGrid` sampled at cell centers.
        """
        if self.is_centered:
            return self
        from ._resample import sample
        values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
        return Field(self.geometry, values, self.boundary)

    def at_faces(self, boundary=None, **kwargs) -> 'Field':
        if self.is_staggered and not boundary:
            return self
        boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
        from ._resample import sample
        values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
        return Field(self.geometry, values, boundary)

    @property
    def sampled_at(self):
        matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
        return matching_sets[-1]

    def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
        """
        Short for `resample(self, representation)`

        See Also
            `resample()`.

        Returns:
            Field object of same type as `representation`
        """
        from ._resample import resample
        return resample(self, representation, keep_boundary, **kwargs)

    def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
        """
        Sample the values of this `Field` at the given location or geometry.

        Args:
            where: Location `Tensor` or `Geometry` or
            at: `'center'` or `'face'`.
            **kwargs: Sampling arguments.

        Returns:
            `Tensor`
        """
        from ._resample import sample
        return sample(self, where, at, **kwargs)

    def closest_values(self, points: Tensor):
        """
        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.
        """
        warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
        if isinstance(points, Geometry):
            points = points.center
        # --- CenteredGrid ---
        local_points = self.box.global_to_local(points) * self.resolution - 0.5
        return math.closest_grid_values(self.values, local_points, self.extrapolation)
        # --- StaggeredGrid ---
        if 'staggered_direction' in points.shape:
            points_ = math.unstack(points, '~vector')
            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 with_values(self, values, **sampling_kwargs):
        """ Returns a copy of this field with `values` replaced. """
        if not isinstance(values, (Tensor, Number)):
            from ._resample import sample
            values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
        else:
            if not spatial(values):
                geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
                if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                    values = values.vector.as_dual()
                values = expand(wrap(values), geo_shape.non_batch.non_channel)
        return Field(self.geometry, values, self.boundary)

    def with_boundary(self, boundary):
        """ Returns a copy of this field with the `boundary` replaced. """
        boundary = as_boundary(boundary, self.geometry)
        boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
        old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
        new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
        if old_determined_slices.values() == new_determined_slices.values():
            return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
        to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
        to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
        values = math.slice_off(self.values, *to_remove)
        if to_add:
            if self.is_mesh:
                values = self.mesh.pad_boundary(values, to_add, self.boundary)
            elif self.is_grid and self.is_staggered:
                values = self.values.vector.dual.as_channel()
                to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
                values = values.vector.as_dual()
            else:
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
        return Field(self.geometry, values, boundary)

    with_extrapolation = with_boundary

    def with_bounds(self, bounds: Box):
        """ Returns a copy of this field with `bounds` replaced. """
        order = list(bounds.vector.item_names)
        geometry = self.geometry.vector[order]
        new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
        values = math.swap_axes(self.values, new_shape)
        return Field(geometry, values, self.boundary)

    def with_geometry(self, elements: Geometry):
        """ Returns a copy of this field with `elements` replaced. """
        assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
        return Field(elements, self.values, self.boundary)

    with_elements = with_geometry

    def shifted(self, delta: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` by `delta`.

        See Also:
            `Field.shifted_to`.

        Args:
            delta: Shift amount for each center position of `geometry`.

        Returns:
            New `Field` sampled at `geometry.center + delta`.
        """
        return self.with_geometry(self.geometry.shifted(delta))

    def shifted_to(self, position: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` to `positions`.

        See Also:
            `Field.shifted`.

        Args:
            position: New center positions of `geometry`.

        Returns:
            New `Field` sampled at given positions.
        """
        return self.with_geometry(self.geometry.at(position))

    def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
        """
        Alias for `phi.field.pad()`.

        Pads this `Field` using its extrapolation.

        Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

        Args:
            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:
            Padded `Field`
        """
        from ._field_math import pad
        return pad(self, widths)

    def gradient(self,
                 boundary: Extrapolation = None,
                 at: str = 'center',
                 dims: math.DimFilter = spatial,
                 stack_dim: Union[Shape, str] = channel('vector'),
                 order=2,
                 implicit: Solve = None,
                 scheme=None,
                 upwind: 'Field' = None,
                 gradient_extrapolation: Extrapolation = None):
        """Alias for `phi.field.spatial_gradient`"""
        from ._field_math import spatial_gradient
        return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)

    def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
        """Alias for `phi.field.divergence`"""
        from ._field_math import divergence
        return divergence(self, order=order, implicit=implicit, upwind=upwind)

    def curl(self, at='corner'):
        """Alias for `phi.field.curl`"""
        from ._field_math import curl
        return curl(self, at=at)

    def laplace(self,
                axes: DimFilter = spatial,
                gradient: 'Field' = None,
                order=2,
                implicit: math.Solve = None,
                weights: Union[Tensor, 'Field'] = None,
                upwind: 'Field' = None,
                correct_skew=True):
        """Alias for `phi.field.laplace`"""
        from ._field_math import laplace
        return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

    def downsample(self, factor: int):
        from ._field_math import downsample2x
        result = self
        while factor >= 2:
            result = downsample2x(result)
            factor /= 2
        if math.close(factor, 1.):
            return result
        from ._resample import resample
        raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")

    def staggered_tensor(self) -> Tensor:
        """
        Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
        """
        assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
        padded = []
        for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
        result = math.stack(padded, channel(vector=self.resolution))
        assert result.shape.is_uniform
        return result

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

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

    def __and__(self, other):
        assert isinstance(other, Field)
        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))

    def __matmul__(self, other: 'Field'):  # value @ representation
        # Deprecated. Use `resample(value, field)` instead.
        warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
        from ._resample import resample
        return resample(self, to=other, keep_boundary=False)

    def __rmatmul__(self, other):  # values @ representation
        if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)):
            warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        return NotImplemented

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

    def __rrshift__(self, other):
        return self.with_values(other)

    def __lshift__(self, other):
        return self.with_values(other)

    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, Field):
            return NotImplemented
        if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)):
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        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`.
        """
        item = slicing_dict(self, item)
        if not item:
            return self
        boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements))
        item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'}
        geometry = self.geometry[item_without_vec]
        if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape:
            assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids"
            dims = item['vector']
            dims_ = self.geometry.shape['vector'].after_gather({'vector': dims})
            dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]]
            proj_dims = set(self.resolution.names) - set(dims)
            if any(dim not in item for dim in proj_dims):
                # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}")
                item['~vector'] = item['vector']
                del item['vector']
                geometry = self.sampled_elements[item]
            else:
                item['~vector'] = dims
                del item['vector']
        values = self.values[item]
        return Field(geometry, values, boundary)

    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 __expand__(self, dims: Shape, **kwargs) -> 'Field':
        return self.with_values(expand(self.values, dims, **kwargs))

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

    def __eq__(self, other):
        if not isinstance(other, Field):
            return False
        if self.geometry != other.geometry:
            return False
        if self.boundary != other.boundary:
            return False
        return math.always_close(self.values, other.values)

    def __hash__(self):
        return hash((self.geometry, self.boundary))

    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: 'Field', operator: Callable) -> 'Field':
        """
        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.boundary)
        return self.with_values(values).with_extrapolation(extrapolation_)

    def _op2(self, other, operator) -> 'Field':
        if isinstance(other, Geometry):
            raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}")
        if isinstance(other, Field):
            if self.geometry == other.geometry:
                values = operator(self.values, other.values)
                extrapolation_ = operator(self.boundary, other.extrapolation)
                return Field(self.geometry, values, extrapolation_)
            from ._resample import sample
            other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry)
            values = operator(self.values, other_values)
            boundary = operator(self.boundary, other.extrapolation)
            return Field(self.geometry, values, boundary)
        else:
            if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank:
                other = math.wrap(other, self.geometry.shape['vector'])
            else:
                other = math.wrap(other)
            # try:
            #     boundary = operator(self.boundary, as_boundary(other, self.geometry))
            # except TypeError:  # e.g. ZERO_GRADIENT + constant
            boundary = self.boundary  # constants don't affect the boundary conditions (legacy reasons)
            if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape:
                other = other.vector.as_dual()
            values = operator(self.values, other)
            return Field(self.geometry, values, boundary)

    def __repr__(self):
        if self.is_grid:
            type_name = "Grid" if self.is_centered else "Grid faces"
        elif self.is_mesh:
            type_name = "Mesh" if self.is_centered else "Mesh faces"
        elif self.is_point_cloud:
            type_name = "Point cloud" if self.is_centered else "Point cloud edges"
        elif self.is_graph:
            type_name = "Graph" if self.is_centered else "Graph edges"
        else:
            type_name = self.__class__.__name__
        if self.values is not None:
            return f"{type_name}[{self.values}, ext={self.boundary}]"
        else:
            return f"{type_name}[{self.resolution}, ext={self.boundary}]"

    def grid_scatter(self, *args, **kwargs):
        """Deprecated. Use `sample` with `scatter=True` instead."""
        warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
        from ._resample import grid_scatter
        return grid_scatter(self, *args, **kwargs)

    def as_boundary(self) -> Extrapolation:
        """
        Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
        If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
        If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

        Returns:
            `Extrapolation`
        """
        from ._embed import FieldEmbedding
        return FieldEmbedding(self)

A Field represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the geometry property and the corresponding values are stored as one Tensor in values. The boundary conditions and values outside the geometry are determined by boundary.

Examples

Create a periodic 2D grid, initialized via noise fluctuations.

>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

Create a field on an unstructured mesh loaded from a .gmsh file

>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
>>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

Create two cubes and compute a scalar values for each.

>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

Class variables

var boundary : phiml.math.extrapolation.Extrapolation

Boundary conditions describe the values outside of geometry and are used by numerical solvers to compute edge values.

var geometry : phi.geom._geom.Geometry

Discretization Geometry. This determines where in space the values are sampled as well as their relationship and interpretation.

var value_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) are considered values. See phiml.math.magic.PhiTreeNode.__value_attrs__

var values : phiml.math._tensors.Tensor

The sampled values, matching some point set of geometry, e.g. center points, see Geometry.sets.

var variable_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) should be traced / optimized. See phiml.math.magic.PhiTreeNode.__variable_attrs__

Instance variables

prop bounds : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop box : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self.geometry, (UniformGrid, Mesh))
    return self.geometry
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    """ Returns the center points of the `elements` of this `Field`. """
    all_points = self.geometry.get_points(self.sampled_at)
    boundary = self.geometry.get_boundary(self.sampled_at)
    return slice_off_constant_faces(all_points, boundary, self.extrapolation)

Returns the center points of the elements of this Field.

prop data : phiml.math._tensors.Tensor
Expand source code
@property
def data(self) -> Tensor:
    return self.values
prop dx : phiml.math._tensors.Tensor
Expand source code
@property
def dx(self) -> Tensor:
    assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
    return self.bounds.size / self.resolution
prop elements
Expand source code
@property
def elements(self):
    # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
    warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
    return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self.boundary

Returns the Extrapolation of this Field.

prop face_areas
Expand source code
@property
def face_areas(self):
    return self.geometry.face_areas
    # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
Expand source code
@property
def face_centers(self):
    return self.geometry.face_centers
    # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
Expand source code
@property
def face_normals(self):
    return self.geometry.face_normals
    # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
Expand source code
@property
def faces(self):
    return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
Expand source code
@property
def graph(self) -> Graph:
    """Cast `self.geometry` to a `phi.geom.Graph`."""
    assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Graph.

prop grid : phi.geom._grid.UniformGrid
Expand source code
@property
def grid(self) -> UniformGrid:
    """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
    assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a UniformGrid.

prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph
Expand source code
@property
def is_graph(self):
    """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
    return isinstance(self.geometry, Graph)

A Field represents graph data if its geometry is a Graph instance.

prop is_grid
Expand source code
@property
def is_grid(self):
    """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
    return isinstance(self.geometry, UniformGrid)

A Field represents grid data if its geometry is a UniformGrid instance.

prop is_mesh
Expand source code
@property
def is_mesh(self):
    """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
    return isinstance(self.geometry, Mesh)

A Field represents mesh data if its geometry is a Mesh instance.

prop is_point_cloud
Expand source code
@property
def is_point_cloud(self):
    """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
    if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
        return False
    if isinstance(self.geometry, (BaseBox, Sphere, Point)):
        return True
    return True

A Field represents graph data if its geometry is not a set of connected elements, but rather individual geometric objects.

prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
Expand source code
@property
def mesh(self) -> Mesh:
    """Cast `self.geometry` to a `phi.geom.Mesh`."""
    assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Mesh.

prop points
Expand source code
@property
def points(self):
    return self.center
prop resolution
Expand source code
@property
def resolution(self):
    return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
Expand source code
@property
def sampled_at(self):
    matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
Expand source code
@property
def sampled_elements(self) -> Geometry:
    """
    If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
    If the values are sampled at the faces, returns `self.faces`.
    """
    return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

If the values represent are sampled at the element centers or represent the whole element, returns self.geometry. If the values are sampled at the faces, returns self.faces.

prop shape : phiml.math._shape.Shape
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
    """
    if self.is_grid and '~vector' in self.values.shape:
        return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
    set_shape = self.geometry.sets[self.sampled_at]
    return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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
prop spatial_rank : int
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`.
    """
    return self.geometry.spatial_rank

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

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
Expand source code
def as_boundary(self) -> Extrapolation:
    """
    Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
    If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
    If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

    Returns:
        `Extrapolation`
    """
    from ._embed import FieldEmbedding
    return FieldEmbedding(self)

Returns an Extrapolation representing this 'Field''s values as a Dirichlet (constant) boundary. If this Field encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this Field's sampled domain are required, this Field's boundary conditions will be applied to determine the boundary values.

Returns

Extrapolation

def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud.
    This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_spheres()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Point` geometry.
    """
    points = self.sampled_elements.center
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
    return Field(Point(points), values, self.boundary)

Returns this field as a PointCloud. This replaces the Field.geometry with a Point instance while leaving the sample points unchanged.

See Also: Field.as_spheres().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Point geometry.

def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
    This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_points()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Sphere` geometry.
    """
    points = self.sampled_elements.center
    volumes = self.sampled_elements.volume
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
        volumes = pack_dims(volumes, dims, list_dim)
    return Field(Sphere(points, volume=volumes), values, self.boundary)

Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the Field.geometry with a Sphere instance while leaving the sample points unchanged.

See Also: Field.as_points().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Sphere geometry.

def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
    """
    Short for `resample(self, representation)`

    See Also
        `resample()`.

    Returns:
        Field object of same type as `representation`
    """
    from ._resample import resample
    return resample(self, representation, keep_boundary, **kwargs)

Short for resample()(self, representation)

See Also resample().

Returns

Field object of same type as representation

def at_centers(self, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_centers(self, **kwargs) -> 'Field':
    """
    Interpolates the values to the cell centers.

    See Also:
        `Field.at_faces()`, `Field.at()`, `resample`.

    Args:
        **kwargs: Sampling arguments.

    Returns:
        `CenteredGrid` sampled at cell centers.
    """
    if self.is_centered:
        return self
    from ._resample import sample
    values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
    return Field(self.geometry, values, self.boundary)

Interpolates the values to the cell centers.

See Also: Field.at_faces(), Field.at(), resample().

Args

**kwargs
Sampling arguments.

Returns

CenteredGrid() sampled at cell centers.

def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field':
    if self.is_staggered and not boundary:
        return self
    boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
    from ._resample import sample
    values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
    return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
Expand source code
def closest_values(self, points: Tensor):
    """
    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.
    """
    warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
    if isinstance(points, Geometry):
        points = points.center
    # --- CenteredGrid ---
    local_points = self.box.global_to_local(points) * self.resolution - 0.5
    return math.closest_grid_values(self.values, local_points, self.extrapolation)
    # --- StaggeredGrid ---
    if 'staggered_direction' in points.shape:
        points_ = math.unstack(points, '~vector')
        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'])

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.

def curl(self, at='corner')
Expand source code
def curl(self, at='corner'):
    """Alias for `phi.field.curl`"""
    from ._field_math import curl
    return curl(self, at=at)

Alias for curl()

def dimension(self, name: str)
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)

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

def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
    """Alias for `phi.field.divergence`"""
    from ._field_math import divergence
    return divergence(self, order=order, implicit=implicit, upwind=upwind)

Alias for divergence()

def downsample(self, factor: int)
Expand source code
def downsample(self, factor: int):
    from ._field_math import downsample2x
    result = self
    while factor >= 2:
        result = downsample2x(result)
        factor /= 2
    if math.close(factor, 1.):
        return result
    from ._resample import resample
    raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)
Expand source code
def gradient(self,
             boundary: Extrapolation = None,
             at: str = 'center',
             dims: math.DimFilter = spatial,
             stack_dim: Union[Shape, str] = channel('vector'),
             order=2,
             implicit: Solve = None,
             scheme=None,
             upwind: 'Field' = None,
             gradient_extrapolation: Extrapolation = None):
    """Alias for `phi.field.spatial_gradient`"""
    from ._field_math import spatial_gradient
    return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
def grid_scatter(self, *args, **kwargs)
Expand source code
def grid_scatter(self, *args, **kwargs):
    """Deprecated. Use `sample` with `scatter=True` instead."""
    warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
    from ._resample import grid_scatter
    return grid_scatter(self, *args, **kwargs)

Deprecated. Use sample() with scatter=True instead.

def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)
Expand source code
def laplace(self,
            axes: DimFilter = spatial,
            gradient: 'Field' = None,
            order=2,
            implicit: math.Solve = None,
            weights: Union[Tensor, 'Field'] = None,
            upwind: 'Field' = None,
            correct_skew=True):
    """Alias for `phi.field.laplace`"""
    from ._field_math import laplace
    return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

Alias for laplace()

def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)
Expand source code
def numpy(self, order: DimFilter = None):
    """
    Return the field values as `NumPy` array(s).

    Args:
        order: Dimension order as `str` or `Shape`.

    Returns:
        A single NumPy array for uniform values, else a list of NumPy arrays.
    """
    if order is None and self.is_grid:
        axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
        order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
    if self.values.shape.is_uniform:
        return self.values.numpy(order)
    else:
        assert order is not None, f"order must be specified for non-uniform Field values"
        order = self.values.shape.only(order, reorder=True)
        stack_dims = order.non_uniform_shape
        inner_order = order.without(stack_dims)
        return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

Return the field values as NumPy array(s).

Args

order
Dimension order as str or Shape.

Returns

A single NumPy array for uniform values, else a list of NumPy arrays.

def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
    """
    Alias for `phi.field.pad()`.

    Pads this `Field` using its extrapolation.

    Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

    Args:
        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:
        Padded `Field`
    """
    from ._field_math import pad
    return pad(self, widths)

Alias for pad().

Pads this Field using its extrapolation.

Unlike padding the values, this function also affects the geometry of the field, changing its size and origin depending on widths.

Args

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

Padded Field

def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
    """
    Sample the values of this `Field` at the given location or geometry.

    Args:
        where: Location `Tensor` or `Geometry` or
        at: `'center'` or `'face'`.
        **kwargs: Sampling arguments.

    Returns:
        `Tensor`
    """
    from ._resample import sample
    return sample(self, where, at, **kwargs)

Sample the values of this Field at the given location or geometry.

Args

where
Location Tensor or Geometry or
at
'center' or 'face'.
**kwargs
Sampling arguments.

Returns

Tensor

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted(self, delta: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` by `delta`.

    See Also:
        `Field.shifted_to`.

    Args:
        delta: Shift amount for each center position of `geometry`.

    Returns:
        New `Field` sampled at `geometry.center + delta`.
    """
    return self.with_geometry(self.geometry.shifted(delta))

Move the positions of this field's geometry by delta.

See Also: Field.shifted_to().

Args

delta
Shift amount for each center position of geometry.

Returns

New Field sampled at geometry.center + delta.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted_to(self, position: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` to `positions`.

    See Also:
        `Field.shifted`.

    Args:
        position: New center positions of `geometry`.

    Returns:
        New `Field` sampled at given positions.
    """
    return self.with_geometry(self.geometry.at(position))

Move the positions of this field's geometry to positions.

See Also: Field.shifted().

Args

position
New center positions of geometry.

Returns

New Field sampled at given positions.

def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
Expand source code
def staggered_tensor(self) -> Tensor:
    """
    Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
    """
    assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
    padded = []
    for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
    result = math.stack(padded, channel(vector=self.resolution))
    assert result.shape.is_uniform
    return result

Stacks all component grids into a single uniform phi.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 phi.math.Tensor.

def to_grid(self, resolution=(), bounds=None, **resolution_)
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
    resolution = resolution.spatial & spatial(**resolution_)
    if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
        return self
    bounds = self.bounds if bounds is None else bounds
    if not resolution:
        half_sizes = self.geometry.bounding_half_extent()
        if (half_sizes > 0).all:
            size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
        else:
            cell_count = non_batch(self.geometry).non_channel.non_dual.volume
            size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
        res = math.maximum(1, math.round(bounds.size / size))
        resolution = spatial(**res.vector)
    return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
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()`.
    """
    if self.values.shape.is_uniform:
        return self.values
    else:
        return self.staggered_tensor()

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

def with_boundary(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)
Expand source code
def with_bounds(self, bounds: Box):
    """ Returns a copy of this field with `bounds` replaced. """
    order = list(bounds.vector.item_names)
    geometry = self.geometry.vector[order]
    new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
    values = math.swap_axes(self.values, new_shape)
    return Field(geometry, values, self.boundary)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)
Expand source code
def with_values(self, values, **sampling_kwargs):
    """ Returns a copy of this field with `values` replaced. """
    if not isinstance(values, (Tensor, Number)):
        from ._resample import sample
        values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
    else:
        if not spatial(values):
            geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
            if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                values = values.vector.as_dual()
            values = expand(wrap(values), geo_shape.non_batch.non_channel)
    return Field(self.geometry, values, self.boundary)

Returns a copy of this field with values replaced.

class SampledField (geometry: phi.geom._geom.Geometry,
values: phiml.math._tensors.Tensor,
boundary: phiml.math.extrapolation.Extrapolation = 0.0,
variable_attrs: Tuple[str, ...] = ('values',),
value_attrs: Tuple[str, ...] = ('values',))
Expand source code
@dataclass(frozen=True)
class Field(metaclass=_FieldType):
    """
    A `Field` represents a discretized physical quantity (like temperature field or velocity field).
    The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`.
    The boundary conditions and values outside the geometry are determined by `boundary`.

    Examples:
        Create a periodic 2D grid, initialized via noise fluctuations.
        >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

        Create a field on an unstructured mesh loaded from a .gmsh file
        >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
        >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

        Create two cubes and compute a scalar values for each.
        >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

    geometry: Geometry
    """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """
    values: Tensor
    """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`."""
    boundary: Extrapolation = 0.
    """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """

    variable_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`"""
    value_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`"""

    def __post_init__(self):
        math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel)  # shape check

    @property
    def grid(self) -> UniformGrid:
        """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
        assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
        return self.geometry

    @property
    def mesh(self) -> Mesh:
        """Cast `self.geometry` to a `phi.geom.Mesh`."""
        assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def graph(self) -> Graph:
        """Cast `self.geometry` to a `phi.geom.Graph`."""
        assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def faces(self):
        return get_faces(self.geometry, self.boundary)

    @property
    def face_centers(self):
        return self.geometry.face_centers
        # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)

    @property
    def face_normals(self):
        return self.geometry.face_normals
        # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)

    @property
    def face_areas(self):
        return self.geometry.face_areas
        # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)

    @property
    def sampled_elements(self) -> Geometry:
        """
        If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
        If the values are sampled at the faces, returns `self.faces`.
        """
        return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

    @property
    def elements(self):
        # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
        warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
        return self.geometry

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

    @property
    def is_staggered(self):
        return is_staggered(self.values, self.geometry)

    @property
    def center(self) -> Tensor:
        """ Returns the center points of the `elements` of this `Field`. """
        all_points = self.geometry.get_points(self.sampled_at)
        boundary = self.geometry.get_boundary(self.sampled_at)
        return slice_off_constant_faces(all_points, boundary, self.extrapolation)

    @property
    def points(self):
        return self.center

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

    def numpy(self, order: DimFilter = None):
        """
        Return the field values as `NumPy` array(s).

        Args:
            order: Dimension order as `str` or `Shape`.

        Returns:
            A single NumPy array for uniform values, else a list of NumPy arrays.
        """
        if order is None and self.is_grid:
            axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
            order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
        if self.values.shape.is_uniform:
            return self.values.numpy(order)
        else:
            assert order is not None, f"order must be specified for non-uniform Field values"
            order = self.values.shape.only(order, reorder=True)
            stack_dims = order.non_uniform_shape
            inner_order = order.without(stack_dims)
            return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

    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()`.
        """
        if self.values.shape.is_uniform:
            return self.values
        else:
            return self.staggered_tensor()

    @property
    def extrapolation(self) -> Extrapolation:
        """ Returns the `Extrapolation` of this `Field`. """
        return self.boundary

    @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
        """
        if self.is_grid and '~vector' in self.values.shape:
            return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
        set_shape = self.geometry.sets[self.sampled_at]
        return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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

    @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`.
        """
        return self.geometry.spatial_rank

    @property
    def bounds(self) -> BaseBox:
        """
        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`.
        """
        if isinstance(self.geometry.bounds, BaseBox):
            return self.geometry.bounds
        extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
        points = self.geometry.center + extent
        lower = math.min(points, dim=points.shape.non_batch.non_channel)
        upper = math.max(points, dim=points.shape.non_batch.non_channel)
        return Box(lower, upper)

    box = bounds

    @property
    def is_grid(self):
        """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
        return isinstance(self.geometry, UniformGrid)

    @property
    def is_mesh(self):
        """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
        return isinstance(self.geometry, Mesh)

    @property
    def is_graph(self):
        """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
        return isinstance(self.geometry, Graph)

    @property
    def is_point_cloud(self):
        """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
        if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
            return False
        if isinstance(self.geometry, (BaseBox, Sphere, Point)):
            return True
        return True

    @property
    def dx(self) -> Tensor:
        assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
        return self.bounds.size / self.resolution

    @property
    def cells(self):
        assert isinstance(self.geometry, (UniformGrid, Mesh))
        return self.geometry

    def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
        resolution = resolution.spatial & spatial(**resolution_)
        if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
            return self
        bounds = self.bounds if bounds is None else bounds
        if not resolution:
            half_sizes = self.geometry.bounding_half_extent()
            if (half_sizes > 0).all:
                size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
            else:
                cell_count = non_batch(self.geometry).non_channel.non_dual.volume
                size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
            res = math.maximum(1, math.round(bounds.size / size))
            resolution = spatial(**res.vector)
        return Field(UniformGrid(resolution, bounds), self, self.boundary)

    def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud.
        This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_spheres()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Point` geometry.
        """
        points = self.sampled_elements.center
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
        return Field(Point(points), values, self.boundary)

    def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
        This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_points()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Sphere` geometry.
        """
        points = self.sampled_elements.center
        volumes = self.sampled_elements.volume
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
            volumes = pack_dims(volumes, dims, list_dim)
        return Field(Sphere(points, volume=volumes), values, self.boundary)

    def at_centers(self, **kwargs) -> 'Field':
        """
        Interpolates the values to the cell centers.

        See Also:
            `Field.at_faces()`, `Field.at()`, `resample`.

        Args:
            **kwargs: Sampling arguments.

        Returns:
            `CenteredGrid` sampled at cell centers.
        """
        if self.is_centered:
            return self
        from ._resample import sample
        values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
        return Field(self.geometry, values, self.boundary)

    def at_faces(self, boundary=None, **kwargs) -> 'Field':
        if self.is_staggered and not boundary:
            return self
        boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
        from ._resample import sample
        values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
        return Field(self.geometry, values, boundary)

    @property
    def sampled_at(self):
        matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
        return matching_sets[-1]

    def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
        """
        Short for `resample(self, representation)`

        See Also
            `resample()`.

        Returns:
            Field object of same type as `representation`
        """
        from ._resample import resample
        return resample(self, representation, keep_boundary, **kwargs)

    def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
        """
        Sample the values of this `Field` at the given location or geometry.

        Args:
            where: Location `Tensor` or `Geometry` or
            at: `'center'` or `'face'`.
            **kwargs: Sampling arguments.

        Returns:
            `Tensor`
        """
        from ._resample import sample
        return sample(self, where, at, **kwargs)

    def closest_values(self, points: Tensor):
        """
        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.
        """
        warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
        if isinstance(points, Geometry):
            points = points.center
        # --- CenteredGrid ---
        local_points = self.box.global_to_local(points) * self.resolution - 0.5
        return math.closest_grid_values(self.values, local_points, self.extrapolation)
        # --- StaggeredGrid ---
        if 'staggered_direction' in points.shape:
            points_ = math.unstack(points, '~vector')
            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 with_values(self, values, **sampling_kwargs):
        """ Returns a copy of this field with `values` replaced. """
        if not isinstance(values, (Tensor, Number)):
            from ._resample import sample
            values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
        else:
            if not spatial(values):
                geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
                if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                    values = values.vector.as_dual()
                values = expand(wrap(values), geo_shape.non_batch.non_channel)
        return Field(self.geometry, values, self.boundary)

    def with_boundary(self, boundary):
        """ Returns a copy of this field with the `boundary` replaced. """
        boundary = as_boundary(boundary, self.geometry)
        boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
        old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
        new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
        if old_determined_slices.values() == new_determined_slices.values():
            return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
        to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
        to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
        values = math.slice_off(self.values, *to_remove)
        if to_add:
            if self.is_mesh:
                values = self.mesh.pad_boundary(values, to_add, self.boundary)
            elif self.is_grid and self.is_staggered:
                values = self.values.vector.dual.as_channel()
                to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
                values = values.vector.as_dual()
            else:
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
        return Field(self.geometry, values, boundary)

    with_extrapolation = with_boundary

    def with_bounds(self, bounds: Box):
        """ Returns a copy of this field with `bounds` replaced. """
        order = list(bounds.vector.item_names)
        geometry = self.geometry.vector[order]
        new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
        values = math.swap_axes(self.values, new_shape)
        return Field(geometry, values, self.boundary)

    def with_geometry(self, elements: Geometry):
        """ Returns a copy of this field with `elements` replaced. """
        assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
        return Field(elements, self.values, self.boundary)

    with_elements = with_geometry

    def shifted(self, delta: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` by `delta`.

        See Also:
            `Field.shifted_to`.

        Args:
            delta: Shift amount for each center position of `geometry`.

        Returns:
            New `Field` sampled at `geometry.center + delta`.
        """
        return self.with_geometry(self.geometry.shifted(delta))

    def shifted_to(self, position: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` to `positions`.

        See Also:
            `Field.shifted`.

        Args:
            position: New center positions of `geometry`.

        Returns:
            New `Field` sampled at given positions.
        """
        return self.with_geometry(self.geometry.at(position))

    def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
        """
        Alias for `phi.field.pad()`.

        Pads this `Field` using its extrapolation.

        Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

        Args:
            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:
            Padded `Field`
        """
        from ._field_math import pad
        return pad(self, widths)

    def gradient(self,
                 boundary: Extrapolation = None,
                 at: str = 'center',
                 dims: math.DimFilter = spatial,
                 stack_dim: Union[Shape, str] = channel('vector'),
                 order=2,
                 implicit: Solve = None,
                 scheme=None,
                 upwind: 'Field' = None,
                 gradient_extrapolation: Extrapolation = None):
        """Alias for `phi.field.spatial_gradient`"""
        from ._field_math import spatial_gradient
        return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)

    def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
        """Alias for `phi.field.divergence`"""
        from ._field_math import divergence
        return divergence(self, order=order, implicit=implicit, upwind=upwind)

    def curl(self, at='corner'):
        """Alias for `phi.field.curl`"""
        from ._field_math import curl
        return curl(self, at=at)

    def laplace(self,
                axes: DimFilter = spatial,
                gradient: 'Field' = None,
                order=2,
                implicit: math.Solve = None,
                weights: Union[Tensor, 'Field'] = None,
                upwind: 'Field' = None,
                correct_skew=True):
        """Alias for `phi.field.laplace`"""
        from ._field_math import laplace
        return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

    def downsample(self, factor: int):
        from ._field_math import downsample2x
        result = self
        while factor >= 2:
            result = downsample2x(result)
            factor /= 2
        if math.close(factor, 1.):
            return result
        from ._resample import resample
        raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")

    def staggered_tensor(self) -> Tensor:
        """
        Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
        """
        assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
        padded = []
        for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
        result = math.stack(padded, channel(vector=self.resolution))
        assert result.shape.is_uniform
        return result

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

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

    def __and__(self, other):
        assert isinstance(other, Field)
        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))

    def __matmul__(self, other: 'Field'):  # value @ representation
        # Deprecated. Use `resample(value, field)` instead.
        warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
        from ._resample import resample
        return resample(self, to=other, keep_boundary=False)

    def __rmatmul__(self, other):  # values @ representation
        if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)):
            warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        return NotImplemented

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

    def __rrshift__(self, other):
        return self.with_values(other)

    def __lshift__(self, other):
        return self.with_values(other)

    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, Field):
            return NotImplemented
        if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)):
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        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`.
        """
        item = slicing_dict(self, item)
        if not item:
            return self
        boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements))
        item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'}
        geometry = self.geometry[item_without_vec]
        if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape:
            assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids"
            dims = item['vector']
            dims_ = self.geometry.shape['vector'].after_gather({'vector': dims})
            dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]]
            proj_dims = set(self.resolution.names) - set(dims)
            if any(dim not in item for dim in proj_dims):
                # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}")
                item['~vector'] = item['vector']
                del item['vector']
                geometry = self.sampled_elements[item]
            else:
                item['~vector'] = dims
                del item['vector']
        values = self.values[item]
        return Field(geometry, values, boundary)

    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 __expand__(self, dims: Shape, **kwargs) -> 'Field':
        return self.with_values(expand(self.values, dims, **kwargs))

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

    def __eq__(self, other):
        if not isinstance(other, Field):
            return False
        if self.geometry != other.geometry:
            return False
        if self.boundary != other.boundary:
            return False
        return math.always_close(self.values, other.values)

    def __hash__(self):
        return hash((self.geometry, self.boundary))

    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: 'Field', operator: Callable) -> 'Field':
        """
        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.boundary)
        return self.with_values(values).with_extrapolation(extrapolation_)

    def _op2(self, other, operator) -> 'Field':
        if isinstance(other, Geometry):
            raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}")
        if isinstance(other, Field):
            if self.geometry == other.geometry:
                values = operator(self.values, other.values)
                extrapolation_ = operator(self.boundary, other.extrapolation)
                return Field(self.geometry, values, extrapolation_)
            from ._resample import sample
            other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry)
            values = operator(self.values, other_values)
            boundary = operator(self.boundary, other.extrapolation)
            return Field(self.geometry, values, boundary)
        else:
            if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank:
                other = math.wrap(other, self.geometry.shape['vector'])
            else:
                other = math.wrap(other)
            # try:
            #     boundary = operator(self.boundary, as_boundary(other, self.geometry))
            # except TypeError:  # e.g. ZERO_GRADIENT + constant
            boundary = self.boundary  # constants don't affect the boundary conditions (legacy reasons)
            if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape:
                other = other.vector.as_dual()
            values = operator(self.values, other)
            return Field(self.geometry, values, boundary)

    def __repr__(self):
        if self.is_grid:
            type_name = "Grid" if self.is_centered else "Grid faces"
        elif self.is_mesh:
            type_name = "Mesh" if self.is_centered else "Mesh faces"
        elif self.is_point_cloud:
            type_name = "Point cloud" if self.is_centered else "Point cloud edges"
        elif self.is_graph:
            type_name = "Graph" if self.is_centered else "Graph edges"
        else:
            type_name = self.__class__.__name__
        if self.values is not None:
            return f"{type_name}[{self.values}, ext={self.boundary}]"
        else:
            return f"{type_name}[{self.resolution}, ext={self.boundary}]"

    def grid_scatter(self, *args, **kwargs):
        """Deprecated. Use `sample` with `scatter=True` instead."""
        warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
        from ._resample import grid_scatter
        return grid_scatter(self, *args, **kwargs)

    def as_boundary(self) -> Extrapolation:
        """
        Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
        If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
        If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

        Returns:
            `Extrapolation`
        """
        from ._embed import FieldEmbedding
        return FieldEmbedding(self)

A Field represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the geometry property and the corresponding values are stored as one Tensor in values. The boundary conditions and values outside the geometry are determined by boundary.

Examples

Create a periodic 2D grid, initialized via noise fluctuations.

>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

Create a field on an unstructured mesh loaded from a .gmsh file

>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
>>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

Create two cubes and compute a scalar values for each.

>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

Class variables

var boundary : phiml.math.extrapolation.Extrapolation

Boundary conditions describe the values outside of geometry and are used by numerical solvers to compute edge values.

var geometry : phi.geom._geom.Geometry

Discretization Geometry. This determines where in space the values are sampled as well as their relationship and interpretation.

var value_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) are considered values. See phiml.math.magic.PhiTreeNode.__value_attrs__

var values : phiml.math._tensors.Tensor

The sampled values, matching some point set of geometry, e.g. center points, see Geometry.sets.

var variable_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) should be traced / optimized. See phiml.math.magic.PhiTreeNode.__variable_attrs__

Instance variables

prop bounds : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop box : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self.geometry, (UniformGrid, Mesh))
    return self.geometry
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    """ Returns the center points of the `elements` of this `Field`. """
    all_points = self.geometry.get_points(self.sampled_at)
    boundary = self.geometry.get_boundary(self.sampled_at)
    return slice_off_constant_faces(all_points, boundary, self.extrapolation)

Returns the center points of the elements of this Field.

prop data : phiml.math._tensors.Tensor
Expand source code
@property
def data(self) -> Tensor:
    return self.values
prop dx : phiml.math._tensors.Tensor
Expand source code
@property
def dx(self) -> Tensor:
    assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
    return self.bounds.size / self.resolution
prop elements
Expand source code
@property
def elements(self):
    # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
    warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
    return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self.boundary

Returns the Extrapolation of this Field.

prop face_areas
Expand source code
@property
def face_areas(self):
    return self.geometry.face_areas
    # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
Expand source code
@property
def face_centers(self):
    return self.geometry.face_centers
    # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
Expand source code
@property
def face_normals(self):
    return self.geometry.face_normals
    # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
Expand source code
@property
def faces(self):
    return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
Expand source code
@property
def graph(self) -> Graph:
    """Cast `self.geometry` to a `phi.geom.Graph`."""
    assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Graph.

prop grid : phi.geom._grid.UniformGrid
Expand source code
@property
def grid(self) -> UniformGrid:
    """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
    assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a UniformGrid.

prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph
Expand source code
@property
def is_graph(self):
    """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
    return isinstance(self.geometry, Graph)

A Field represents graph data if its geometry is a Graph instance.

prop is_grid
Expand source code
@property
def is_grid(self):
    """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
    return isinstance(self.geometry, UniformGrid)

A Field represents grid data if its geometry is a UniformGrid instance.

prop is_mesh
Expand source code
@property
def is_mesh(self):
    """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
    return isinstance(self.geometry, Mesh)

A Field represents mesh data if its geometry is a Mesh instance.

prop is_point_cloud
Expand source code
@property
def is_point_cloud(self):
    """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
    if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
        return False
    if isinstance(self.geometry, (BaseBox, Sphere, Point)):
        return True
    return True

A Field represents graph data if its geometry is not a set of connected elements, but rather individual geometric objects.

prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
Expand source code
@property
def mesh(self) -> Mesh:
    """Cast `self.geometry` to a `phi.geom.Mesh`."""
    assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Mesh.

prop points
Expand source code
@property
def points(self):
    return self.center
prop resolution
Expand source code
@property
def resolution(self):
    return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
Expand source code
@property
def sampled_at(self):
    matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
Expand source code
@property
def sampled_elements(self) -> Geometry:
    """
    If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
    If the values are sampled at the faces, returns `self.faces`.
    """
    return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

If the values represent are sampled at the element centers or represent the whole element, returns self.geometry. If the values are sampled at the faces, returns self.faces.

prop shape : phiml.math._shape.Shape
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
    """
    if self.is_grid and '~vector' in self.values.shape:
        return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
    set_shape = self.geometry.sets[self.sampled_at]
    return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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
prop spatial_rank : int
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`.
    """
    return self.geometry.spatial_rank

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

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
Expand source code
def as_boundary(self) -> Extrapolation:
    """
    Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
    If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
    If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

    Returns:
        `Extrapolation`
    """
    from ._embed import FieldEmbedding
    return FieldEmbedding(self)

Returns an Extrapolation representing this 'Field''s values as a Dirichlet (constant) boundary. If this Field encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this Field's sampled domain are required, this Field's boundary conditions will be applied to determine the boundary values.

Returns

Extrapolation

def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud.
    This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_spheres()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Point` geometry.
    """
    points = self.sampled_elements.center
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
    return Field(Point(points), values, self.boundary)

Returns this field as a PointCloud. This replaces the Field.geometry with a Point instance while leaving the sample points unchanged.

See Also: Field.as_spheres().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Point geometry.

def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
    This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_points()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Sphere` geometry.
    """
    points = self.sampled_elements.center
    volumes = self.sampled_elements.volume
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
        volumes = pack_dims(volumes, dims, list_dim)
    return Field(Sphere(points, volume=volumes), values, self.boundary)

Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the Field.geometry with a Sphere instance while leaving the sample points unchanged.

See Also: Field.as_points().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Sphere geometry.

def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
    """
    Short for `resample(self, representation)`

    See Also
        `resample()`.

    Returns:
        Field object of same type as `representation`
    """
    from ._resample import resample
    return resample(self, representation, keep_boundary, **kwargs)

Short for resample()(self, representation)

See Also resample().

Returns

Field object of same type as representation

def at_centers(self, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_centers(self, **kwargs) -> 'Field':
    """
    Interpolates the values to the cell centers.

    See Also:
        `Field.at_faces()`, `Field.at()`, `resample`.

    Args:
        **kwargs: Sampling arguments.

    Returns:
        `CenteredGrid` sampled at cell centers.
    """
    if self.is_centered:
        return self
    from ._resample import sample
    values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
    return Field(self.geometry, values, self.boundary)

Interpolates the values to the cell centers.

See Also: Field.at_faces(), Field.at(), resample().

Args

**kwargs
Sampling arguments.

Returns

CenteredGrid() sampled at cell centers.

def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field':
    if self.is_staggered and not boundary:
        return self
    boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
    from ._resample import sample
    values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
    return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
Expand source code
def closest_values(self, points: Tensor):
    """
    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.
    """
    warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
    if isinstance(points, Geometry):
        points = points.center
    # --- CenteredGrid ---
    local_points = self.box.global_to_local(points) * self.resolution - 0.5
    return math.closest_grid_values(self.values, local_points, self.extrapolation)
    # --- StaggeredGrid ---
    if 'staggered_direction' in points.shape:
        points_ = math.unstack(points, '~vector')
        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'])

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.

def curl(self, at='corner')
Expand source code
def curl(self, at='corner'):
    """Alias for `phi.field.curl`"""
    from ._field_math import curl
    return curl(self, at=at)

Alias for curl()

def dimension(self, name: str)
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)

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

def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
    """Alias for `phi.field.divergence`"""
    from ._field_math import divergence
    return divergence(self, order=order, implicit=implicit, upwind=upwind)

Alias for divergence()

def downsample(self, factor: int)
Expand source code
def downsample(self, factor: int):
    from ._field_math import downsample2x
    result = self
    while factor >= 2:
        result = downsample2x(result)
        factor /= 2
    if math.close(factor, 1.):
        return result
    from ._resample import resample
    raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)
Expand source code
def gradient(self,
             boundary: Extrapolation = None,
             at: str = 'center',
             dims: math.DimFilter = spatial,
             stack_dim: Union[Shape, str] = channel('vector'),
             order=2,
             implicit: Solve = None,
             scheme=None,
             upwind: 'Field' = None,
             gradient_extrapolation: Extrapolation = None):
    """Alias for `phi.field.spatial_gradient`"""
    from ._field_math import spatial_gradient
    return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
def grid_scatter(self, *args, **kwargs)
Expand source code
def grid_scatter(self, *args, **kwargs):
    """Deprecated. Use `sample` with `scatter=True` instead."""
    warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
    from ._resample import grid_scatter
    return grid_scatter(self, *args, **kwargs)

Deprecated. Use sample() with scatter=True instead.

def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)
Expand source code
def laplace(self,
            axes: DimFilter = spatial,
            gradient: 'Field' = None,
            order=2,
            implicit: math.Solve = None,
            weights: Union[Tensor, 'Field'] = None,
            upwind: 'Field' = None,
            correct_skew=True):
    """Alias for `phi.field.laplace`"""
    from ._field_math import laplace
    return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

Alias for laplace()

def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)
Expand source code
def numpy(self, order: DimFilter = None):
    """
    Return the field values as `NumPy` array(s).

    Args:
        order: Dimension order as `str` or `Shape`.

    Returns:
        A single NumPy array for uniform values, else a list of NumPy arrays.
    """
    if order is None and self.is_grid:
        axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
        order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
    if self.values.shape.is_uniform:
        return self.values.numpy(order)
    else:
        assert order is not None, f"order must be specified for non-uniform Field values"
        order = self.values.shape.only(order, reorder=True)
        stack_dims = order.non_uniform_shape
        inner_order = order.without(stack_dims)
        return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

Return the field values as NumPy array(s).

Args

order
Dimension order as str or Shape.

Returns

A single NumPy array for uniform values, else a list of NumPy arrays.

def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
    """
    Alias for `phi.field.pad()`.

    Pads this `Field` using its extrapolation.

    Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

    Args:
        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:
        Padded `Field`
    """
    from ._field_math import pad
    return pad(self, widths)

Alias for pad().

Pads this Field using its extrapolation.

Unlike padding the values, this function also affects the geometry of the field, changing its size and origin depending on widths.

Args

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

Padded Field

def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
    """
    Sample the values of this `Field` at the given location or geometry.

    Args:
        where: Location `Tensor` or `Geometry` or
        at: `'center'` or `'face'`.
        **kwargs: Sampling arguments.

    Returns:
        `Tensor`
    """
    from ._resample import sample
    return sample(self, where, at, **kwargs)

Sample the values of this Field at the given location or geometry.

Args

where
Location Tensor or Geometry or
at
'center' or 'face'.
**kwargs
Sampling arguments.

Returns

Tensor

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted(self, delta: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` by `delta`.

    See Also:
        `Field.shifted_to`.

    Args:
        delta: Shift amount for each center position of `geometry`.

    Returns:
        New `Field` sampled at `geometry.center + delta`.
    """
    return self.with_geometry(self.geometry.shifted(delta))

Move the positions of this field's geometry by delta.

See Also: Field.shifted_to().

Args

delta
Shift amount for each center position of geometry.

Returns

New Field sampled at geometry.center + delta.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted_to(self, position: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` to `positions`.

    See Also:
        `Field.shifted`.

    Args:
        position: New center positions of `geometry`.

    Returns:
        New `Field` sampled at given positions.
    """
    return self.with_geometry(self.geometry.at(position))

Move the positions of this field's geometry to positions.

See Also: Field.shifted().

Args

position
New center positions of geometry.

Returns

New Field sampled at given positions.

def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
Expand source code
def staggered_tensor(self) -> Tensor:
    """
    Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
    """
    assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
    padded = []
    for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
    result = math.stack(padded, channel(vector=self.resolution))
    assert result.shape.is_uniform
    return result

Stacks all component grids into a single uniform phi.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 phi.math.Tensor.

def to_grid(self, resolution=(), bounds=None, **resolution_)
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
    resolution = resolution.spatial & spatial(**resolution_)
    if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
        return self
    bounds = self.bounds if bounds is None else bounds
    if not resolution:
        half_sizes = self.geometry.bounding_half_extent()
        if (half_sizes > 0).all:
            size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
        else:
            cell_count = non_batch(self.geometry).non_channel.non_dual.volume
            size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
        res = math.maximum(1, math.round(bounds.size / size))
        resolution = spatial(**res.vector)
    return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
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()`.
    """
    if self.values.shape.is_uniform:
        return self.values
    else:
        return self.staggered_tensor()

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

def with_boundary(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)
Expand source code
def with_bounds(self, bounds: Box):
    """ Returns a copy of this field with `bounds` replaced. """
    order = list(bounds.vector.item_names)
    geometry = self.geometry.vector[order]
    new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
    values = math.swap_axes(self.values, new_shape)
    return Field(geometry, values, self.boundary)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)
Expand source code
def with_values(self, values, **sampling_kwargs):
    """ Returns a copy of this field with `values` replaced. """
    if not isinstance(values, (Tensor, Number)):
        from ._resample import sample
        values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
    else:
        if not spatial(values):
            geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
            if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                values = values.vector.as_dual()
            values = expand(wrap(values), geo_shape.non_batch.non_channel)
    return Field(self.geometry, values, self.boundary)

Returns a copy of this field with values replaced.

class Grid
Expand source code
@dataclass(frozen=True)
class Field(metaclass=_FieldType):
    """
    A `Field` represents a discretized physical quantity (like temperature field or velocity field).
    The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`.
    The boundary conditions and values outside the geometry are determined by `boundary`.

    Examples:
        Create a periodic 2D grid, initialized via noise fluctuations.
        >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

        Create a field on an unstructured mesh loaded from a .gmsh file
        >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
        >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

        Create two cubes and compute a scalar values for each.
        >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

    geometry: Geometry
    """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """
    values: Tensor
    """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`."""
    boundary: Extrapolation = 0.
    """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """

    variable_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`"""
    value_attrs: Tuple[str, ...] = ('values',)
    """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`"""

    def __post_init__(self):
        math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel)  # shape check

    @property
    def grid(self) -> UniformGrid:
        """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
        assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
        return self.geometry

    @property
    def mesh(self) -> Mesh:
        """Cast `self.geometry` to a `phi.geom.Mesh`."""
        assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def graph(self) -> Graph:
        """Cast `self.geometry` to a `phi.geom.Graph`."""
        assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
        return self.geometry

    @property
    def faces(self):
        return get_faces(self.geometry, self.boundary)

    @property
    def face_centers(self):
        return self.geometry.face_centers
        # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)

    @property
    def face_normals(self):
        return self.geometry.face_normals
        # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)

    @property
    def face_areas(self):
        return self.geometry.face_areas
        # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)

    @property
    def sampled_elements(self) -> Geometry:
        """
        If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
        If the values are sampled at the faces, returns `self.faces`.
        """
        return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

    @property
    def elements(self):
        # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
        warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
        return self.geometry

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

    @property
    def is_staggered(self):
        return is_staggered(self.values, self.geometry)

    @property
    def center(self) -> Tensor:
        """ Returns the center points of the `elements` of this `Field`. """
        all_points = self.geometry.get_points(self.sampled_at)
        boundary = self.geometry.get_boundary(self.sampled_at)
        return slice_off_constant_faces(all_points, boundary, self.extrapolation)

    @property
    def points(self):
        return self.center

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

    def numpy(self, order: DimFilter = None):
        """
        Return the field values as `NumPy` array(s).

        Args:
            order: Dimension order as `str` or `Shape`.

        Returns:
            A single NumPy array for uniform values, else a list of NumPy arrays.
        """
        if order is None and self.is_grid:
            axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
            order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
        if self.values.shape.is_uniform:
            return self.values.numpy(order)
        else:
            assert order is not None, f"order must be specified for non-uniform Field values"
            order = self.values.shape.only(order, reorder=True)
            stack_dims = order.non_uniform_shape
            inner_order = order.without(stack_dims)
            return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

    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()`.
        """
        if self.values.shape.is_uniform:
            return self.values
        else:
            return self.staggered_tensor()

    @property
    def extrapolation(self) -> Extrapolation:
        """ Returns the `Extrapolation` of this `Field`. """
        return self.boundary

    @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
        """
        if self.is_grid and '~vector' in self.values.shape:
            return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
        set_shape = self.geometry.sets[self.sampled_at]
        return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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

    @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`.
        """
        return self.geometry.spatial_rank

    @property
    def bounds(self) -> BaseBox:
        """
        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`.
        """
        if isinstance(self.geometry.bounds, BaseBox):
            return self.geometry.bounds
        extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
        points = self.geometry.center + extent
        lower = math.min(points, dim=points.shape.non_batch.non_channel)
        upper = math.max(points, dim=points.shape.non_batch.non_channel)
        return Box(lower, upper)

    box = bounds

    @property
    def is_grid(self):
        """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
        return isinstance(self.geometry, UniformGrid)

    @property
    def is_mesh(self):
        """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
        return isinstance(self.geometry, Mesh)

    @property
    def is_graph(self):
        """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
        return isinstance(self.geometry, Graph)

    @property
    def is_point_cloud(self):
        """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
        if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
            return False
        if isinstance(self.geometry, (BaseBox, Sphere, Point)):
            return True
        return True

    @property
    def dx(self) -> Tensor:
        assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
        return self.bounds.size / self.resolution

    @property
    def cells(self):
        assert isinstance(self.geometry, (UniformGrid, Mesh))
        return self.geometry

    def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
        resolution = resolution.spatial & spatial(**resolution_)
        if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
            return self
        bounds = self.bounds if bounds is None else bounds
        if not resolution:
            half_sizes = self.geometry.bounding_half_extent()
            if (half_sizes > 0).all:
                size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
            else:
                cell_count = non_batch(self.geometry).non_channel.non_dual.volume
                size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
            res = math.maximum(1, math.round(bounds.size / size))
            resolution = spatial(**res.vector)
        return Field(UniformGrid(resolution, bounds), self, self.boundary)

    def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud.
        This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_spheres()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Point` geometry.
        """
        points = self.sampled_elements.center
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
        return Field(Point(points), values, self.boundary)

    def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
        """
        Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
        This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

        See Also:
            `Field.as_points()`.

        Args:
            list_dim: If not `None`, packs spatial, instance and dual dims.
                Defaults to `instance('elements')`.

        Returns:
            `Field` with same values and boundaries but `Sphere` geometry.
        """
        points = self.sampled_elements.center
        volumes = self.sampled_elements.volume
        values = self.values
        if list_dim:
            dims = non_batch(points).non_channel & non_batch(points).non_channel
            points = pack_dims(points, dims, list_dim)
            values = pack_dims(values, dims, list_dim)
            volumes = pack_dims(volumes, dims, list_dim)
        return Field(Sphere(points, volume=volumes), values, self.boundary)

    def at_centers(self, **kwargs) -> 'Field':
        """
        Interpolates the values to the cell centers.

        See Also:
            `Field.at_faces()`, `Field.at()`, `resample`.

        Args:
            **kwargs: Sampling arguments.

        Returns:
            `CenteredGrid` sampled at cell centers.
        """
        if self.is_centered:
            return self
        from ._resample import sample
        values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
        return Field(self.geometry, values, self.boundary)

    def at_faces(self, boundary=None, **kwargs) -> 'Field':
        if self.is_staggered and not boundary:
            return self
        boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
        from ._resample import sample
        values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
        return Field(self.geometry, values, boundary)

    @property
    def sampled_at(self):
        matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
        return matching_sets[-1]

    def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
        """
        Short for `resample(self, representation)`

        See Also
            `resample()`.

        Returns:
            Field object of same type as `representation`
        """
        from ._resample import resample
        return resample(self, representation, keep_boundary, **kwargs)

    def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
        """
        Sample the values of this `Field` at the given location or geometry.

        Args:
            where: Location `Tensor` or `Geometry` or
            at: `'center'` or `'face'`.
            **kwargs: Sampling arguments.

        Returns:
            `Tensor`
        """
        from ._resample import sample
        return sample(self, where, at, **kwargs)

    def closest_values(self, points: Tensor):
        """
        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.
        """
        warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
        if isinstance(points, Geometry):
            points = points.center
        # --- CenteredGrid ---
        local_points = self.box.global_to_local(points) * self.resolution - 0.5
        return math.closest_grid_values(self.values, local_points, self.extrapolation)
        # --- StaggeredGrid ---
        if 'staggered_direction' in points.shape:
            points_ = math.unstack(points, '~vector')
            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 with_values(self, values, **sampling_kwargs):
        """ Returns a copy of this field with `values` replaced. """
        if not isinstance(values, (Tensor, Number)):
            from ._resample import sample
            values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
        else:
            if not spatial(values):
                geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
                if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                    values = values.vector.as_dual()
                values = expand(wrap(values), geo_shape.non_batch.non_channel)
        return Field(self.geometry, values, self.boundary)

    def with_boundary(self, boundary):
        """ Returns a copy of this field with the `boundary` replaced. """
        boundary = as_boundary(boundary, self.geometry)
        boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
        old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
        new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
        if old_determined_slices.values() == new_determined_slices.values():
            return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
        to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
        to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
        values = math.slice_off(self.values, *to_remove)
        if to_add:
            if self.is_mesh:
                values = self.mesh.pad_boundary(values, to_add, self.boundary)
            elif self.is_grid and self.is_staggered:
                values = self.values.vector.dual.as_channel()
                to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
                values = values.vector.as_dual()
            else:
                values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
        return Field(self.geometry, values, boundary)

    with_extrapolation = with_boundary

    def with_bounds(self, bounds: Box):
        """ Returns a copy of this field with `bounds` replaced. """
        order = list(bounds.vector.item_names)
        geometry = self.geometry.vector[order]
        new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
        values = math.swap_axes(self.values, new_shape)
        return Field(geometry, values, self.boundary)

    def with_geometry(self, elements: Geometry):
        """ Returns a copy of this field with `elements` replaced. """
        assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
        return Field(elements, self.values, self.boundary)

    with_elements = with_geometry

    def shifted(self, delta: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` by `delta`.

        See Also:
            `Field.shifted_to`.

        Args:
            delta: Shift amount for each center position of `geometry`.

        Returns:
            New `Field` sampled at `geometry.center + delta`.
        """
        return self.with_geometry(self.geometry.shifted(delta))

    def shifted_to(self, position: Tensor) -> 'Field':
        """
        Move the positions of this field's `geometry` to `positions`.

        See Also:
            `Field.shifted`.

        Args:
            position: New center positions of `geometry`.

        Returns:
            New `Field` sampled at given positions.
        """
        return self.with_geometry(self.geometry.at(position))

    def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
        """
        Alias for `phi.field.pad()`.

        Pads this `Field` using its extrapolation.

        Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

        Args:
            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:
            Padded `Field`
        """
        from ._field_math import pad
        return pad(self, widths)

    def gradient(self,
                 boundary: Extrapolation = None,
                 at: str = 'center',
                 dims: math.DimFilter = spatial,
                 stack_dim: Union[Shape, str] = channel('vector'),
                 order=2,
                 implicit: Solve = None,
                 scheme=None,
                 upwind: 'Field' = None,
                 gradient_extrapolation: Extrapolation = None):
        """Alias for `phi.field.spatial_gradient`"""
        from ._field_math import spatial_gradient
        return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)

    def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
        """Alias for `phi.field.divergence`"""
        from ._field_math import divergence
        return divergence(self, order=order, implicit=implicit, upwind=upwind)

    def curl(self, at='corner'):
        """Alias for `phi.field.curl`"""
        from ._field_math import curl
        return curl(self, at=at)

    def laplace(self,
                axes: DimFilter = spatial,
                gradient: 'Field' = None,
                order=2,
                implicit: math.Solve = None,
                weights: Union[Tensor, 'Field'] = None,
                upwind: 'Field' = None,
                correct_skew=True):
        """Alias for `phi.field.laplace`"""
        from ._field_math import laplace
        return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

    def downsample(self, factor: int):
        from ._field_math import downsample2x
        result = self
        while factor >= 2:
            result = downsample2x(result)
            factor /= 2
        if math.close(factor, 1.):
            return result
        from ._resample import resample
        raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")

    def staggered_tensor(self) -> Tensor:
        """
        Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
        """
        assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
        padded = []
        for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
        result = math.stack(padded, channel(vector=self.resolution))
        assert result.shape.is_uniform
        return result

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

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

    def __and__(self, other):
        assert isinstance(other, Field)
        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))

    def __matmul__(self, other: 'Field'):  # value @ representation
        # Deprecated. Use `resample(value, field)` instead.
        warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
        from ._resample import resample
        return resample(self, to=other, keep_boundary=False)

    def __rmatmul__(self, other):  # values @ representation
        if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)):
            warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning)
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        return NotImplemented

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

    def __rrshift__(self, other):
        return self.with_values(other)

    def __lshift__(self, other):
        return self.with_values(other)

    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, Field):
            return NotImplemented
        if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)):
            from ._resample import resample
            return resample(other, to=self, keep_boundary=False)
        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`.
        """
        item = slicing_dict(self, item)
        if not item:
            return self
        boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements))
        item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'}
        geometry = self.geometry[item_without_vec]
        if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape:
            assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids"
            dims = item['vector']
            dims_ = self.geometry.shape['vector'].after_gather({'vector': dims})
            dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]]
            proj_dims = set(self.resolution.names) - set(dims)
            if any(dim not in item for dim in proj_dims):
                # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}")
                item['~vector'] = item['vector']
                del item['vector']
                geometry = self.sampled_elements[item]
            else:
                item['~vector'] = dims
                del item['vector']
        values = self.values[item]
        return Field(geometry, values, boundary)

    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 __expand__(self, dims: Shape, **kwargs) -> 'Field':
        return self.with_values(expand(self.values, dims, **kwargs))

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

    def __eq__(self, other):
        if not isinstance(other, Field):
            return False
        if self.geometry != other.geometry:
            return False
        if self.boundary != other.boundary:
            return False
        return math.always_close(self.values, other.values)

    def __hash__(self):
        return hash((self.geometry, self.boundary))

    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: 'Field', operator: Callable) -> 'Field':
        """
        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.boundary)
        return self.with_values(values).with_extrapolation(extrapolation_)

    def _op2(self, other, operator) -> 'Field':
        if isinstance(other, Geometry):
            raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}")
        if isinstance(other, Field):
            if self.geometry == other.geometry:
                values = operator(self.values, other.values)
                extrapolation_ = operator(self.boundary, other.extrapolation)
                return Field(self.geometry, values, extrapolation_)
            from ._resample import sample
            other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry)
            values = operator(self.values, other_values)
            boundary = operator(self.boundary, other.extrapolation)
            return Field(self.geometry, values, boundary)
        else:
            if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank:
                other = math.wrap(other, self.geometry.shape['vector'])
            else:
                other = math.wrap(other)
            # try:
            #     boundary = operator(self.boundary, as_boundary(other, self.geometry))
            # except TypeError:  # e.g. ZERO_GRADIENT + constant
            boundary = self.boundary  # constants don't affect the boundary conditions (legacy reasons)
            if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape:
                other = other.vector.as_dual()
            values = operator(self.values, other)
            return Field(self.geometry, values, boundary)

    def __repr__(self):
        if self.is_grid:
            type_name = "Grid" if self.is_centered else "Grid faces"
        elif self.is_mesh:
            type_name = "Mesh" if self.is_centered else "Mesh faces"
        elif self.is_point_cloud:
            type_name = "Point cloud" if self.is_centered else "Point cloud edges"
        elif self.is_graph:
            type_name = "Graph" if self.is_centered else "Graph edges"
        else:
            type_name = self.__class__.__name__
        if self.values is not None:
            return f"{type_name}[{self.values}, ext={self.boundary}]"
        else:
            return f"{type_name}[{self.resolution}, ext={self.boundary}]"

    def grid_scatter(self, *args, **kwargs):
        """Deprecated. Use `sample` with `scatter=True` instead."""
        warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
        from ._resample import grid_scatter
        return grid_scatter(self, *args, **kwargs)

    def as_boundary(self) -> Extrapolation:
        """
        Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
        If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
        If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

        Returns:
            `Extrapolation`
        """
        from ._embed import FieldEmbedding
        return FieldEmbedding(self)

A Field represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the geometry property and the corresponding values are stored as one Tensor in values. The boundary conditions and values outside the geometry are determined by boundary.

Examples

Create a periodic 2D grid, initialized via noise fluctuations.

>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)

Create a field on an unstructured mesh loaded from a .gmsh file

>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-'))
>>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})

Create two cubes and compute a scalar values for each.

>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)

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

Class variables

var boundary : phiml.math.extrapolation.Extrapolation

Boundary conditions describe the values outside of geometry and are used by numerical solvers to compute edge values.

var geometry : phi.geom._geom.Geometry

Discretization Geometry. This determines where in space the values are sampled as well as their relationship and interpretation.

var value_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) are considered values. See phiml.math.magic.PhiTreeNode.__value_attrs__

var values : phiml.math._tensors.Tensor

The sampled values, matching some point set of geometry, e.g. center points, see Geometry.sets.

var variable_attrs : Tuple[str, ...]

Which of the three attributes (geometry,values,boundary) should be traced / optimized. See phiml.math.magic.PhiTreeNode.__variable_attrs__

Instance variables

prop bounds : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop box : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    """
    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`.
    """
    if isinstance(self.geometry.bounds, BaseBox):
        return self.geometry.bounds
    extent = self.geometry.bounding_half_extent().vector.as_dual('_extent')
    points = self.geometry.center + extent
    lower = math.min(points, dim=points.shape.non_batch.non_channel)
    upper = math.max(points, dim=points.shape.non_batch.non_channel)
    return Box(lower, upper)

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.

prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self.geometry, (UniformGrid, Mesh))
    return self.geometry
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    """ Returns the center points of the `elements` of this `Field`. """
    all_points = self.geometry.get_points(self.sampled_at)
    boundary = self.geometry.get_boundary(self.sampled_at)
    return slice_off_constant_faces(all_points, boundary, self.extrapolation)

Returns the center points of the elements of this Field.

prop data : phiml.math._tensors.Tensor
Expand source code
@property
def data(self) -> Tensor:
    return self.values
prop dx : phiml.math._tensors.Tensor
Expand source code
@property
def dx(self) -> Tensor:
    assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}"
    return self.bounds.size / self.resolution
prop elements
Expand source code
@property
def elements(self):
    # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.")
    warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2)
    return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self.boundary

Returns the Extrapolation of this Field.

prop face_areas
Expand source code
@property
def face_areas(self):
    return self.geometry.face_areas
    # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
Expand source code
@property
def face_centers(self):
    return self.geometry.face_centers
    # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
Expand source code
@property
def face_normals(self):
    return self.geometry.face_normals
    # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
Expand source code
@property
def faces(self):
    return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
Expand source code
@property
def graph(self) -> Graph:
    """Cast `self.geometry` to a `phi.geom.Graph`."""
    assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Graph.

prop grid : phi.geom._grid.UniformGrid
Expand source code
@property
def grid(self) -> UniformGrid:
    """Cast `self.geometry` to a `phi.geom.UniformGrid`."""
    assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a UniformGrid.

prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph
Expand source code
@property
def is_graph(self):
    """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance."""
    return isinstance(self.geometry, Graph)

A Field represents graph data if its geometry is a Graph instance.

prop is_grid
Expand source code
@property
def is_grid(self):
    """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance."""
    return isinstance(self.geometry, UniformGrid)

A Field represents grid data if its geometry is a UniformGrid instance.

prop is_mesh
Expand source code
@property
def is_mesh(self):
    """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance."""
    return isinstance(self.geometry, Mesh)

A Field represents mesh data if its geometry is a Mesh instance.

prop is_point_cloud
Expand source code
@property
def is_point_cloud(self):
    """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects."""
    if isinstance(self.geometry, (UniformGrid, Mesh, Graph)):
        return False
    if isinstance(self.geometry, (BaseBox, Sphere, Point)):
        return True
    return True

A Field represents graph data if its geometry is not a set of connected elements, but rather individual geometric objects.

prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
Expand source code
@property
def mesh(self) -> Mesh:
    """Cast `self.geometry` to a `phi.geom.Mesh`."""
    assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}"
    return self.geometry

Cast self.geometry to a Mesh.

prop points
Expand source code
@property
def points(self):
    return self.center
prop resolution
Expand source code
@property
def resolution(self):
    return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
Expand source code
@property
def sampled_at(self):
    matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
Expand source code
@property
def sampled_elements(self) -> Geometry:
    """
    If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`.
    If the values are sampled at the faces, returns `self.faces`.
    """
    return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry

If the values represent are sampled at the element centers or represent the whole element, returns self.geometry. If the values are sampled at the faces, returns self.faces.

prop shape : phiml.math._shape.Shape
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
    """
    if self.is_grid and '~vector' in self.values.shape:
        return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector']
    set_shape = self.geometry.sets[self.sampled_at]
    return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values

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
prop spatial_rank : int
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`.
    """
    return self.geometry.spatial_rank

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

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
Expand source code
def as_boundary(self) -> Extrapolation:
    """
    Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary.
    If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries.
    If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values.

    Returns:
        `Extrapolation`
    """
    from ._embed import FieldEmbedding
    return FieldEmbedding(self)

Returns an Extrapolation representing this 'Field''s values as a Dirichlet (constant) boundary. If this Field encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this Field's sampled domain are required, this Field's boundary conditions will be applied to determine the boundary values.

Returns

Extrapolation

def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud.
    This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_spheres()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Point` geometry.
    """
    points = self.sampled_elements.center
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
    return Field(Point(points), values, self.boundary)

Returns this field as a PointCloud. This replaces the Field.geometry with a Point instance while leaving the sample points unchanged.

See Also: Field.as_spheres().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Point geometry.

def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field':
    """
    Returns this field as a PointCloud with spherical / circular elements, preserving element volumes.
    This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged.

    See Also:
        `Field.as_points()`.

    Args:
        list_dim: If not `None`, packs spatial, instance and dual dims.
            Defaults to `instance('elements')`.

    Returns:
        `Field` with same values and boundaries but `Sphere` geometry.
    """
    points = self.sampled_elements.center
    volumes = self.sampled_elements.volume
    values = self.values
    if list_dim:
        dims = non_batch(points).non_channel & non_batch(points).non_channel
        points = pack_dims(points, dims, list_dim)
        values = pack_dims(values, dims, list_dim)
        volumes = pack_dims(volumes, dims, list_dim)
    return Field(Sphere(points, volume=volumes), values, self.boundary)

Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the Field.geometry with a Sphere instance while leaving the sample points unchanged.

See Also: Field.as_points().

Args

list_dim
If not None, packs spatial, instance and dual dims. Defaults to instance('elements').

Returns

Field with same values and boundaries but Sphere geometry.

def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field':
    """
    Short for `resample(self, representation)`

    See Also
        `resample()`.

    Returns:
        Field object of same type as `representation`
    """
    from ._resample import resample
    return resample(self, representation, keep_boundary, **kwargs)

Short for resample()(self, representation)

See Also resample().

Returns

Field object of same type as representation

def at_centers(self, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_centers(self, **kwargs) -> 'Field':
    """
    Interpolates the values to the cell centers.

    See Also:
        `Field.at_faces()`, `Field.at()`, `resample`.

    Args:
        **kwargs: Sampling arguments.

    Returns:
        `CenteredGrid` sampled at cell centers.
    """
    if self.is_centered:
        return self
    from ._resample import sample
    values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs)
    return Field(self.geometry, values, self.boundary)

Interpolates the values to the cell centers.

See Also: Field.at_faces(), Field.at(), resample().

Args

**kwargs
Sampling arguments.

Returns

CenteredGrid() sampled at cell centers.

def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field':
    if self.is_staggered and not boundary:
        return self
    boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary
    from ._resample import sample
    values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs)
    return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
Expand source code
def closest_values(self, points: Tensor):
    """
    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.
    """
    warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2)
    if isinstance(points, Geometry):
        points = points.center
    # --- CenteredGrid ---
    local_points = self.box.global_to_local(points) * self.resolution - 0.5
    return math.closest_grid_values(self.values, local_points, self.extrapolation)
    # --- StaggeredGrid ---
    if 'staggered_direction' in points.shape:
        points_ = math.unstack(points, '~vector')
        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'])

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.

def curl(self, at='corner')
Expand source code
def curl(self, at='corner'):
    """Alias for `phi.field.curl`"""
    from ._field_math import curl
    return curl(self, at=at)

Alias for curl()

def dimension(self, name: str)
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)

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

def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None):
    """Alias for `phi.field.divergence`"""
    from ._field_math import divergence
    return divergence(self, order=order, implicit=implicit, upwind=upwind)

Alias for divergence()

def downsample(self, factor: int)
Expand source code
def downsample(self, factor: int):
    from ._field_math import downsample2x
    result = self
    while factor >= 2:
        result = downsample2x(result)
        factor /= 2
    if math.close(factor, 1.):
        return result
    from ._resample import resample
    raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)
Expand source code
def gradient(self,
             boundary: Extrapolation = None,
             at: str = 'center',
             dims: math.DimFilter = spatial,
             stack_dim: Union[Shape, str] = channel('vector'),
             order=2,
             implicit: Solve = None,
             scheme=None,
             upwind: 'Field' = None,
             gradient_extrapolation: Extrapolation = None):
    """Alias for `phi.field.spatial_gradient`"""
    from ._field_math import spatial_gradient
    return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
def grid_scatter(self, *args, **kwargs)
Expand source code
def grid_scatter(self, *args, **kwargs):
    """Deprecated. Use `sample` with `scatter=True` instead."""
    warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2)
    from ._resample import grid_scatter
    return grid_scatter(self, *args, **kwargs)

Deprecated. Use sample() with scatter=True instead.

def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)
Expand source code
def laplace(self,
            axes: DimFilter = spatial,
            gradient: 'Field' = None,
            order=2,
            implicit: math.Solve = None,
            weights: Union[Tensor, 'Field'] = None,
            upwind: 'Field' = None,
            correct_skew=True):
    """Alias for `phi.field.laplace`"""
    from ._field_math import laplace
    return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)

Alias for laplace()

def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)
Expand source code
def numpy(self, order: DimFilter = None):
    """
    Return the field values as `NumPy` array(s).

    Args:
        order: Dimension order as `str` or `Shape`.

    Returns:
        A single NumPy array for uniform values, else a list of NumPy arrays.
    """
    if order is None and self.is_grid:
        axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True)
        order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel)
    if self.values.shape.is_uniform:
        return self.values.numpy(order)
    else:
        assert order is not None, f"order must be specified for non-uniform Field values"
        order = self.values.shape.only(order, reorder=True)
        stack_dims = order.non_uniform_shape
        inner_order = order.without(stack_dims)
        return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]

Return the field values as NumPy array(s).

Args

order
Dimension order as str or Shape.

Returns

A single NumPy array for uniform values, else a list of NumPy arrays.

def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field':
    """
    Alias for `phi.field.pad()`.

    Pads this `Field` using its extrapolation.

    Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`.

    Args:
        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:
        Padded `Field`
    """
    from ._field_math import pad
    return pad(self, widths)

Alias for pad().

Pads this Field using its extrapolation.

Unlike padding the values, this function also affects the geometry of the field, changing its size and origin depending on widths.

Args

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

Padded Field

def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor':
    """
    Sample the values of this `Field` at the given location or geometry.

    Args:
        where: Location `Tensor` or `Geometry` or
        at: `'center'` or `'face'`.
        **kwargs: Sampling arguments.

    Returns:
        `Tensor`
    """
    from ._resample import sample
    return sample(self, where, at, **kwargs)

Sample the values of this Field at the given location or geometry.

Args

where
Location Tensor or Geometry or
at
'center' or 'face'.
**kwargs
Sampling arguments.

Returns

Tensor

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted(self, delta: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` by `delta`.

    See Also:
        `Field.shifted_to`.

    Args:
        delta: Shift amount for each center position of `geometry`.

    Returns:
        New `Field` sampled at `geometry.center + delta`.
    """
    return self.with_geometry(self.geometry.shifted(delta))

Move the positions of this field's geometry by delta.

See Also: Field.shifted_to().

Args

delta
Shift amount for each center position of geometry.

Returns

New Field sampled at geometry.center + delta.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
Expand source code
def shifted_to(self, position: Tensor) -> 'Field':
    """
    Move the positions of this field's `geometry` to `positions`.

    See Also:
        `Field.shifted`.

    Args:
        position: New center positions of `geometry`.

    Returns:
        New `Field` sampled at given positions.
    """
    return self.with_geometry(self.geometry.at(position))

Move the positions of this field's geometry to positions.

See Also: Field.shifted().

Args

position
New center positions of geometry.

Returns

New Field sampled at given positions.

def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
Expand source code
def staggered_tensor(self) -> Tensor:
    """
    Stacks all component grids into a single uniform `phi.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 `phi.math.Tensor`.
    """
    assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution"
    padded = []
    for dim, component in zip(self.resolution.names, self.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.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds))
    result = math.stack(padded, channel(vector=self.resolution))
    assert result.shape.is_uniform
    return result

Stacks all component grids into a single uniform phi.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 phi.math.Tensor.

def to_grid(self, resolution=(), bounds=None, **resolution_)
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_):
    resolution = resolution.spatial & spatial(**resolution_)
    if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds):
        return self
    bounds = self.bounds if bounds is None else bounds
    if not resolution:
        half_sizes = self.geometry.bounding_half_extent()
        if (half_sizes > 0).all:
            size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel)
        else:
            cell_count = non_batch(self.geometry).non_channel.non_dual.volume
            size = (bounds.volume / cell_count) ** (1 / self.spatial_rank)
        res = math.maximum(1, math.round(bounds.size / size))
        resolution = spatial(**res.vector)
    return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
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()`.
    """
    if self.values.shape.is_uniform:
        return self.values
    else:
        return self.staggered_tensor()

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

def with_boundary(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)
Expand source code
def with_bounds(self, bounds: Box):
    """ Returns a copy of this field with `bounds` replaced. """
    order = list(bounds.vector.item_names)
    geometry = self.geometry.vector[order]
    new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True)
    values = math.swap_axes(self.values, new_shape)
    return Field(geometry, values, self.boundary)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)
Expand source code
def with_boundary(self, boundary):
    """ Returns a copy of this field with the `boundary` replaced. """
    boundary = as_boundary(boundary, self.geometry)
    boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements'
    old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)}
    new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)}
    if old_determined_slices.values() == new_determined_slices.values():
        return Field(self.geometry, self.values, boundary)  # ToDo unnecessary once the rest is implemented
    to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()}
    to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()]
    values = math.slice_off(self.values, *to_remove)
    if to_add:
        if self.is_mesh:
            values = self.mesh.pad_boundary(values, to_add, self.boundary)
        elif self.is_grid and self.is_staggered:
            values = self.values.vector.dual.as_channel()
            to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()}
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
            values = values.vector.as_dual()
        else:
            values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds)
    return Field(self.geometry, values, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)
Expand source code
def with_geometry(self, elements: Geometry):
    """ Returns a copy of this field with `elements` replaced. """
    assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}"
    return Field(elements, self.values, self.boundary)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)
Expand source code
def with_values(self, values, **sampling_kwargs):
    """ Returns a copy of this field with `values` replaced. """
    if not isinstance(values, (Tensor, Number)):
        from ._resample import sample
        values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs)
    else:
        if not spatial(values):
            geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape
            if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values):
                values = values.vector.as_dual()
            values = expand(wrap(values), geo_shape.non_batch.non_channel)
    return Field(self.geometry, values, self.boundary)

Returns a copy of this field with values replaced.

class HardGeometryMask (geometry: phi.geom._geom.Geometry)
Expand source code
class HardGeometryMask(FieldInitializer):
    """
    Deprecated since version 2.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """
    def __init__(self, geometry: Geometry):
        self.geometry = geometry
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)

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

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

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

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

Ancestors

  • phi.field._field.FieldInitializer

Subclasses

  • phi.field._mask.SoftGeometryMask

Instance variables

prop shape
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)
Expand source code
class Noise(FieldInitializer):
    """
    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))

    def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> Tensor:
        if isinstance(geometry, UniformGrid):
            if at == 'center':
                return self.grid_sample(geometry.resolution, geometry.grid_size)
            elif at == 'face':
                result = {dim: self.grid_sample(grid.resolution, grid.grid_size) for dim, grid in geometry.staggered_cells(boundaries).items()}
                return vec(geometry.face_shape.dual, **result)
        raise NotImplementedError(f"{type(geometry)} not supported. Only UniformGrid 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.name == 'vector' and 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, size) * resolution * 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 __repr__(self):
        return f"{self._shape}, scale={self.scale}, smoothness={self.smoothness}"

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.

Ancestors

  • phi.field._field.FieldInitializer

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.name == 'vector' and 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, size) * resolution * 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 Scene
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.wrap(math.map(lambda d: expanduser(d), 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 = wrap(directory)
        else:
            id = math.wrap(id)
            paths = wrap(math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id))
        # test all exist
        for path in math.flatten(wrap(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)
        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: Field, name: str, frame: int):
        """
        Write a `Field` 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, Field):
            raise ValueError(f"Only Field instances can be saved but got {field}")
        name = _slugify_filename(name)
        files = wrap(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) -> Field:
        """
        Reads a single `Field` 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:
            `Field`
        """
        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)

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

Static methods

def at(directory: str | tuple | list | phiml.math._tensors.Tensor | ForwardRef('Scene'),
id: int | phiml.math._tensors.Tensor | None = None) ‑> phi.field._scene.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.wrap(math.map(lambda d: expanduser(d), 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 = wrap(directory)
    else:
        id = math.wrap(id)
        paths = wrap(math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id))
    # test all exist
    for path in math.flatten(wrap(paths), flatten_batch=True):
        if not isdir(path):
            raise IOError(f"There is no scene at '{path}'")
    return Scene(paths)

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.

def create(parent_directory: str,
shape: phiml.math._shape.Shape = (),
name='sim',
copy_calling_script=True,
**dimensions) ‑> phi.field._scene.Scene
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

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

def list(parent_directory: str,
name='sim',
include_other: bool = False,
dim: phiml.math._shape.Shape | None = None) ‑> phi.field._scene.Scene | tuple
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)

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.

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

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

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

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

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

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

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

prop is_batch
Expand source code
@property
def is_batch(self):
    return self._paths.rank > 0
prop path : str
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()

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

prop paths : phiml.math._tensors.Tensor
Expand source code
@property
def paths(self) -> math.Tensor:
    return self._paths
prop properties
Expand source code
@property
def properties(self):
    self._init_properties()
    return self._properties
prop 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)
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)

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

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

def exists_config(self)
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)

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

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

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

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.

def read_array(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.Field
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> Field:
    """
    Reads a single `Field` 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:
        `Field`
    """
    name = _slugify_filename(name)
    files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
    return read(files, convert_to_backend=convert_to_backend)

Reads a single Field 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

Field

def read_field(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.Field
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> Field:
    """
    Reads a single `Field` 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:
        `Field`
    """
    name = _slugify_filename(name)
    files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths)
    return read(files, convert_to_backend=convert_to_backend)

Reads a single Field 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

Field

def remove(self)
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)

Deletes the scene directory and all contained files.

def rename(self, name: str)
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)

Deletes the scene directory and all contained files.

def subpath(self, name: str, create=False, create_parent=False) ‑> str | tuple
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)
    return result

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 write(self, data: dict = None, frame=0, **kw_data)
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)

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.
def write_field(self, field: phi.field._field.Field, name: str, frame: int)
Expand source code
def write_field(self, field: Field, name: str, frame: int):
    """
    Write a `Field` 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, Field):
        raise ValueError(f"Only Field instances can be saved but got {field}")
    name = _slugify_filename(name)
    files = wrap(math.map(lambda dir_: _filename(dir_, name, frame), self._paths))
    write(field, files)

Write a Field 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.
class GeometryMask (geometry: phi.geom._geom.Geometry,
balance: phiml.math._tensors.Tensor | float = 0.5)
Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 2.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, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor:
        return self.geometry.approximate_fraction_inside(geometry, self.balance)

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

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

Ancestors

  • phi.field._mask.HardGeometryMask
  • phi.field._field.FieldInitializer
class SoftGeometryMask (geometry: phi.geom._geom.Geometry,
balance: phiml.math._tensors.Tensor | float = 0.5)
Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 2.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, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor:
        return self.geometry.approximate_fraction_inside(geometry, self.balance)

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

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

Ancestors

  • phi.field._mask.HardGeometryMask
  • phi.field._field.FieldInitializer