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

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: Union[phiml.math._tensors.Tensor, phi.geom._geom.Geometry], values: Any = 1.0, extrapolation: Union[phiml.math.extrapolation.Extrapolation, float] = 0.0, bounds: phi.geom._box.Box = None) ‑> phi.field._field.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 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

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

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: Union[phiml.math.extrapolation.Extrapolation, phiml.math._tensors.Tensor, float, phi.field._field.Field, ForwardRef(None)]) ‑> phiml.math.extrapolation.Extrapolation

Returns an Extrapolation representing obj.

Args

obj

One of

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

Returns

Extrapolation

def assert_close(*fields: phi.field._field.Field, rel_tolerance: float = 1e-05, abs_tolerance: float = 0, msg: str = '', verbose: bool = True)

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

def bake_extrapolation(grid: phi.field._field.Field) ‑> phi.field._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.

def cast(x: ~MagicType, dtype: Union[phiml.backend._dtype.DType, type]) ‑> ~OtherMagicType

Casts x to a different data type.

Implementations:

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

Args

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

Returns

Tensor with data type dtype

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

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

def center_of_mass(density: phi.field._field.Field)

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

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)

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

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

def curl(field: phi.field._field.Field, at='corner')

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 0x7ff7ac40eca0>

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

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

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

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

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

See phi.math.fourier_laplace()

def fourier_poisson(grid: phi.field._field.Field, times=1) ‑> phi.field._field.Field

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

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

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

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

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

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

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

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

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

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)

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: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function non_batch>) ‑> phiml.math._tensors.Tensor

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

Args

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

Returns

loss
Tensor
def l2_loss(x, reduce: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function non_batch>) ‑> phiml.math._tensors.Tensor

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

Args

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

Returns

loss
Tensor
def laplace(u: phi.field._field.Field, axes: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, gradient: phi.field._field.Field = None, order=2, implicit: phiml.math._optimize.Solve = None, implicitness: int = None, weights: Union[phiml.math._tensors.Tensor, phi.field._field.Field] = None, upwind: phi.field._field.Field = None, correct_skew=True) ‑> phi.field._field.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 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

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)

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

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

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)

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) ‑> Union[phiml.math._tensors.Tensor, phi.field._field.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)

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

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: Union[int, tuple, list, dict]) ‑> phi.field._field.Field

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: Union[str, phiml.math._tensors.Tensor], convert_to_backend=True) ‑> phi.field._field.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.

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.

def reduce_sample(field: Union[phi.field._field.Field, phi.geom._geom.Geometry, phi.field._field.FieldInitializer, Callable], geometry: phi.geom._geom.Geometry, **kwargs) ‑> phiml.math._tensors.Tensor

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

def resample(value: Union[phi.field._field.Field, phi.geom._geom.Geometry, phiml.math._tensors.Tensor, float, phi.field._field.FieldInitializer], to: Union[phi.field._field.Field, phi.geom._geom.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.

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

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

def safe_mul(x, y)

See phiml.math.safe_mul()

def sample(field: Union[phi.field._field.Field, phi.geom._geom.Geometry, phi.field._field.FieldInitializer, Callable], geometry: phi.geom._geom.Geometry, at: str = 'center', boundary: Union[phiml.math.extrapolation.Extrapolation, phiml.math._tensors.Tensor, numbers.Number] = None, dot_face_normal: Optional[phi.geom._geom.Geometry] = None, **kwargs) ‑> phiml.math._tensors.Tensor

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

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

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

See Also: 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: Optional[phiml.math._shape.Shape] = (shiftᶜ=None), dims=<function spatial>, pad=True)

Wraps :func:math.shift for CenteredGrid.

Args

grid
CenteredGrid:
offsets
tuple:
stack_dim
(Default value = 'shift')
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.

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

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

def solve_linear(f: Union[Callable[[~X], ~Y], phiml.math._tensors.Tensor], y: ~Y, solve: phiml.math._optimize.Solve[~X, ~Y], *f_args, grad_for_f=False, f_kwargs: dict = None, **f_kwargs_) ‑> ~X

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

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

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: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, stack_dim: Union[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)

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

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)

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

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)

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

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

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

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

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

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

def unstack(value, dim: Union[str, tuple, list, set, ForwardRef('Shape'), Callable]) ‑> 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)
def upsample2x(grid: phi.field._field.Field) ‑> phi.field._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

Field of same type as grid.

def vec_abs(field: phi.field._field.Field)

See phi.math.vec_abs()

def vec_length(field: phi.field._field.Field)

See phi.math.vec_abs()

def vec_squared(field: phi.field._field.Field)

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

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: Union[str, phiml.math._tensors.Tensor])

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

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

See Also: read()

Args

field
Field to be saved.
file
Single file as str or Tensor of string type. If file is a tensor, the dimensions of field are matched to the dimensions of file. Dimensions of file that are missing in field result in data duplication. Dimensions of field that are missing in file result in larger files.

Classes

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

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

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

Expand source code
class AngularVelocity(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 = math.cross_product(strength, distances)
        velocity = math.sum(velocity, self.location.shape.batch.without(points.shape))
        return velocity

Ancestors

  • phi.field._field.FieldInitializer
class Field (geometry: Union[phiml.math._tensors.Tensor, phi.geom._geom.Geometry], values: Union[phiml.math._tensors.Tensor, numbers.Number, bool, Callable, phi.field._field.FieldInitializer, phi.geom._geom.Geometry, ForwardRef('Field')], boundary: Union[numbers.Number, phiml.math.extrapolation.Extrapolation, ForwardRef('Field'), dict] = 0, **sampling_kwargs)

Base class for all fields.

Important implementations:

  • CenteredGrid
  • StaggeredGrid
  • PointCloud
  • Noise

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class Field:
    """
    Base class for all fields.
    
    Important implementations:
    
    * CenteredGrid
    * StaggeredGrid
    * PointCloud
    * Noise
    
    See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 geometry: Union[Geometry, Tensor],
                 values: Union[Tensor, Number, bool, Callable, FieldInitializer, Geometry, 'Field'],
                 boundary: Union[Number, Extrapolation, 'Field', dict] = 0,
                 **sampling_kwargs):
        """
        Args:
          elements: Geometry object specifying the sample points and sizes
          values: values corresponding to elements
          extrapolation: values outside elements
        """
        assert isinstance(geometry, Geometry), f"geometry must be a Geometry object but got {type(geometry).__name__}"
        self._boundary: Extrapolation = as_boundary(boundary, geometry)
        self._geometry: Geometry = geometry
        if isinstance(values, (Tensor, Number, bool)):
            values = wrap(values)
        else:
            from ._resample import sample
            values = sample(values, geometry, 'center', self._boundary, **sampling_kwargs)
        matching_sets = [s for s, s_shape in geometry.sets.items() if s_shape in values.shape]
        if not matching_sets:
            values = expand(wrap(values), non_batch(geometry) - 'vector')
        self._values: Tensor = values
        math.merge_shapes(values, non_batch(self.sampled_elements).non_channel)  # shape check

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

    @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 values(self) -> Tensor:
        """ Returns the `values` of this `Field`. """
        return self._values

    data = 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 boundary(self) -> Extrapolation:
        """
        Returns the boundary conditions set for this `Field`.

        Returns:
            Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
        """
        return self._boundary

    @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 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.transpose(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, self.resolution)
        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 __value_attrs__(self):
        return '_values',

    def __variable_attrs__(self):
        return '_values', '_geometry', '_boundary'

    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)

Subclasses

  • phi.field._mask.HardGeometryMask

Instance variables

prop boundary : phiml.math.extrapolation.Extrapolation

Returns the boundary conditions set for this Field.

Returns

Single Extrapolation instance that encodes the (varying) boundary conditions for all boundaries of this field's elements.

Expand source code
@property
def boundary(self) -> Extrapolation:
    """
    Returns the boundary conditions set for this `Field`.

    Returns:
        Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
    """
    return self._boundary
prop bounds : phi.geom._box.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.

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)
prop box : phi.geom._box.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.

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)
prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self._geometry, (UniformGrid, Mesh))
    return self._geometry
prop center : phiml.math._tensors.Tensor

Returns the center points of the elements of this Field.

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)
prop data : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    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

Returns the Extrapolation of this Field.

Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self._boundary
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 geometry : phi.geom._geom.Geometry

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

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

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

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

Cast self.geometry to a 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
prop grid : phi.geom._grid.UniformGrid

Cast self.geometry to a 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
prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph

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

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

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

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

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

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

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

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
prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self._values, self._geometry)
prop mesh : phi.geom._mesh.Mesh

Cast self.geometry to a 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
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 in self._values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.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.

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
prop shape : phiml.math._shape.Shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self) -> Shape:
    """
    Returns a shape with the following properties
    
    * The spatial dimension names match the dimensions of this Field
    * The batch dimensions match the batch dimensions of this Field
    * The channel dimensions match the channels of this Field
    """
    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
prop spatial_rank : int

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

Expand source code
@property
def spatial_rank(self) -> int:
    """
    Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D).
    This is equal to the spatial rank of the `data`.
    """
    return self._geometry.spatial_rank
prop values : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    return self._values

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.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

def as_points(self, list_dim: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Union[ForwardRef('Field'), phi.geom._geom.Geometry], keep_boundary=False, **kwargs) ‑> phi.field._field.Field

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

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
def closest_values(self, points: phiml.math._tensors.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.

def curl(self, at='corner')

Alias for curl()

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

def divergence(self, order=2, implicit: phiml.math._optimize.Solve = None, upwind: Field = None)

Alias for divergence()

def downsample(self, factor: int)
def gradient(self, boundary: phiml.math.extrapolation.Extrapolation = None, at: str = 'center', dims: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, stack_dim: Union[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)
def grid_scatter(self, *args, **kwargs)

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

def laplace(self, axes: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, gradient: Field = None, order=2, implicit: phiml.math._optimize.Solve = None, weights: Union[phiml.math._tensors.Tensor, ForwardRef('Field')] = None, upwind: Field = None, correct_skew=True)

Alias for laplace()

def numpy(self, order: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = 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.

def pad(self, widths: Union[int, tuple, list, dict]) ‑> phi.field._field.Field

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: Union[phi.geom._geom.Geometry, ForwardRef('Field'), phiml.math._tensors.Tensor], at: str = 'center', **kwargs) ‑> phiml.math._tensors.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

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def staggered_tensor(self) ‑> phiml.math._tensors.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.

def to_grid(self, resolution=(), bounds=None, **resolution_)
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().

def with_boundary(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)

Returns a copy of this field with values replaced.

class SampledField (geometry: Union[phiml.math._tensors.Tensor, phi.geom._geom.Geometry], values: Union[phiml.math._tensors.Tensor, numbers.Number, bool, Callable, phi.field._field.FieldInitializer, phi.geom._geom.Geometry, ForwardRef('Field')], boundary: Union[numbers.Number, phiml.math.extrapolation.Extrapolation, ForwardRef('Field'), dict] = 0, **sampling_kwargs)

Base class for all fields.

Important implementations:

  • CenteredGrid
  • StaggeredGrid
  • PointCloud
  • Noise

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class Field:
    """
    Base class for all fields.
    
    Important implementations:
    
    * CenteredGrid
    * StaggeredGrid
    * PointCloud
    * Noise
    
    See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 geometry: Union[Geometry, Tensor],
                 values: Union[Tensor, Number, bool, Callable, FieldInitializer, Geometry, 'Field'],
                 boundary: Union[Number, Extrapolation, 'Field', dict] = 0,
                 **sampling_kwargs):
        """
        Args:
          elements: Geometry object specifying the sample points and sizes
          values: values corresponding to elements
          extrapolation: values outside elements
        """
        assert isinstance(geometry, Geometry), f"geometry must be a Geometry object but got {type(geometry).__name__}"
        self._boundary: Extrapolation = as_boundary(boundary, geometry)
        self._geometry: Geometry = geometry
        if isinstance(values, (Tensor, Number, bool)):
            values = wrap(values)
        else:
            from ._resample import sample
            values = sample(values, geometry, 'center', self._boundary, **sampling_kwargs)
        matching_sets = [s for s, s_shape in geometry.sets.items() if s_shape in values.shape]
        if not matching_sets:
            values = expand(wrap(values), non_batch(geometry) - 'vector')
        self._values: Tensor = values
        math.merge_shapes(values, non_batch(self.sampled_elements).non_channel)  # shape check

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

    @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 values(self) -> Tensor:
        """ Returns the `values` of this `Field`. """
        return self._values

    data = 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 boundary(self) -> Extrapolation:
        """
        Returns the boundary conditions set for this `Field`.

        Returns:
            Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
        """
        return self._boundary

    @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 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.transpose(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, self.resolution)
        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 __value_attrs__(self):
        return '_values',

    def __variable_attrs__(self):
        return '_values', '_geometry', '_boundary'

    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)

Subclasses

  • phi.field._mask.HardGeometryMask

Instance variables

prop boundary : phiml.math.extrapolation.Extrapolation

Returns the boundary conditions set for this Field.

Returns

Single Extrapolation instance that encodes the (varying) boundary conditions for all boundaries of this field's elements.

Expand source code
@property
def boundary(self) -> Extrapolation:
    """
    Returns the boundary conditions set for this `Field`.

    Returns:
        Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
    """
    return self._boundary
prop bounds : phi.geom._box.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.

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)
prop box : phi.geom._box.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.

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)
prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self._geometry, (UniformGrid, Mesh))
    return self._geometry
prop center : phiml.math._tensors.Tensor

Returns the center points of the elements of this Field.

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)
prop data : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    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

Returns the Extrapolation of this Field.

Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self._boundary
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 geometry : phi.geom._geom.Geometry

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

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

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

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

Cast self.geometry to a 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
prop grid : phi.geom._grid.UniformGrid

Cast self.geometry to a 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
prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph

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

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

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

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

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

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

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

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
prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self._values, self._geometry)
prop mesh : phi.geom._mesh.Mesh

Cast self.geometry to a 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
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 in self._values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.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.

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
prop shape : phiml.math._shape.Shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self) -> Shape:
    """
    Returns a shape with the following properties
    
    * The spatial dimension names match the dimensions of this Field
    * The batch dimensions match the batch dimensions of this Field
    * The channel dimensions match the channels of this Field
    """
    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
prop spatial_rank : int

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

Expand source code
@property
def spatial_rank(self) -> int:
    """
    Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D).
    This is equal to the spatial rank of the `data`.
    """
    return self._geometry.spatial_rank
prop values : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    return self._values

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.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

def as_points(self, list_dim: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Union[ForwardRef('Field'), phi.geom._geom.Geometry], keep_boundary=False, **kwargs) ‑> phi.field._field.Field

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

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
def closest_values(self, points: phiml.math._tensors.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.

def curl(self, at='corner')

Alias for curl()

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

def divergence(self, order=2, implicit: phiml.math._optimize.Solve = None, upwind: Field = None)

Alias for divergence()

def downsample(self, factor: int)
def gradient(self, boundary: phiml.math.extrapolation.Extrapolation = None, at: str = 'center', dims: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, stack_dim: Union[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)
def grid_scatter(self, *args, **kwargs)

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

def laplace(self, axes: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, gradient: Field = None, order=2, implicit: phiml.math._optimize.Solve = None, weights: Union[phiml.math._tensors.Tensor, ForwardRef('Field')] = None, upwind: Field = None, correct_skew=True)

Alias for laplace()

def numpy(self, order: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = 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.

def pad(self, widths: Union[int, tuple, list, dict]) ‑> phi.field._field.Field

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: Union[phi.geom._geom.Geometry, ForwardRef('Field'), phiml.math._tensors.Tensor], at: str = 'center', **kwargs) ‑> phiml.math._tensors.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

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def staggered_tensor(self) ‑> phiml.math._tensors.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.

def to_grid(self, resolution=(), bounds=None, **resolution_)
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().

def with_boundary(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)

Returns a copy of this field with values replaced.

class Grid

Base class for all fields.

Important implementations:

  • CenteredGrid
  • StaggeredGrid
  • PointCloud
  • Noise

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class Field:
    """
    Base class for all fields.
    
    Important implementations:
    
    * CenteredGrid
    * StaggeredGrid
    * PointCloud
    * Noise
    
    See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
    """

    def __init__(self,
                 geometry: Union[Geometry, Tensor],
                 values: Union[Tensor, Number, bool, Callable, FieldInitializer, Geometry, 'Field'],
                 boundary: Union[Number, Extrapolation, 'Field', dict] = 0,
                 **sampling_kwargs):
        """
        Args:
          elements: Geometry object specifying the sample points and sizes
          values: values corresponding to elements
          extrapolation: values outside elements
        """
        assert isinstance(geometry, Geometry), f"geometry must be a Geometry object but got {type(geometry).__name__}"
        self._boundary: Extrapolation = as_boundary(boundary, geometry)
        self._geometry: Geometry = geometry
        if isinstance(values, (Tensor, Number, bool)):
            values = wrap(values)
        else:
            from ._resample import sample
            values = sample(values, geometry, 'center', self._boundary, **sampling_kwargs)
        matching_sets = [s for s, s_shape in geometry.sets.items() if s_shape in values.shape]
        if not matching_sets:
            values = expand(wrap(values), non_batch(geometry) - 'vector')
        self._values: Tensor = values
        math.merge_shapes(values, non_batch(self.sampled_elements).non_channel)  # shape check

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

    @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 values(self) -> Tensor:
        """ Returns the `values` of this `Field`. """
        return self._values

    data = 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 boundary(self) -> Extrapolation:
        """
        Returns the boundary conditions set for this `Field`.

        Returns:
            Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
        """
        return self._boundary

    @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 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.transpose(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, self.resolution)
        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 __value_attrs__(self):
        return '_values',

    def __variable_attrs__(self):
        return '_values', '_geometry', '_boundary'

    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)

Subclasses

  • phi.field._mask.HardGeometryMask

Instance variables

prop boundary : phiml.math.extrapolation.Extrapolation

Returns the boundary conditions set for this Field.

Returns

Single Extrapolation instance that encodes the (varying) boundary conditions for all boundaries of this field's elements.

Expand source code
@property
def boundary(self) -> Extrapolation:
    """
    Returns the boundary conditions set for this `Field`.

    Returns:
        Single `Extrapolation` instance that encodes the (varying) boundary conditions for all boundaries of this field's `elements`.
    """
    return self._boundary
prop bounds : phi.geom._box.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.

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)
prop box : phi.geom._box.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.

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)
prop cells
Expand source code
@property
def cells(self):
    assert isinstance(self._geometry, (UniformGrid, Mesh))
    return self._geometry
prop center : phiml.math._tensors.Tensor

Returns the center points of the elements of this Field.

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)
prop data : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    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

Returns the Extrapolation of this Field.

Expand source code
@property
def extrapolation(self) -> Extrapolation:
    """ Returns the `Extrapolation` of this `Field`. """
    return self._boundary
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 geometry : phi.geom._geom.Geometry

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

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

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

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

Cast self.geometry to a 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
prop grid : phi.geom._grid.UniformGrid

Cast self.geometry to a 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
prop is_centered
Expand source code
@property
def is_centered(self):
    return not self.is_staggered
prop is_graph

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

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

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

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

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

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

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

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
prop is_staggered
Expand source code
@property
def is_staggered(self):
    return is_staggered(self._values, self._geometry)
prop mesh : phi.geom._mesh.Mesh

Cast self.geometry to a 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
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 in self._values.shape]
    return matching_sets[-1]
prop sampled_elements : phi.geom._geom.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.

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
prop shape : phiml.math._shape.Shape

Returns a shape with the following properties

  • The spatial dimension names match the dimensions of this Field
  • The batch dimensions match the batch dimensions of this Field
  • The channel dimensions match the channels of this Field
Expand source code
@property
def shape(self) -> Shape:
    """
    Returns a shape with the following properties
    
    * The spatial dimension names match the dimensions of this Field
    * The batch dimensions match the batch dimensions of this Field
    * The channel dimensions match the channels of this Field
    """
    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
prop spatial_rank : int

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

Expand source code
@property
def spatial_rank(self) -> int:
    """
    Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D).
    This is equal to the spatial rank of the `data`.
    """
    return self._geometry.spatial_rank
prop values : phiml.math._tensors.Tensor

Returns the values of this Field.

Expand source code
@property
def values(self) -> Tensor:
    """ Returns the `values` of this `Field`. """
    return self._values

Methods

def as_boundary(self) ‑> phiml.math.extrapolation.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

def as_points(self, list_dim: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Optional[phiml.math._shape.Shape] = (elementsⁱ=None)) ‑> phi.field._field.Field

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: Union[ForwardRef('Field'), phi.geom._geom.Geometry], keep_boundary=False, **kwargs) ‑> phi.field._field.Field

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

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
def closest_values(self, points: phiml.math._tensors.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.

def curl(self, at='corner')

Alias for curl()

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

def divergence(self, order=2, implicit: phiml.math._optimize.Solve = None, upwind: Field = None)

Alias for divergence()

def downsample(self, factor: int)
def gradient(self, boundary: phiml.math.extrapolation.Extrapolation = None, at: str = 'center', dims: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, stack_dim: Union[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)
def grid_scatter(self, *args, **kwargs)

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

def laplace(self, axes: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = <function spatial>, gradient: Field = None, order=2, implicit: phiml.math._optimize.Solve = None, weights: Union[phiml.math._tensors.Tensor, ForwardRef('Field')] = None, upwind: Field = None, correct_skew=True)

Alias for laplace()

def numpy(self, order: Union[str, tuple, list, set, ForwardRef('Shape'), Callable] = 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.

def pad(self, widths: Union[int, tuple, list, dict]) ‑> phi.field._field.Field

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: Union[phi.geom._geom.Geometry, ForwardRef('Field'), phiml.math._tensors.Tensor], at: str = 'center', **kwargs) ‑> phiml.math._tensors.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

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.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.

def staggered_tensor(self) ‑> phiml.math._tensors.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.

def to_grid(self, resolution=(), bounds=None, **resolution_)
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().

def with_boundary(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_bounds(self, bounds: phi.geom._box.Box)

Returns a copy of this field with bounds replaced.

def with_elements(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_extrapolation(self, boundary)

Returns a copy of this field with the boundary replaced.

def with_geometry(self, elements: phi.geom._geom.Geometry)

Returns a copy of this field with elements replaced.

def with_values(self, values, **sampling_kwargs)

Returns a copy of this field with values replaced.

class HardGeometryMask (geometry: phi.geom._geom.Geometry)

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class HardGeometryMask(Field):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """

    def __init__(self, geometry: Geometry):
        super().__init__(geometry, 1, 0)
        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, **kwargs) -> Tensor:
        return math.to_float(self.geometry.lies_inside(geometry.center))

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

Ancestors

  • phi.field._field.Field

Subclasses

  • phi.field._mask.SoftGeometryMask

Instance variables

prop shape

Returns a shape with the following properties

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

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

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

Args

shape
Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid.
scale
Size of noise fluctuations in physical units.
smoothness
Determines how quickly high frequencies die out.
**dims
Additional dimensions, added to shape.
Expand source code
class Noise(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}"

Ancestors

  • phi.field._field.FieldInitializer

Methods

def grid_sample(self, resolution: phiml.math._shape.Shape, size, shape: phiml.math._shape.Shape = None)
class Scene

Provides methods for reading and writing simulation data.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Returns:
            `Scene` object for existing scene.
        """
        if isinstance(directory, Scene):
            assert id is None, f"Got id={id} but directory is already a Scene."
            return directory
        if isinstance(directory, (tuple, list)):
            directory = math.wrap(directory, batch('scenes'))
        directory = math.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)

Static methods

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

Creates a Scene for an existing directory.

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

Args

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

Returns

Scene object for existing scene.

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

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

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

Args

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

Returns

Single Scene object representing the new scene(s).

def list(parent_directory: str, name='sim', include_other: bool = False, dim: Optional[phiml.math._shape.Shape] = None) ‑> Union[phi.field._scene.Scene, tuple]

Lists all scenes inside the given directory.

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

Args

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

Returns

tuple of scenes.

def stack(*scenes: Scene, dim: phiml.math._shape.Shape = (batchᵇ=None)) ‑> phi.field._scene.Scene

Instance variables

prop complete_frames

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

See Also: Scene.frames

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

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

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

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

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

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

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

Expand source code
@property
def path(self) -> str:
    """
    Relative path of the scene directory.
    This property only exists for single scenes, not scene batches.
    """
    assert not self.is_batch, "Scene.path is not defined for scene batches."
    return self._paths.native()
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)

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)
def copy_src_text(self, filename, text)
def exist_properties(self)

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

def exists_config(self)

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

def mkdir(self)
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.
def put_property(self, key, value)
def read(self, *names: str, frame=0, convert_to_backend=True)

Reads one or multiple fields from disc.

See Also: Scene.write().

Args

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

Returns

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

def read_array(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.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

def read_field(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.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

def remove(self)

Deletes the scene directory and all contained files.

def rename(self, name: str)

Deletes the scene directory and all contained files.

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

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

See Also: Scene.read().

Args

data
dict mapping field names to Field objects that can be written using write().
kw_data
Additional data, overrides elements in data.
frame
Frame number.
def write_field(self, field: phi.field._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.
class GeometryMask (geometry: phi.geom._geom.Geometry, balance: Union[phiml.math._tensors.Tensor, float] = 0.5)

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """
    def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5):
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)
        super().__init__(geometry)
        self.balance = balance

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

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

Ancestors

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

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

Args

elements
Geometry object specifying the sample points and sizes
values
values corresponding to elements
extrapolation
values outside elements
Expand source code
class SoftGeometryMask(HardGeometryMask):
    """
    Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead.
    """
    def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5):
        warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2)
        super().__init__(geometry)
        self.balance = balance

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

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

Ancestors

  • phi.field._mask.HardGeometryMask
  • phi.field._field.Field