Module phi.field
The fields module provides a number of data structures and functions to represent continuous, spatially varying data.
All fields are subclasses of Field
which provides abstract functions for sampling field values at physical locations.
The most important field types are:
CenteredGrid
embeds a tensor in the physical space. Uses linear interpolation between grid points.StaggeredGrid
samples the vector components at face centers instead of at cell centers.Noise
is a function that produces a procedurally generated noise field
Use grid()
to create a Grid
from data or by sampling another Field
or Geometry
.
Alternatively, the phi.physics.Domain
class provides convenience methods for grid creation.
All fields can be sampled at physical locations or volumes using sample()
or reduce_sample()
.
See the phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
Expand source code
"""
The fields module provides a number of data structures and functions to represent continuous, spatially varying data.
All fields are subclasses of `Field` which provides abstract functions for sampling field values at physical locations.
The most important field types are:
* `CenteredGrid` embeds a tensor in the physical space. Uses linear interpolation between grid points.
* `StaggeredGrid` samples the vector components at face centers instead of at cell centers.
* `Noise` is a function that produces a procedurally generated noise field
Use `grid()` to create a `Grid` from data or by sampling another `Field` or `phi.geom.Geometry`.
Alternatively, the `phi.physics.Domain` class provides convenience methods for grid creation.
All fields can be sampled at physical locations or volumes using `sample()` or `reduce_sample()`.
See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
"""
from ._field import Field, SampledField, sample, reduce_sample, resample, as_extrapolation
from ._mask import HardGeometryMask, SoftGeometryMask as GeometryMask, SoftGeometryMask
from ._grid import Grid, CenteredGrid, StaggeredGrid
from ._point_cloud import PointCloud
from ._noise import Noise
from ._angular_velocity import AngularVelocity
from phi.math import (
abs, sign, round, ceil, floor, sqrt, exp, isfinite, is_finite, real, imag, sin, cos, cast, to_float, to_int32, to_int64, convert,
stop_gradient,
jit_compile, jit_compile_linear, functional_gradient, jacobian, gradient,
solve_linear, solve_nonlinear, minimize,
l2_loss, l1_loss, frequency_loss,
unstack, stack, concat # expand, rename_dims, pack_dims, unpack_dims
)
from ._field_math import (
assert_close,
bake_extrapolation,
laplace, spatial_gradient, divergence, stagger, curl, # spatial operators
fourier_poisson, fourier_laplace,
mean, pad, shift, normalize, center_of_mass,
concat, stack,
where, maximum, minimum,
vec_squared, vec_length as vec_abs, vec_length,
downsample2x, upsample2x,
finite_fill,
native_call,
integrate,
pack_dims,
support, mask,
)
from ._field_io import write, read
from ._scene import Scene
__all__ = [key for key in globals().keys() if not key.startswith('_')]
__pdoc__ = {
'Grid.__init__': False,
'Scene.__init__': False,
}
Functions
def abs(x) ‑> phi.math._tensors.Tensor
-
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
orPhiTreeNode
Returns
Absolute value of
x
of same type asx
.Expand source code
def abs_(x) -> Tensor or PhiTreeNode: """ Computes *||x||<sub>1</sub>*. Complex `x` result in matching precision float values. *Note*: The gradient of this operation is undefined for *x=0*. TensorFlow and PyTorch return 0 while Jax returns 1. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` Returns: Absolute value of `x` of same type as `x`. """ return _backend_op1(x, Backend.abs)
def as_extrapolation(obj: Extrapolation) ‑> Extrapolation
-
Returns an
Extrapolation
representingobj
.Args
obj
-
One of
float
orTensor
: Extrapolate with a constant valueExtrapolation
: Use as-is.Field
: Sample values fromobj
, embedding another field insideobj
.
Returns
Extrapolation
Expand source code
def as_extrapolation(obj: Extrapolation or float or Field or None) -> Extrapolation: """ Returns an `Extrapolation` representing `obj`. Args: obj: One of * `float` or `Tensor`: Extrapolate with a constant value * `Extrapolation`: Use as-is. * `Field`: Sample values from `obj`, embedding another field inside `obj`. Returns: `Extrapolation` """ if isinstance(obj, Field): from ._embed import FieldEmbedding return FieldEmbedding(obj) else: return math.extrapolation.as_extrapolation(obj)
def assert_close(*fields: phi.field._field.SampledField, 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. Seeassert_close()
.Expand source code
def assert_close(*fields: SampledField or Tensor or Number, rel_tolerance: float = 1e-5, abs_tolerance: float = 0, msg: str = "", verbose: bool = True): """ Raises an AssertionError if the `values` of the given fields are not close. See `phi.math.assert_close()`. """ f0 = next(filter(lambda t: isinstance(t, SampledField), fields)) values = [(f @ f0).values if isinstance(f, SampledField) else math.wrap(f) for f in fields] math.assert_close(*values, rel_tolerance=rel_tolerance, abs_tolerance=abs_tolerance, msg=msg, verbose=verbose)
def bake_extrapolation(grid: ~GridType) ‑> ~GridType
-
Pads
grid
with its current extrapolation. ForStaggeredGrid
s, the resulting grid will have a consistent shape, independent of the original extrapolation.Args
grid
CenteredGrid
orStaggeredGrid
.
Returns
Padded grid with extrapolation
NONE
.Expand source code
def bake_extrapolation(grid: GridType) -> GridType: """ Pads `grid` with its current extrapolation. For `StaggeredGrid`s, the resulting grid will have a consistent shape, independent of the original extrapolation. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: Padded grid with extrapolation `phi.math.extrapolation.NONE`. """ if grid.extrapolation == math.extrapolation.NONE: return grid if isinstance(grid, StaggeredGrid): values = grid.values.unstack('vector') padded = [] for dim, value in zip(grid.shape.spatial.names, values): lower, upper = grid.extrapolation.valid_outer_faces(dim) padded.append(math.pad(value, {dim: (0 if lower else 1, 0 if upper else 1)}, grid.extrapolation[{'vector': dim}], bounds=grid.bounds)) return StaggeredGrid(math.stack(padded, grid.shape['vector']), bounds=grid.bounds, extrapolation=math.extrapolation.NONE) elif isinstance(grid, CenteredGrid): return pad(grid, 1).with_extrapolation(math.extrapolation.NONE) else: raise ValueError(f"Not a valid grid: {grid}")
def cast(x: ~MagicType, dtype: phi.math.backend._dtype.DType) ‑> ~OtherMagicType
-
Casts
x
to a different data type.Implementations:
- NumPy:
x.astype()
- PyTorch:
x.to()
- TensorFlow:
tf.cast
- Jax:
jax.numpy.array
See Also:
to_float()
,to_int32()
,to_int64()
,to_complex
.Args
x
Tensor
dtype
- New data type as
DType
, e.g.DType(int, 16)
.
Returns
Tensor
with data typedtype
Expand source code
def cast(x: MagicType, dtype: DType or type) -> OtherMagicType: """ Casts `x` to a different data type. Implementations: * NumPy: [`x.astype()`](numpy.ndarray.astype) * PyTorch: [`x.to()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.to) * TensorFlow: [`tf.cast`](https://www.tensorflow.org/api_docs/python/tf/cast) * Jax: [`jax.numpy.array`](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html) See Also: `to_float`, `to_int32`, `to_int64`, `to_complex`. Args: x: `Tensor` dtype: New data type as `phi.math.DType`, e.g. `DType(int, 16)`. Returns: `Tensor` with data type `dtype` """ if not isinstance(dtype, DType): dtype = DType.as_dtype(dtype) if hasattr(x, '__cast__'): return x.__cast__(dtype) elif isinstance(x, (Number, bool)): return dtype.kind(x) elif isinstance(x, PhiTreeNode): attrs = {key: getattr(x, key) for key in value_attributes(x)} new_attrs = {k: cast(v, dtype) for k, v in attrs.items()} return copy_with(x, **new_attrs) try: backend = choose_backend(x) return backend.cast(x, dtype) except NoBackendFound: if dtype.kind == bool: return bool(x) raise ValueError(f"Cannot cast object of type '{type(x).__name__}'")
- NumPy:
def ceil(x) ‑> phi.math._tensors.Tensor
-
Computes ⌈x⌉ of the
Tensor
orPhiTreeNode
x
.Expand source code
def ceil(x) -> Tensor or PhiTreeNode: """ Computes *⌈x⌉* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.ceil)
def center_of_mass(density: phi.field._field.SampledField)
-
Compute the center of mass of a density field.
Args
density
- Scalar
SampledField
Returns
Tensor
holding only batch dimensions.Expand source code
def center_of_mass(density: SampledField): """ Compute the center of mass of a density field. Args: density: Scalar `SampledField` Returns: `Tensor` holding only batch dimensions. """ assert 'vector' not in density.shape return mean(density.points * density) / mean(density)
def concat(fields: List[~SampledFieldType], dim: str) ‑> ~SampledFieldType
-
Concatenates the given
SampledField
s alongdim
.See Also:
stack()
.Args
fields
- List of matching
SampledField
instances. dim
- Concatenation dimension as
Shape
. Size is ignored.
Returns
SampledField
matching concatenated fields.Expand source code
def concat(fields: List[SampledFieldType] or Tuple[SampledFieldType, ...], dim: str or Shape) -> SampledFieldType: """ Concatenates the given `SampledField`s along `dim`. See Also: `stack()`. Args: fields: List of matching `SampledField` instances. dim: Concatenation dimension as `Shape`. Size is ignored. Returns: `SampledField` matching concatenated fields. """ assert all(isinstance(f, SampledField) for f in fields) assert all(isinstance(f, type(fields[0])) for f in fields) if any(f.extrapolation != fields[0].extrapolation for f in fields): raise NotImplementedError("Concatenating extrapolations not supported") if isinstance(fields[0], Grid): values = math.concat([f.values for f in fields], dim) return fields[0].with_values(values) elif isinstance(fields[0], PointCloud): elements = geom.concat([f.elements for f in fields], dim) values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim) return PointCloud(elements=elements, values=values, extrapolation=fields[0].extrapolation, add_overlapping=fields[0]._add_overlapping, bounds=fields[0]._bounds) raise NotImplementedError(type(fields[0]))
def convert(x, backend: phi.math.backend._backend.Backend = None, use_dlpack=True)
-
Convert the native representation of a
Tensor
orPhiTreeNode
to the native format ofbackend
.Warning: This operation breaks the automatic differentiation chain.
See Also:
convert()
.Args
x
Tensor
to convert. Ifx
is aPhiTreeNode
, its variable attributes are converted.backend
- Target backend. If
None
, uses the current default backend, seedefault_backend()
.
Returns
Tensor
with native representation belonging tobackend
.Expand source code
def convert(x, backend: Backend = None, use_dlpack=True): """ Convert the native representation of a `Tensor` or `phi.math.magic.PhiTreeNode` to the native format of `backend`. *Warning*: This operation breaks the automatic differentiation chain. See Also: `phi.math.backend.convert()`. Args: x: `Tensor` to convert. If `x` is a `phi.math.magic.PhiTreeNode`, its variable attributes are converted. backend: Target backend. If `None`, uses the current default backend, see `phi.math.backend.default_backend()`. Returns: `Tensor` with native representation belonging to `backend`. """ if isinstance(x, Tensor): return x._op1(lambda native: b_convert(native, backend, use_dlpack=use_dlpack)) elif isinstance(x, PhiTreeNode): return copy_with(x, **{a: convert(getattr(x, a), backend, use_dlpack=use_dlpack) for a in variable_attributes(x)}) else: return choose_backend(x).as_tensor(x)
def cos(x) ‑> phi.math._tensors.Tensor
-
Computes cos(x) of the
Tensor
orPhiTreeNode
x
.Expand source code
def cos(x) -> Tensor or PhiTreeNode: """ Computes *cos(x)* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.cos)
def curl(field: phi.field._grid.Grid, type: type = phi.field._grid.CenteredGrid)
-
Computes the finite-difference curl of the give 2D
StaggeredGrid
.Expand source code
def curl(field: Grid, type: type = CenteredGrid): """ Computes the finite-difference curl of the give 2D `StaggeredGrid`. """ assert field.spatial_rank in (2, 3), "curl is only defined in 2 and 3 spatial dimensions." if isinstance(field, CenteredGrid) and field.spatial_rank == 2: if 'vector' not in field.shape and type == StaggeredGrid: # 2D curl of scalar field grad = math.spatial_gradient(field.values, dx=field.dx, difference='forward', padding=None, stack_dim=channel('vector')) result = grad.vector.flip() * (1, -1) # (d/dy, -d/dx) bounds = Box(field.bounds.lower + 0.5 * field.dx, field.bounds.upper - 0.5 * field.dx) # lose 1 cell per dimension return StaggeredGrid(result, bounds=bounds, extrapolation=field.extrapolation.spatial_gradient()) if 'vector' in field.shape and type == CenteredGrid: # 2D curl of vector field x, y = field.shape.spatial.names vy_dx = math.spatial_gradient(field.values.vector[1], dx=field.dx.vector[0], padding=field.extrapolation, dims=x, stack_dim=None) vx_dy = math.spatial_gradient(field.values.vector[0], dx=field.dx.vector[1], padding=field.extrapolation, dims=y, stack_dim=None) c = vy_dx - vx_dy return field.with_values(c) elif isinstance(field, StaggeredGrid) and field.spatial_rank == 2: if type == CenteredGrid: for dim in field.resolution.names: l, u = field.extrapolation.valid_outer_faces(dim) assert l == u, "periodic extrapolation not yet supported" values = bake_extrapolation(field).values x_padded = math.pad(values.vector['x'], {'y': (1, 1)}, field.extrapolation) y_padded = math.pad(values.vector['y'], {'x': (1, 1)}, field.extrapolation) vx_dy = math.spatial_gradient(x_padded, field.dx, 'forward', None, dims='y', stack_dim=None) vy_dx = math.spatial_gradient(y_padded, field.dx, 'forward', None, dims='x', stack_dim=None) result = vy_dx - vx_dy return CenteredGrid(result, field.extrapolation.spatial_gradient(), bounds=field.bounds) raise NotImplementedError()
def divergence(field: phi.field._grid.Grid, order=2, implicit: phi.math._optimize.Solve = None) ‑> phi.field._grid.CenteredGrid
-
Computes the divergence of a grid using finite differences.
This function can operate in two modes depending on the type of
field
:CenteredGrid
approximates the divergence at cell centers using central differencesStaggeredGrid
exactly computes the divergence at cell centers
Args
field
- vector field as
CenteredGrid
orStaggeredGrid
order
- Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
- When a
Solve
object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.
Returns
Divergence field as
CenteredGrid
Expand source code
def divergence(field: Grid, order=2, implicit: Solve = None) -> CenteredGrid: """ Computes the divergence of a grid using finite differences. This function can operate in two modes depending on the type of `field`: * `CenteredGrid` approximates the divergence at cell centers using central differences * `StaggeredGrid` exactly computes the divergence at cell centers Args: field: vector field as `CenteredGrid` or `StaggeredGrid` order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. Returns: Divergence field as `CenteredGrid` """ extrap_map = {} if not implicit: if order == 2: if isinstance(field, CenteredGrid): values, needed_shifts = [-1 / 2, 1 / 2], (-1, 1) else: values, needed_shifts = [-1, 1], (0, 1) elif order == 4: if isinstance(field, CenteredGrid): values, needed_shifts = [1 / 12, -2 / 3, 2 / 3, -1 / 12], (-2, -1, 1, 2) else: values, needed_shifts = [1 / 24, -27 / 24, 27 / 24, -1 / 24], (-1, 0, 1, 2) else: extrap_map_rhs = {} if order == 6: extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC) extrap_map_rhs['symmetric'] = combine_by_direction(ANTIREFLECT, ANTISYMMETRIC) if isinstance(field, CenteredGrid): values, needed_shifts = [-1 / 36, -14 / 18, 14 / 18, 1 / 36], (-2, -1, 1, 2) values_rhs, needed_shifts_rhs = [1 / 3, 1, 1 / 3], (-1, 0, 1) else: values, needed_shifts = [-17 / 186, -63 / 62, 63 / 62, 17 / 186], (-1, 0, 1, 2) values_rhs, needed_shifts_rhs = [9 / 62, 1, 9 / 62], (-1, 0, 1) base_widths = (abs(min(needed_shifts)), max(needed_shifts)) field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation)) # ToDo does this line do anything? spatial_dims = field.shape.spatial.names if isinstance(field, StaggeredGrid): base_widths = (base_widths[0]+1, base_widths[1]) padded_components = [] for dim, component in zip(field.shape.spatial.names, unstack(field, 'vector')): border_valid = field.extrapolation.valid_outer_faces(dim) padding_widths = (base_widths[0] - border_valid[0], base_widths[1] - border_valid[1]) padded_components.append(pad(component, {dim: padding_widths})) elif isinstance(field, CenteredGrid): padded_components = [pad(component, {dim: base_widths}) for dim, component in zip(spatial_dims, unstack(field, 'vector'))] if field.extrapolation == math.extrapolation.NONE: padded_components = [pad(component, {dim_: (0, 0) if dim_ == dim else (-1, -1) for dim_ in spatial_dims}) for dim, component in zip(spatial_dims, padded_components)] shifted_components = [shift(padded_component, needed_shifts, None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, spatial_dims)] result_components = [sum([value * shift for value, shift in zip(values, shifted_component)]) / field.dx.vector[dim] for shifted_component, dim in zip(shifted_components, spatial_dims)] if implicit: result_components = stack(result_components, channel('vector')) result_components.with_values(result_components.values._cache()) implicit.x0 = field result_components = solve_linear(_lhs_for_implicit_scheme, result_components, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=channel('vector')) result_components = unstack(result_components, 'vector') result_components = [component.with_bounds(field.bounds) for component in result_components] result = sum(result_components) if field.extrapolation == math.extrapolation.NONE and isinstance(field, CenteredGrid): result = result.with_bounds(Box(field.bounds.lower + field.dx, field.bounds.upper - field.dx)) return result
def downsample2x(grid: phi.field._grid.Grid) ‑> ~GridType
-
Reduces the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.
See Also:
upsample2x()
.Args
grid
CenteredGrid
orStaggeredGrid
.
Returns
Grid
of same type asgrid
.Expand source code
def downsample2x(grid: Grid) -> GridType: """ Reduces the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation. See Also: `upsample2x()`. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: `Grid` of same type as `grid`. """ if isinstance(grid, CenteredGrid): values = math.downsample2x(grid.values, grid.extrapolation) return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation) elif isinstance(grid, StaggeredGrid): values = [] for dim, centered_grid in zip(grid.shape.spatial.names, unstack(grid, 'vector')): odd_discarded = centered_grid.values[{dim: slice(None, None, 2)}] others_interpolated = math.downsample2x(odd_discarded, grid.extrapolation, dims=grid.shape.spatial.without(dim)) values.append(others_interpolated) return StaggeredGrid(math.stack(values, channel('vector')), bounds=grid.bounds, extrapolation=grid.extrapolation) else: raise ValueError(type(grid))
def exp(x) ‑> phi.math._tensors.Tensor
-
Computes exp(x) of the
Tensor
orPhiTreeNode
x
.Expand source code
def exp(x) -> Tensor or PhiTreeNode: """ Computes *exp(x)* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.exp)
def finite_fill(grid: ~GridType, distance=1, diagonal=True) ‑> ~GridType
-
Extrapolates values of
grid
which are marked by nonzero values invalid
using `phi.math.masked_fill(). Ifvalues
is a StaggeredGrid, its components get extrapolated independently.Args
grid
- Grid holding the values for extrapolation and possible non-finite values to be filled.
distance
- Number of extrapolation steps, i.e. how far a cell can be from the closest finite value to get filled.
diagonal
- Whether to extrapolate values to their diagonal neighbors per step.
Returns
grid
- Grid with extrapolated values.
valid
- binary Grid marking all valid values after extrapolation.
Expand source code
def finite_fill(grid: GridType, distance=1, diagonal=True) -> GridType: """ Extrapolates values of `grid` which are marked by nonzero values in `valid` using `phi.math.masked_fill(). If `values` is a StaggeredGrid, its components get extrapolated independently. Args: grid: Grid holding the values for extrapolation and possible non-finite values to be filled. distance: Number of extrapolation steps, i.e. how far a cell can be from the closest finite value to get filled. diagonal: Whether to extrapolate values to their diagonal neighbors per step. Returns: grid: Grid with extrapolated values. valid: binary Grid marking all valid values after extrapolation. """ if isinstance(grid, CenteredGrid): new_values = math.finite_fill(grid.values, distance=distance, diagonal=diagonal, padding=grid.extrapolation) return grid.with_values(new_values) elif isinstance(grid, StaggeredGrid): new_values = [finite_fill(c, distance=distance, diagonal=diagonal).values for c in grid.vector] return grid.with_values(math.stack(new_values, channel(grid))) else: raise ValueError(grid)
def floor(x) ‑> phi.math._tensors.Tensor
-
Computes ⌊x⌋ of the
Tensor
orPhiTreeNode
x
.Expand source code
def floor(x) -> Tensor or PhiTreeNode: """ Computes *⌊x⌋* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.floor)
def fourier_laplace(grid: ~GridType, times=1) ‑> ~GridType
-
Expand source code
def fourier_laplace(grid: GridType, times=1) -> GridType: """ See `phi.math.fourier_laplace()` """ assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC values = math.fourier_laplace(grid.values, dx=grid.dx, times=times) return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
def fourier_poisson(grid: ~GridType, times=1) ‑> ~GridType
-
Expand source code
def fourier_poisson(grid: GridType, times=1) -> GridType: """ See `phi.math.fourier_poisson()` """ assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC values = math.fourier_poisson(grid.values, dx=grid.dx, times=times) return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-05, ignore_mean=False, n=2) ‑> phi.math._tensors.Tensor
-
Penalizes the squared
values
in frequency (Fourier) space. Lower frequencies are weighted more strongly then higher frequencies, depending onfrequency_falloff
.Args
x
Tensor
orPhiTreeNode
Values to penalize, typicallyactual - target
.frequency_falloff
- Large values put more emphasis on lower frequencies, 1.0 weights all frequencies equally. Note: The total loss is not normalized. Varying the value will result in losses of different magnitudes.
threshold
- Frequency amplitudes below this value are ignored. Setting this to zero may cause infinities or NaN values during backpropagation.
ignore_mean
- If
True
, does not penalize the mean value (frequency=0 component).
Returns
Scalar loss value
Expand source code
def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-5, ignore_mean=False, n=2) -> Tensor: """ Penalizes the squared `values` in frequency (Fourier) space. Lower frequencies are weighted more strongly then higher frequencies, depending on `frequency_falloff`. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` Values to penalize, typically `actual - target`. frequency_falloff: Large values put more emphasis on lower frequencies, 1.0 weights all frequencies equally. *Note*: The total loss is not normalized. Varying the value will result in losses of different magnitudes. threshold: Frequency amplitudes below this value are ignored. Setting this to zero may cause infinities or NaN values during backpropagation. ignore_mean: If `True`, does not penalize the mean value (frequency=0 component). Returns: Scalar loss value """ assert n in (1, 2) if isinstance(x, Tensor): if ignore_mean: x -= math.mean(x, x.shape.non_batch) k_squared = vec_squared(math.fftfreq(x.shape.spatial)) weights = math.exp(-0.5 * k_squared * frequency_falloff ** 2) diff_fft = abs_square(math.fft(x) * weights) diff_fft = math.sqrt(math.maximum(diff_fft, threshold)) return l2_loss(diff_fft) if n == 2 else l1_loss(diff_fft) elif isinstance(x, PhiTreeNode): losses = [frequency_loss(getattr(x, a), frequency_falloff, threshold, ignore_mean, n) for a in variable_values(x)] return sum(losses) else: raise ValueError(x)
def functional_gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Creates a function which computes the gradient of
f
.Example:
def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = functional_gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = functional_gradient(loss_function, 'x,y', get_output=True)(x, y)
Functional gradients are implemented for the following backends:
- PyTorch:
torch.autograd.grad
/torch.autograd.backward
- TensorFlow:
tf.GradientTape
- Jax:
jax.grad
When the gradient function is invoked,
f
is called with tensors that track the gradient. For PyTorch,arg.requires_grad = True
for all positional arguments off
.Args
f
- Function to be differentiated.
f
must return a floating pointTensor
with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function ifreturn_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 off
, auxiliary data and gradient off
ifget_output=True
, else just the gradient off
.Expand source code
def functional_gradient(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the gradient of `f`. Example: ```python def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = functional_gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = functional_gradient(loss_function, 'x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and gradient of `f` if `get_output=True`, else just the gradient of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=True)
- PyTorch:
def gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Creates a function which computes the Jacobian matrix of
f
. For scalar functions, consider usingfunctional_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:
- PyTorch:
torch.autograd.grad
/torch.autograd.backward
- TensorFlow:
tf.GradientTape
- Jax:
jax.grad
When the gradient function is invoked,
f
is called with tensors that track the gradient. For PyTorch,arg.requires_grad = True
for all positional arguments off
.Args
f
- Function to be differentiated.
f
must return a floating pointTensor
with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function ifreturn_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 off
, auxiliary data and Jacobian off
ifget_output=True
, else just the Jacobian off
.Expand source code
def jacobian(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the Jacobian matrix of `f`. For scalar functions, consider using `functional_gradient()` instead. Example: ```python def f(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = jacobian(loss_function, wrt='x', get_output=False)(x, y) (loss, prediction), (dx, dy) = jacobian(loss_function, wrt='x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and Jacobian of `f` if `get_output=True`, else just the Jacobian of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=False)
- PyTorch:
def imag(x) ‑> phi.math._tensors.Tensor
-
Returns the imaginary part of
x
. Ifx
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
orPhiTreeNode
or native tensor.
Returns
Imaginary component of
x
ifx
is complex, zeros otherwise.Expand source code
def imag(x) -> Tensor or PhiTreeNode: """ 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 `phi.math.magic.PhiTreeNode` or native tensor. Returns: Imaginary component of `x` if `x` is complex, zeros otherwise. """ return _backend_op1(x, Backend.imag)
def integrate(field: phi.field._field.Field, region: phi.geom._geom.Geometry, **kwargs) ‑> phi.math._tensors.Tensor
-
Computes ∫R f(x) dxd , where f denotes the
Field
, R theregion
and d the number of spatial dimensions (d=field.shape.spatial_rank
). Depending on thesample()
implementation forfield
, the integral may be a rough approximation.This method is currently only implemented for
CenteredGrid
.Args
field
Field
to integrate.region
- Region to integrate over.
**kwargs
- Specify numerical scheme.
Returns
Integral as
phi.Tensor
Expand source code
def integrate(field: Field, region: Geometry, **kwargs) -> Tensor: """ Computes *∫<sub>R</sub> f(x) dx<sup>d</sup>* , where *f* denotes the `Field`, *R* the `region` and *d* the number of spatial dimensions (`d=field.shape.spatial_rank`). Depending on the `sample` implementation for `field`, the integral may be a rough approximation. This method is currently only implemented for `CenteredGrid`. Args: field: `Field` to integrate. region: Region to integrate over. **kwargs: Specify numerical scheme. Returns: Integral as `phi.Tensor` """ if not isinstance(field, CenteredGrid): raise NotImplementedError() return field._sample(region, **kwargs) * region.volume
def is_finite(x) ‑> phi.math._tensors.Tensor
-
Returns a
Tensor
orPhiTreeNode
matchingx
with valuesTrue
wherex
has a finite value andFalse
otherwise.Expand source code
def is_finite(x) -> Tensor or PhiTreeNode: """ Returns a `Tensor` or `phi.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """ return _backend_op1(x, Backend.isfinite)
def isfinite(x) ‑> phi.math._tensors.Tensor
-
Returns a
Tensor
orPhiTreeNode
matchingx
with valuesTrue
wherex
has a finite value andFalse
otherwise.Expand source code
def is_finite(x) -> Tensor or PhiTreeNode: """ Returns a `Tensor` or `phi.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """ return _backend_op1(x, Backend.isfinite)
def jacobian(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Creates a function which computes the Jacobian matrix of
f
. For scalar functions, consider usingfunctional_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:
- PyTorch:
torch.autograd.grad
/torch.autograd.backward
- TensorFlow:
tf.GradientTape
- Jax:
jax.grad
When the gradient function is invoked,
f
is called with tensors that track the gradient. For PyTorch,arg.requires_grad = True
for all positional arguments off
.Args
f
- Function to be differentiated.
f
must return a floating pointTensor
with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function ifreturn_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 off
, auxiliary data and Jacobian off
ifget_output=True
, else just the Jacobian off
.Expand source code
def jacobian(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the Jacobian matrix of `f`. For scalar functions, consider using `functional_gradient()` instead. Example: ```python def f(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = jacobian(loss_function, wrt='x', get_output=False)(x, y) (loss, prediction), (dx, dy) = jacobian(loss_function, wrt='x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and Jacobian of `f` if `get_output=True`, else just the Jacobian of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=False)
- PyTorch:
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, PhiTreeNode
positional arguments do not match in non-variable properties.
Compilation is implemented for the following backends:
- PyTorch:
torch.jit.trace
- TensorFlow:
tf.function
- Jax:
jax.jit
Jit-compilations cannot be nested, i.e. you cannot call
jit_compile()
while another function is being compiled. An exception to this isjit_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
orPhiTreeNode
returning a singleTensor
orPhiTreeNode
. auxiliary_args
- Comma-separated parameter names of arguments that are not relevant to backpropagation.
forget_traces
- If
True
, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.
Returns
Function with similar signature and return values as
f
.Expand source code
def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) -> Callable: """ Compiles a graph based on the function `f`. The graph compilation is performed just-in-time (jit), e.g. when the returned function is called for the first time. The traced function will compute the same result as `f` but may run much faster. Some checks may be disabled in the compiled function. Can be used as a decorator: ```python @math.jit_compile def my_function(x: math.Tensor) -> math.Tensor: ``` Invoking the returned function may invoke re-tracing / re-compiling `f` after the first call if either * it is called with a different number of arguments, * the tensor arguments have different dimension names or types (the dimension order also counts), * any `Tensor` arguments require a different backend than previous invocations, * `phi.math.magic.PhiTreeNode` positional arguments do not match in non-variable properties. Compilation is implemented for the following backends: * PyTorch: [`torch.jit.trace`](https://pytorch.org/docs/stable/jit.html) * TensorFlow: [`tf.function`](https://www.tensorflow.org/guide/function) * Jax: [`jax.jit`](https://jax.readthedocs.io/en/latest/notebooks/quickstart.html#using-jit-to-speed-up-functions) Jit-compilations cannot be nested, i.e. you cannot call `jit_compile()` while another function is being compiled. An exception to this is `jit_compile_linear()` which can be called from within a jit-compiled function. See Also: `jit_compile_linear()` Args: f: Function to be traced. All positional arguments must be of type `Tensor` or `phi.math.magic.PhiTreeNode` returning a single `Tensor` or `phi.math.magic.PhiTreeNode`. auxiliary_args: Comma-separated parameter names of arguments that are not relevant to backpropagation. forget_traces: If `True`, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces. Returns: Function with similar signature and return values as `f`. """ if f is None: kwargs = {k: v for k, v in locals().items() if v is not None} return partial(jit_compile, **kwargs) auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip()) return f if isinstance(f, (JitFunction, LinearFunction)) and f.auxiliary_args == auxiliary_args else JitFunction(f, auxiliary_args, forget_traces or False)
def jit_compile_linear(f: Callable[[~X], ~Y] = None, auxiliary_args: str = None, forget_traces: bool = None) ‑> phi.math._functional.LinearFunction[~X, ~Y]
-
Compile an optimized representation of the linear function
f
. For backends that support sparse tensors, a sparse matrix will be constructed forf
.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
andf
must return aTensor
. 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 asf
.Expand source code
def jit_compile_linear(f: Callable[[X], Y] = None, auxiliary_args: str = None, forget_traces: bool = None) -> 'LinearFunction[X, Y]': """ Compile an optimized representation of the linear function `f`. For backends that support sparse tensors, a sparse matrix will be constructed for `f`. Can be used as a decorator: ```python @math.jit_compile_linear def my_linear_function(x: math.Tensor) -> math.Tensor: ``` Unlike `jit_compile()`, `jit_compile_linear()` can be called during a regular jit compilation. See Also: `jit_compile()` Args: f: Function that is linear in its positional arguments. All positional arguments must be of type `Tensor` and `f` must return a `Tensor`. auxiliary_args: Which parameters `f` is not linear in. These arguments are treated as conditioning arguments and will cause re-tracing on change. forget_traces: If `True`, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces. Returns: `LinearFunction` with similar signature and return values as `f`. """ if f is None: kwargs = {k: v for k, v in locals().items() if v is not None} return partial(jit_compile_linear, **kwargs) if isinstance(f, JitFunction): f = f.f # cannot trace linear function from jitted version if isinstance(auxiliary_args, str): auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip()) else: assert auxiliary_args is None f_params = function_parameters(f) auxiliary_args = f_params[1:] return f if isinstance(f, LinearFunction) and f.auxiliary_args == auxiliary_args else LinearFunction(f, auxiliary_args, forget_traces or False)
def l1_loss(x, reduce: Union[str, tuple, list, set, phi.math._shape.Shape, Callable] = <function non_batch>) ‑> phi.math._tensors.Tensor
-
Computes ∑i ||xi||1, summing over all non-batch dimensions.
Args
x
Tensor
orPhiTreeNode
or 0D or 1D native tensor. ForPhiTreeNode
objects, only value the sum over all value attributes is computed.reduce
- Dimensions to reduce as
DimFilter
.
Returns
loss
Tensor
Expand source code
def l1_loss(x, reduce: DimFilter = math.non_batch) -> Tensor: """ Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>1</sub>*, summing over all non-batch dimensions. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` or 0D or 1D native tensor. For `phi.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed. reduce: Dimensions to reduce as `DimFilter`. Returns: loss: `Tensor` """ if isinstance(x, Tensor): return math.sum_(abs(x), reduce) elif isinstance(x, PhiTreeNode): return sum([l1_loss(getattr(x, a), reduce) for a in variable_values(x)]) else: try: backend = math.choose_backend(x) shape = backend.staticshape(x) if len(shape) == 0: return abs(x) elif len(shape) == 1: return backend.sum(abs(x)) else: raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-Flow tensors.") except math.NoBackendFound: raise ValueError(x)
def l2_loss(x, reduce: Union[str, tuple, list, set, phi.math._shape.Shape, Callable] = <function non_batch>) ‑> phi.math._tensors.Tensor
-
Computes ∑i ||xi||22 / 2, summing over all non-batch dimensions.
Args
x
Tensor
orPhiTreeNode
or 0D or 1D native tensor. ForPhiTreeNode
objects, only value the sum over all value attributes is computed.reduce
- Dimensions to reduce as
DimFilter
.
Returns
loss
Tensor
Expand source code
def l2_loss(x, reduce: DimFilter = math.non_batch) -> Tensor: """ Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>2</sub><sup>2</sup> / 2*, summing over all non-batch dimensions. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` or 0D or 1D native tensor. For `phi.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed. reduce: Dimensions to reduce as `DimFilter`. Returns: loss: `Tensor` """ if isinstance(x, Tensor): if x.dtype.kind == complex: x = abs(x) return math.sum_(x ** 2, reduce) * 0.5 elif isinstance(x, PhiTreeNode): return sum([l2_loss(getattr(x, a), reduce) for a in variable_values(x)]) else: try: backend = math.choose_backend(x) shape = backend.staticshape(x) if len(shape) == 0: return x ** 2 * 0.5 elif len(shape) == 1: return backend.sum(x ** 2) * 0.5 else: raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-Flow tensors.") except math.NoBackendFound: raise ValueError(x)
def laplace(field: ~GridType, axes=<function spatial>, order=2, implicit: phi.math._optimize.Solve = None, weights: phi.math._tensors.Tensor = None) ‑> ~GridType
-
Spatial Laplace operator for scalar grid. If a vector grid is passed, it is assumed to be centered and the laplace is computed component-wise.
Args
field
- n-dimensional
CenteredGrid
axes
- The second derivative along these dimensions is summed over
weights
- (Optional) Multiply the axis terms by these factors before summation.
Must be a
Tensor
orField
with a single channel dimension that lists all laplace axes by name. order
- Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
- When a
Solve
object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.
Returns
laplacian field as
CenteredGrid
Expand source code
def laplace(field: GridType, axes=spatial, order=2, implicit: math.Solve = None, weights: Tensor or Field = None) -> GridType: """ Spatial Laplace operator for scalar grid. If a vector grid is passed, it is assumed to be centered and the laplace is computed component-wise. Args: field: n-dimensional `CenteredGrid` axes: The second derivative along these dimensions is summed over weights: (Optional) Multiply the axis terms by these factors before summation. Must be a `phi.math.Tensor` or `phi.field.Field` with a single channel dimension that lists all laplace axes by name. order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. Returns: laplacian field as `CenteredGrid` """ if isinstance(weights, Field): weights = weights.at(field).values axes_names = field.shape.only(axes).names extrap_map = {} if not implicit: if order == 2: values, needed_shifts = [1, -2, 1], (-1, 0, 1) elif order == 4: values, needed_shifts = [-1/12, 4/3, -5/2, 4/3, -1/12], (-2, -1, 0, 1, 2) else: extrap_map_rhs = {} if order == 6: values, needed_shifts = [3/44, 12/11, -51/22, 12/11, 3/44], (-2, -1, 0, 1, 2) extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC) values_rhs, needed_shifts_rhs = [2/11, 1, 2/11], (-1, 0, 1) extrap_map_rhs['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC) base_widths = (abs(min(needed_shifts)), max(needed_shifts)) field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation)) padded_components = [pad(field, {dim: base_widths}) for dim in axes_names] shifted_components = [shift(padded_component, needed_shifts, None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, axes_names)] result_components = [sum([value * shift_ for value, shift_ in zip(values, shifted_component)]) / field.dx.vector[dim]**2 for shifted_component, dim in zip(shifted_components, axes_names)] if implicit: result_components = stack(result_components, channel('laplacian')) result_components.with_values(result_components.values._cache()) result_components = result_components.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map_rhs), field.extrapolation)) implicit.x0 = result_components result_components = solve_linear(_lhs_for_implicit_scheme, result_components, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=channel('laplacian')) result_components = unstack(result_components, 'laplacian') extrap_map = extrap_map_rhs result_components = [component.with_bounds(field.bounds) for component in result_components] if weights is not None: assert channel(weights).rank == 1 and channel(weights).item_names is not None, f"weights must have one channel dimension listing the laplace dims but got {shape(weights)}" assert set(channel(weights).item_names[0]) >= set(axes_names), f"the channel dim of weights must contain all laplace dims {axes_names} but only has {channel(weights).item_names}" result_components = [c * weights[ax] for c, ax in zip(result_components, axes_names)] result = sum(result_components) result = result.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation)) return result
def mask(obj: ~SampledFieldType) ‑> ~SampledFieldType
-
Returns a
Field
that masks the inside (or non-zero values whenobj
is a grid) of a physical object. The mask takes the value 1 inside the object and 0 outside. ForCenteredGrid
andStaggeredGrid
, the mask labels non-zero non-NaN entries as 1 and all other values as 0Returns
Grid
type orPointCloud
Expand source code
def mask(obj: SampledFieldType or Geometry) -> SampledFieldType: """ Returns a `Field` that masks the inside (or non-zero values when `obj` is a grid) of a physical object. The mask takes the value 1 inside the object and 0 outside. For `CenteredGrid` and `StaggeredGrid`, the mask labels non-zero non-NaN entries as 1 and all other values as 0 Returns: `Grid` type or `PointCloud` """ if isinstance(obj, PointCloud): return PointCloud(obj.elements, 1, math.extrapolation.remove_constant_offset(obj.extrapolation), bounds=obj.bounds) elif isinstance(obj, Geometry): return PointCloud(obj, 1, 0) elif isinstance(obj, CenteredGrid): values = math.cast(obj.values != 0, int) return obj.with_values(values) else: raise ValueError(obj)
def maximum(f1: phi.field._field.Field, f2: phi.field._field.Field)
-
Element-wise maximum. One of the given fields needs to be an instance of
SampledField
and the the result will be sampled at the corresponding points. If both areSampledFields
but have different points,f1
takes priority.Args
Returns
Expand source code
def maximum(f1: Field or Geometry or float, f2: Field or Geometry or float): """ Element-wise maximum. One of the given fields needs to be an instance of `SampledField` and the the result will be sampled at the corresponding points. If both are `SampledFields` but have different points, `f1` takes priority. Args: f1: `Field` or `Geometry` or constant. f2: `Field` or `Geometry` or constant. Returns: `SampledField` """ f1, f2 = _auto_resample(f1, f2) return f1.with_values(math.maximum(f1.values, f2.values))
def mean(field: phi.field._field.SampledField) ‑> phi.math._tensors.Tensor
-
Computes the mean value by reducing all spatial / instance dimensions.
Args
field
SampledField
Returns
phi.Tensor
Expand source code
def mean(field: SampledField) -> Tensor: """ Computes the mean value by reducing all spatial / instance dimensions. Args: field: `SampledField` Returns: `phi.Tensor` """ return math.mean(field.values, field.shape.non_channel.non_batch)
def minimize(f: Callable[[~X], ~Y], solve: phi.math._optimize.Solve[~X, ~Y]) ‑> ~X
-
Finds a minimum of the scalar function f(x). The
method
argument ofsolve
determines which optimizer is used. All optimizers supported byscipy.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 withmethod='GD'
.math.minimize()
is limited to backends that supportjacobian()
, 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 beTensor
orPhiTreeNode
. Ifsolve.x0
is atuple
orlist
, 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 off
must be a scalar floatTensor
orPhiTreeNode
. solve
Solve
object to specify method type, parameters and initial guess forx
.
Returns
x
- solution, the minimum point
x
.
Raises
NotConverged
- If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
- If the optimization failed prematurely.
Expand source code
def minimize(f: Callable[[X], Y], solve: Solve[X, Y]) -> X: """ Finds a minimum of the scalar function *f(x)*. The `method` argument of `solve` determines which optimizer is used. All optimizers supported by `scipy.optimize.minimize` are supported, see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html . Additionally a gradient descent solver with adaptive step size can be used with `method='GD'`. `math.minimize()` is limited to backends that support `jacobian()`, i.e. PyTorch, TensorFlow and Jax. To obtain additional information about the performed solve, use a `SolveTape`. See Also: `solve_nonlinear()`. Args: f: Function whose output is subject to minimization. All positional arguments of `f` are optimized and must be `Tensor` or `phi.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 `phi.math.magic.PhiTreeNode`. solve: `Solve` object to specify method type, parameters and initial guess for `x`. Returns: x: solution, the minimum point `x`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the optimization failed prematurely. """ solve = solve.with_defaults('optimization') assert (solve.rel_tol == 0).all, f"rel_tol must be zero for minimize() but got {solve.rel_tol}" assert solve.preprocess_y is None, "minimize() does not allow preprocess_y" x0_nest, x0_tensors = disassemble_tree(solve.x0) x0_tensors = [to_float(t) for t in x0_tensors] backend = choose_backend_t(*x0_tensors, prefer_default=True) batch_dims = merge_shapes(*[t.shape for t in x0_tensors]).batch x0_natives = [] for t in x0_tensors: t._expand() assert t.shape.is_uniform x0_natives.append(reshaped_native(t, [batch_dims, t.shape.non_batch], force_expand=True)) x0_flat = backend.concat(x0_natives, -1) def unflatten_assemble(x_flat, additional_dims: Shape = EMPTY_SHAPE, convert=True): i = 0 x_tensors = [] for x0_native, x0_tensor in zip(x0_natives, x0_tensors): vol = backend.shape(x0_native)[-1] flat_native = x_flat[..., i:i + vol] x_tensors.append(reshaped_tensor(flat_native, [*additional_dims, batch_dims, x0_tensor.shape.non_batch], convert=convert)) i += vol x = assemble_tree(x0_nest, x_tensors) return x def native_function(x_flat): x = unflatten_assemble(x_flat) if isinstance(x, (tuple, list)): y = f(*x) else: y = f(x) _, y_tensors = disassemble_tree(y) assert not non_batch(y_tensors[0]), f"Failed to minimize '{f.__name__}' because it returned a non-scalar output {shape(y_tensors[0])}. Reduce all non-batch dimensions, e.g. using math.l2_loss()" try: loss_native = reshaped_native(y_tensors[0], [batch_dims]) except AssertionError: raise AssertionError(f"Failed to minimize '{f.__name__}' because its output loss {shape(y_tensors[0])} has more batch dimensions than the initial guess {batch_dims}.") return y_tensors[0].sum, (loss_native,) atol = backend.to_float(reshaped_native(solve.abs_tol, [batch_dims], force_expand=True)) maxi = reshaped_numpy(solve.max_iterations, [batch_dims], force_expand=True) trj = _SOLVE_TAPES and any(t.record_trajectories for t in _SOLVE_TAPES) t = time.perf_counter() ret = backend.minimize(solve.method, native_function, x0_flat, atol, maxi, trj) t = time.perf_counter() - t if not trj: assert isinstance(ret, SolveResult) converged = reshaped_tensor(ret.converged, [batch_dims]) diverged = reshaped_tensor(ret.diverged, [batch_dims]) x = unflatten_assemble(ret.x) iterations = reshaped_tensor(ret.iterations, [batch_dims]) function_evaluations = reshaped_tensor(ret.function_evaluations, [batch_dims]) residual = reshaped_tensor(ret.residual, [batch_dims]) result = SolveInfo(solve, x, residual, iterations, function_evaluations, converged, diverged, ret.method, ret.message, t) else: # trajectory assert isinstance(ret, (tuple, list)) and all(isinstance(r, SolveResult) for r in ret) converged = reshaped_tensor(ret[-1].converged, [batch_dims]) diverged = reshaped_tensor(ret[-1].diverged, [batch_dims]) x = unflatten_assemble(ret[-1].x) x_ = unflatten_assemble(numpy.stack([r.x for r in ret]), additional_dims=batch('trajectory'), convert=False) residual = stack([reshaped_tensor(r.residual, [batch_dims]) for r in ret], batch('trajectory')) iterations = reshaped_tensor(ret[-1].iterations, [batch_dims]) function_evaluations = stack([reshaped_tensor(r.function_evaluations, [batch_dims]) for r in ret], batch('trajectory')) result = SolveInfo(solve, x_, residual, iterations, function_evaluations, converged, diverged, ret[-1].method, ret[-1].message, t) for tape in _SOLVE_TAPES: tape._add(solve, trj, result) result.convergence_check(False) # raises ConvergenceException return x
def minimum(f1: phi.field._field.Field, f2: phi.field._field.Field)
-
Element-wise minimum. One of the given fields needs to be an instance of
SampledField
and the the result will be sampled at the corresponding points. If both areSampledFields
but have different points,f1
takes priority.Args
Returns
Expand source code
def minimum(f1: Field or Geometry or float, f2: Field or Geometry or float): """ Element-wise minimum. One of the given fields needs to be an instance of `SampledField` and the the result will be sampled at the corresponding points. If both are `SampledFields` but have different points, `f1` takes priority. Args: f1: `Field` or `Geometry` or constant. f2: `Field` or `Geometry` or constant. Returns: `SampledField` """ f1, f2 = _auto_resample(f1, f2) return f1.with_values(math.minimum(f1.values, f2.values))
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) ‑> phi.field._field.SampledField
-
Similar to
native_call()
.Args
f
- Function to be called on native tensors of
inputs.values
. The function output must have the same dimension layout as the inputs and the batch size must be identical. *inputs
SampledField
orphi.Tensor
instances.extrapolation
- (Optional) Extrapolation of the output field. If
None
, uses the extrapolation of the first input field.
Returns
SampledField
matching the firstSampledField
ininputs
.Expand source code
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) -> SampledField or Tensor: """ Similar to `phi.math.native_call()`. Args: f: Function to be called on native tensors of `inputs.values`. The function output must have the same dimension layout as the inputs and the batch size must be identical. *inputs: `SampledField` or `phi.Tensor` instances. extrapolation: (Optional) Extrapolation of the output field. If `None`, uses the extrapolation of the first input field. Returns: `SampledField` matching the first `SampledField` in `inputs`. """ input_tensors = [i.values if isinstance(i, SampledField) else tensor(i) for i in inputs] values = math.native_call(f, *input_tensors, channels_last=channels_last, channel_dim=channel_dim) for i in inputs: if isinstance(i, SampledField): result = i.with_values(values=values) if extrapolation is not None: result = result.with_extrapolation(extrapolation) return result else: raise AssertionError("At least one input must be a SampledField.")
def normalize(field: phi.field._field.SampledField, norm: phi.field._field.SampledField, epsilon=1e-05)
-
Multiplies the values of
field
so that its sum matches the source.Expand source code
def normalize(field: SampledField, norm: SampledField, epsilon=1e-5): """ Multiplies the values of `field` so that its sum matches the source. """ data = math.normalize_to(field.values, norm.values, epsilon) return field.with_values(data)
def pack_dims(field: ~SampledFieldType, dims: phi.math._shape.Shape, packed_dim: phi.math._shape.Shape, pos: int = None) ‑> ~SampledFieldType
-
Currently only supports grids and non-spatial dimensions.
See Also:
pack_dims()
.Args
field
SampledField
Returns
SampledField
of same type asfield
.Expand source code
def pack_dims(field: SampledFieldType, dims: Shape or tuple or list or str, packed_dim: Shape, pos: int or None = None) -> SampledFieldType: """ Currently only supports grids and non-spatial dimensions. See Also: `phi.math.pack_dims()`. Args: field: `SampledField` Returns: `SampledField` of same type as `field`. """ if isinstance(field, Grid): if spatial(field.shape.only(dims)): raise NotImplementedError("Packing spatial dimensions not supported for grids") return field.with_values(math.pack_dims(field.values, dims, packed_dim, pos)) else: raise NotImplementedError()
def pad(grid: ~GridType, widths: int) ‑> ~GridType
-
Pads a
Grid
using its extrapolation.Unlike
pad()
, this function also affects thebounds
of the grid, changing its size and origin depending onwidths
.Args
grid
CenteredGrid
orStaggeredGrid
widths
- Either
int
or(lower, upper)
to pad the same number of cells in all spatial dimensions ordict
mapping dimension names to(lower, upper)
.
Returns
Grid
of the same type asgrid
Expand source code
def pad(grid: GridType, widths: int or tuple or list or dict) -> GridType: """ Pads a `Grid` using its extrapolation. Unlike `phi.math.pad()`, this function also affects the `bounds` of the grid, changing its size and origin depending on `widths`. Args: grid: `CenteredGrid` or `StaggeredGrid` widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: `Grid` of the same type as `grid` """ if isinstance(widths, int): widths = {axis: (widths, widths) for axis in grid.shape.spatial.names} elif isinstance(widths, (tuple, list)): widths = {axis: (width if isinstance(width, (tuple, list)) else (width, width)) for axis, width in zip(grid.shape.spatial.names, widths)} else: assert isinstance(widths, dict) widths_list = [widths[axis] if axis in widths.keys() else (0, 0) for axis in grid.shape.spatial.names] if isinstance(grid, Grid): data = math.pad(grid.values, widths, grid.extrapolation, bounds=grid.bounds) w_lower = math.wrap([w[0] for w in widths_list]) w_upper = math.wrap([w[1] for w in widths_list]) bounds = Box(grid.box.lower - w_lower * grid.dx, grid.box.upper + w_upper * grid.dx) return type(grid)(values=data, bounds=bounds, extrapolation=grid.extrapolation) raise NotImplementedError(f"{type(grid)} not supported. Only Grid instances allowed.")
def read(file: str, convert_to_backend=True) ‑> phi.field._field.SampledField
-
Loads a previously saved
SampledField
from disc.See Also:
write()
.Args
file
- Single file as
str
orTensor
of string type. Iffile
is a tensor, all contained files are loaded an stacked according to the dimensions offile
. convert_to_backend
- Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.
Returns
Loaded
SampledField
.Expand source code
def read(file: str or Tensor, convert_to_backend=True) -> SampledField: """ Loads a previously saved `SampledField` from disc. See Also: `write()`. Args: file: Single file as `str` or `Tensor` of string type. If `file` is a tensor, all contained files are loaded an stacked according to the dimensions of `file`. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Loaded `SampledField`. """ if isinstance(file, str): return read_single_field(file, convert_to_backend=convert_to_backend) if isinstance(file, Tensor): if file.rank == 0: return read_single_field(file.native(), convert_to_backend=convert_to_backend) else: dim = file.shape[0] files = file.unstack(dim.name) fields = [read(file_, convert_to_backend=convert_to_backend) for file_ in files] return stack(fields, dim) else: raise ValueError(file)
def real(x) ‑> phi.math._tensors.Tensor
-
See Also:
imag()
,conjugate()
.Args
x
Tensor
orPhiTreeNode
or native tensor.
Returns
Real component of
x
.Expand source code
def real(x) -> Tensor or PhiTreeNode: """ See Also: `imag()`, `conjugate()`. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` or native tensor. Returns: Real component of `x`. """ return _backend_op1(x, Backend.real)
def reduce_sample(field: phi.field._field.Field, geometry: phi.geom._geom.Geometry, dim=(vectorᶜ=None), **kwargs) ‑> phi.math._tensors.Tensor
-
Similar to
sample()
, but matches channel dimensions ofgeometry
with channel dimensions of this field. Currently,geometry
may have at most one channel dimension.See Also:
sample()
,Field.at()
, Resampling overview.Args
field
- Source
Field
to sample. geometry
- Single or batched
Geometry
orSampledField
or locationTensor
. When passing aSampledField
, itselements
are used as sample points. When passing a vector-valuedTensor
, aPoint
geometry will be created. dim
- Dimension of result, resulting from reduction of channel dimensions.
**kwargs
- Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.
Returns
Sampled values as a
Tensor
Expand source code
def reduce_sample(field: Field or Geometry, geometry: Geometry or SampledField or Tensor, dim=channel('vector'), **kwargs) -> math.Tensor: """ Similar to `sample()`, but matches channel dimensions of `geometry` with channel dimensions of this field. Currently, `geometry` may have at most one channel dimension. See Also: `sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields). Args: field: Source `Field` to sample. geometry: Single or batched `phi.geom.Geometry` or `SampledField` or location `Tensor`. When passing a `SampledField`, its `elements` are used as sample points. When passing a vector-valued `Tensor`, a `Point` geometry will be created. dim: Dimension of result, resulting from reduction of channel dimensions. **kwargs: Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points. Returns: Sampled values as a `phi.math.Tensor` """ geometry = _get_geometry(geometry) if isinstance(field, Geometry): from ._field_math import mask field = mask(field) if isinstance(field, SampledField) and field.elements.shallow_equals(geometry): return field.values if channel(geometry).without('vector'): # Reduce this dimension geom_ch = channel(geometry).without('vector') assert geom_ch.rank == 1, "Only single-dimension reduction supported." if field.shape.channel.volume > 1: assert field.shape.channel.volume == geom_ch.volume, f"Cannot sample field with channels {field.shape.channel} at elements with channels {geometry.shape.channel}." components = math.unstack(field, field.shape.channel.name) sampled = [c._sample(p, **kwargs) for c, p in zip(components, geometry.unstack(geom_ch.name))] else: sampled = [field._sample(p, **kwargs) for p in geometry.unstack(channel(geometry).without('vector').name)] dim = dim.with_size(geometry.shape.channel.item_names[0]) return math.stack(sampled, dim) else: # Nothing to reduce return field._sample(geometry, **kwargs)
def resample(value: Union[phi.field._field.Field, phi.geom._geom.Geometry, phi.math._tensors.Tensor, float], to: phi.field._field.SampledField, keep_extrapolation=False, **kwargs)
-
Samples a
Field
,Geometry
or value at the sample points of the fieldto
. The result will approximatevalue
on the data structure ofto
. Unlikesample()
, this method returns aField
object, not aTensor
.Aliases
value.at(to)
, (and the deprecatedvalue @ to
).See Also:
sample()
,reduce_sample()
,Field.at()
, Resampling overview.Args
value
- Object containing values to resample. This can be
to
SampledField
(CenteredGrid
,StaggeredGrid
orPointCloud
) object defining the sample points. The current values ofto
are ignored.keep_extrapolation
- Only available if
self
is aSampledField
. If True, the resampled field will inherit the extrapolation fromself
instead ofrepresentation
. This can result in non-compatible value tensors for staggered grids where the tensor size depends on the extrapolation type. **kwargs
- Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.
Returns
Field object of same type as
representation
Examples
>>> grid = CenteredGrid(x=64, y=32) >>> field.resample(Noise(), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(1, to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(Box(x=1, y=2), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(grid, to=grid) == grid True
Expand source code
def resample(value: Union[Field, Geometry, Tensor, float], to: SampledField, keep_extrapolation=False, **kwargs): """ Samples a `Field`, `Geometry` or value at the sample points of the field `to`. The result will approximate `value` on the data structure of `to`. Unlike `sample()`, this method returns a `Field` object, not a `Tensor`. Aliases: `value.at(to)`, (and the deprecated `value @ to`). See Also: `sample()`, `reduce_sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields). Args: value: Object containing values to resample. This can be to: `SampledField` (`CenteredGrid`, `StaggeredGrid` or `PointCloud`) object defining the sample points. The current values of `to` are ignored. keep_extrapolation: Only available if `self` is a `SampledField`. If True, the resampled field will inherit the extrapolation from `self` instead of `representation`. This can result in non-compatible value tensors for staggered grids where the tensor size depends on the extrapolation type. **kwargs: Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points. Returns: Field object of same type as `representation` Examples: >>> grid = CenteredGrid(x=64, y=32) >>> field.resample(Noise(), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(1, to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(Box(x=1, y=2), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(grid, to=grid) == grid True """ if not isinstance(value, (Field, Geometry)): return to.with_values(value) resampled = reduce_sample(value, to.elements, **kwargs) extrap = value.extrapolation if isinstance(value, SampledField) and keep_extrapolation else to.extrapolation return to.with_values(resampled).with_extrapolation(extrap)
def round(x) ‑> phi.math._tensors.Tensor
-
Rounds the
Tensor
orPhiTreeNode
x
to the closest integer.Expand source code
def round_(x) -> Tensor or PhiTreeNode: """ Rounds the `Tensor` or `phi.math.magic.PhiTreeNode` `x` to the closest integer. """ return _backend_op1(x, Backend.round)
def sample(field: phi.field._field.Field, geometry: phi.geom._geom.Geometry, **kwargs) ‑> phi.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. Thegeometry
must not share any channel dimensions with this field. Spatial dimensions ofgeometry
can be used to sample a grid of geometries.See Also:
reduce_sample()
,Field.at()
, Resampling overview.Args
field
- Source
Field
to sample. geometry
- Single or batched
Geometry
orSampledField
or locationTensor
. When passing aSampledField
, itselements
are used as sample points. When passing a vector-valuedTensor
, aPoint
geometry will be created. **kwargs
- Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.
Returns
Sampled values as a
Tensor
Expand source code
def sample(field: Field or Geometry, geometry: Geometry or SampledField or Tensor, **kwargs) -> math.Tensor: """ Computes the field value inside the volume of the (batched) `geometry`. The field value may be determined by integrating over the volume, sampling the central value or any other way. The batch dimensions of `geometry` are matched with this field. The `geometry` must not share any channel dimensions with this field. Spatial dimensions of `geometry` can be used to sample a grid of geometries. See Also: `reduce_sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields). Args: field: Source `Field` to sample. geometry: Single or batched `phi.geom.Geometry` or `SampledField` or location `Tensor`. When passing a `SampledField`, its `elements` are used as sample points. When passing a vector-valued `Tensor`, a `Point` geometry will be created. **kwargs: Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points. Returns: Sampled values as a `phi.math.Tensor` """ geometry = _get_geometry(geometry) if isinstance(field, Geometry): from ._field_math import mask field = mask(field) geom_ch = channel(geometry).without('vector') assert all(dim not in field.shape for dim in geom_ch) if isinstance(field, SampledField) and field.elements.shallow_equals(geometry) and not geom_ch: return field.values if geom_ch: sampled = [field._sample(p, **kwargs) for p in geometry.unstack(geom_ch.name)] return math.stack(sampled, geom_ch) else: return field._sample(geometry, **kwargs)
def shift(grid: phi.field._grid.CenteredGrid, offsets: tuple, stack_dim: Optional[phi.math._shape.Shape] = (shiftᶜ=None), dims=<function spatial>, pad=True)
-
Wraps :func:
math.shift
for CenteredGrid.Args
grid
- CenteredGrid:
offsets
- tuple:
stack_dim
- (Default value = 'shift')
Expand source code
def shift(grid: CenteredGrid, offsets: tuple, stack_dim: Optional[Shape] = channel('shift'), dims=spatial, pad=True): """ Wraps :func:`math.shift` for CenteredGrid. Args: grid: CenteredGrid: offsets: tuple: stack_dim: (Default value = 'shift') """ if pad: padding = grid.extrapolation new_bounds = grid.bounds else: padding = None max_lower_shift = min(offsets) if min(offsets) < 0 else 0 max_upper_shift = max(offsets) if max(offsets) > 0 else 0 w_lower = math.wrap([max_lower_shift if dim in dims else 0 for dim in grid.shape.spatial.names]) w_upper = math.wrap([max_upper_shift if dim in dims else 0 for dim in grid.shape.spatial.names]) new_bounds = Box(grid.box.lower - w_lower * grid.dx, grid.box.upper - w_upper * grid.dx) data = math.shift(grid.values, offsets, dims=dims, padding=padding, stack_dim=stack_dim) return [type(grid)(data[i], bounds=new_bounds, extrapolation=grid.extrapolation) for i in range(len(offsets))]
def sign(x) ‑> phi.math._tensors.Tensor
-
The sign of positive numbers is 1 and -1 for negative numbers. The sign of 0 is undefined.
Args
x
Tensor
orPhiTreeNode
Returns
Tensor
orPhiTreeNode
matchingx
.Expand source code
def sign(x) -> Tensor or PhiTreeNode: """ The sign of positive numbers is 1 and -1 for negative numbers. The sign of 0 is undefined. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` Returns: `Tensor` or `phi.math.magic.PhiTreeNode` matching `x`. """ return _backend_op1(x, Backend.sign)
def sin(x) ‑> phi.math._tensors.Tensor
-
Computes sin(x) of the
Tensor
orPhiTreeNode
x
.Expand source code
def sin(x) -> Tensor or PhiTreeNode: """ Computes *sin(x)* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.sin)
def solve_linear(f: Callable[[~X], ~Y], y: ~Y, solve: phi.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'
: Biconjugate gradient'biCGstab'
: Biconjugate gradient stabilized, first order'biCGstab(2)'
: Biconjugate gradient stabilized, second order
For maximum performance, compile
f
usingjit_compile_linear()
beforehand. Then, an optimized representation off
(such as a sparse matrix) will be used to solve the linear system.To obtain additional information about the performed solve, perform the solve within a
SolveTape
context. The used implementation can be obtained asSolveInfo.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
orPhiTreeNode
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)
- Linear function with
y
- Desired output of
f(x)
asTensor
orPhiTreeNode
. solve
Solve
object specifying optimization method, parameters and initial guess forx
.*f_args
- Positional arguments to be passed to
f
aftersolve.x0
. These arguments will not be solved for. Supports vararg mode or pass all arguments as atuple
. 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
asTensor
orPhiTreeNode
.
Raises
NotConverged
- If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
- If the solve failed prematurely.
Expand source code
def solve_linear(f: Callable[[X], Y] or Tensor, y: Y, solve: Solve[X, Y], *f_args, grad_for_f=False, f_kwargs: dict = None, **f_kwargs_) -> X: """ Solves the system of linear equations *f(x) = y* and returns *x*. This method will use the solver specified in `solve`. The following method identifiers are supported by all backends: * `'auto'`: Automatically choose a solver * `'CG'`: Conjugate gradient, only for symmetric and positive definite matrices. * `'CG-adaptive'`: Conjugate gradient with adaptive step size, only for symmetric and positive definite matrices. * `'biCG'`: Biconjugate gradient * `'biCGstab'`: Biconjugate gradient stabilized, first order * `'biCGstab(2)'`: Biconjugate gradient stabilized, second order For maximum performance, compile `f` using `jit_compile_linear()` beforehand. Then, an optimized representation of `f` (such as a sparse matrix) will be used to solve the linear system. To obtain additional information about the performed solve, perform the solve within a `SolveTape` context. The used implementation can be obtained as `SolveInfo.method`. The gradient of this operation will perform another linear solve with the parameters specified by `Solve.gradient_solve`. See Also: `solve_nonlinear()`, `jit_compile_linear()`. Args: f: One of the following: * Linear function with `Tensor` or `phi.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 `phi.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 `phi.math.magic.PhiTreeNode`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the solve failed prematurely. """ # --- Handle parameters --- f_kwargs = f_kwargs or {} f_kwargs.update(f_kwargs_) f_args = f_args[0] if len(f_args) == 1 and isinstance(f_args[0], tuple) else f_args # --- Get input and output tensors --- solve = solve.with_defaults('solve') y_tree, y_tensors = disassemble_tree(y) x0_tree, x0_tensors = disassemble_tree(solve.x0) assert solve.x0 is not None, "Please specify the initial guess as Solve(..., x0=initial_guess)" assert len(x0_tensors) == len(y_tensors) == 1, "Only single-tensor linear solves are currently supported" backend = choose_backend_t(*y_tensors, *x0_tensors) prefer_explicit = backend.supports(Backend.sparse_coo_tensor) or backend.supports(Backend.csr_matrix) or grad_for_f if isinstance(f, Tensor) or (isinstance(f, LinearFunction) and prefer_explicit): # Matrix solve if isinstance(f, LinearFunction): matrix, bias = f.sparse_matrix_and_bias(solve.x0, *f_args, **f_kwargs) else: matrix = f bias = 0 def _matrix_solve_forward(y, solve: Solve, matrix: Tensor, is_backprop=False): backend_matrix = native_matrix(matrix) pattern_dims_in = channel(**dual(matrix).untyped_dict).names pattern_dims_out = non_dual(matrix).names # batch dims can be sparse or batched matrices result = _linear_solve_forward(y, solve, backend_matrix, pattern_dims_in, pattern_dims_out, backend, is_backprop) return result # must return exactly `x` so gradient isn't computed w.r.t. other quantities _matrix_solve = attach_gradient_solve(_matrix_solve_forward, auxiliary_args='is_backprop,solve', matrix_adjoint=grad_for_f) return _matrix_solve(y - bias, solve, matrix) else: # Matrix-free solve f_args = cached(f_args) solve = cached(solve) assert not grad_for_f, f"grad_for_f=True can only be used for math.jit_compile_linear functions but got '{f_name(f)}'. Please decorate the linear function with @jit_compile_linear" def _function_solve_forward(y, solve: Solve, f_args: tuple, f_kwargs: dict = None, is_backprop=False): y_nest, (y_tensor,) = disassemble_tree(y) x0_nest, (x0_tensor,) = disassemble_tree(solve.x0) # active_dims = (y_tensor.shape & x0_tensor.shape).non_batch # assumes batch dimensions are not active batches = (y_tensor.shape & x0_tensor.shape).batch def native_lin_f(native_x, batch_index=None): if batch_index is not None and batches.volume > 1: native_x = backend.tile(backend.expand_dims(native_x), [batches.volume, 1]) x = assemble_tree(x0_nest, [reshaped_tensor(native_x, [batches, non_batch(x0_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(x0_tensor)], convert=False)]) y = f(x, *f_args, **f_kwargs) _, (y_tensor,) = disassemble_tree(y) y_native = reshaped_native(y_tensor, [batches, non_batch(y_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(y_tensor)]) if batch_index is not None and batches.volume > 1: y_native = y_native[batch_index] return y_native result = _linear_solve_forward(y, solve, native_lin_f, pattern_dims_in=non_batch(x0_tensor).names, pattern_dims_out=non_batch(y_tensor).names, backend=backend, is_backprop=is_backprop) return result # must return exactly `x` so gradient isn't computed w.r.t. other quantities _function_solve = attach_gradient_solve(_function_solve_forward, auxiliary_args='is_backprop,f_kwargs,solve', matrix_adjoint=grad_for_f) return _function_solve(y, solve, f_args, f_kwargs=f_kwargs)
def solve_nonlinear(f: Callable, y, solve: phi.math._optimize.Solve) ‑> phi.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 off
are optimized and must beTensor
orPhiTreeNode
. The output off
must matchy
. y
- Desired output of
f(x)
asTensor
orPhiTreeNode
. solve
Solve
object specifying optimization method, parameters and initial guess forx
.
Returns
x
- Solution fulfilling
f(x) = y
within specified tolerance asTensor
orPhiTreeNode
.
Raises
NotConverged
- If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
- If the solve failed prematurely.
Expand source code
def solve_nonlinear(f: Callable, y, solve: Solve) -> Tensor: """ Solves the non-linear equation *f(x) = y* by minimizing the norm of the residual. This method is limited to backends that support `jacobian()`, currently PyTorch, TensorFlow and Jax. To obtain additional information about the performed solve, use a `SolveTape`. See Also: `minimize()`, `solve_linear()`. Args: f: Function whose output is optimized to match `y`. All positional arguments of `f` are optimized and must be `Tensor` or `phi.math.magic.PhiTreeNode`. The output of `f` must match `y`. y: Desired output of `f(x)` as `Tensor` or `phi.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 `phi.math.magic.PhiTreeNode`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the solve failed prematurely. """ def min_func(x): diff = f(x) - y l2 = l2_loss(diff) return l2 if solve.preprocess_y is not None: y = solve.preprocess_y(y) from ._nd import l2_loss solve = solve.with_defaults('solve') tol = math.maximum(solve.rel_tol * l2_loss(y), solve.abs_tol) min_solve = copy_with(solve, abs_tol=tol, rel_tol=0, preprocess_y=None) return minimize(min_func, min_solve)
def spatial_gradient(field: phi.field._grid.CenteredGrid, gradient_extrapolation: Extrapolation = None, type: type = phi.field._grid.CenteredGrid, dims: Union[str, tuple, list, set, phi.math._shape.Shape, Callable] = <function spatial>, stack_dim: phi.math._shape.Shape = (vectorᶜ=None), order=2, implicit: phi.math._optimize.Solve = None)
-
Finite difference spatial_gradient.
This function can operate in two modes:
type=CenteredGrid
approximates the spatial_gradient at cell centers using central differencestype=StaggeredGrid
computes the spatial_gradient at face centers of neighbouring cells
Args
field
- centered grid of any number of dimensions (scalar field, vector field, tensor field)
gradient_extrapolation
- Extrapolation of the output
type
- either
CenteredGrid
orStaggeredGrid
dims
- Along which dimensions to compute the spatial gradient. Only supported when
type==CenteredGrid
. stack_dim
- Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions.
The
field
must not have a dimension of the same name. order
- Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
- When a
Solve
object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used.
Returns
spatial_gradient field of type
type
.Expand source code
def spatial_gradient(field: CenteredGrid, gradient_extrapolation: Extrapolation = None, type: type = CenteredGrid, dims: math.DimFilter = spatial, stack_dim: Shape = channel('vector'), order=2, implicit: Solve = None): """ Finite difference spatial_gradient. This function can operate in two modes: * `type=CenteredGrid` approximates the spatial_gradient at cell centers using central differences * `type=StaggeredGrid` computes the spatial_gradient at face centers of neighbouring cells Args: field: centered grid of any number of dimensions (scalar field, vector field, tensor field) gradient_extrapolation: Extrapolation of the output type: either `CenteredGrid` or `StaggeredGrid` dims: Along which dimensions to compute the spatial gradient. Only supported when `type==CenteredGrid`. stack_dim: Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions. The `field` must not have a dimension of the same name. order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. Returns: spatial_gradient field of type `type`. """ if gradient_extrapolation is None: gradient_extrapolation = field.extrapolation.spatial_gradient() extrap_map = {} if not implicit: if order == 2: if type == CenteredGrid: values, needed_shifts = [-1/2, 1/2], (-1, 1) else: values, needed_shifts = [-1, 1], (0, 1) elif order == 4: if type == CenteredGrid: values, needed_shifts = [1/12, -2/3, 2/3, -1/12], (-2, -1, 1, 2) else: values, needed_shifts = [1/24, -27/24, 27/24, -1/24], (-1, 0, 1, 2) else: raise NotImplementedError(f"explicit {order}th-order not supported") else: extrap_map_rhs = {} if order == 6: if type == CenteredGrid: values, needed_shifts = [-1/36, -14/18, 14/18, 1/36], (-2, -1, 1, 2) values_rhs, needed_shifts_rhs = [1/3, 1, 1/3], (-1, 0, 1) else: values, needed_shifts = [-17/186, -63/62, 63/62, 17/186], (-1, 0, 1, 2) extrap_map['symmetric'] = combine_by_direction(REFLECT, SYMMETRIC) values_rhs, needed_shifts_rhs = [9/62, 1, 9/62], (-1, 0, 1) extrap_map_rhs['symmetric'] = combine_by_direction(ANTIREFLECT, ANTISYMMETRIC) else: raise NotImplementedError(f"implicit {order}th-order not supported") base_widths = (abs(min(needed_shifts)), max(needed_shifts)) field.with_extrapolation(extrapolation.map(_ex_map_f(extrap_map), field.extrapolation)) # ToDo does this line do anything? if implicit: gradient_extrapolation = extrapolation.map(_ex_map_f(extrap_map_rhs), gradient_extrapolation) spatial_dims = field.shape.only(dims).names stack_dim = stack_dim._with_item_names((spatial_dims,)) if type == CenteredGrid: # ToDo if extrapolation == math.extrapolation.NONE, extend size by 1 # pad = 1 if extrapolation == math.extrapolation.NONE else 0 # bounds = Box(field.bounds.lower - field.dx, field.bounds.upper + field.dx) if extrapolation == math.extrapolation.NONE else field.bounds std_widths = (0, 0) if gradient_extrapolation == math.extrapolation.NONE: base_widths = (abs(min(needed_shifts))+1, max(needed_shifts)+1) std_widths = (1, 1) padded_components = [pad(field, {dim_: base_widths if dim_ == dim else std_widths for dim_ in spatial_dims}) for dim in spatial_dims] elif type == StaggeredGrid: assert spatial_dims == field.shape.spatial.names, f"spatial_gradient with type=StaggeredGrid requires dims=spatial, i.e. dims='{','.join(field.shape.spatial.names)}'" base_widths = (base_widths[0], base_widths[1]-1) padded_components = pad_for_staggered_output(field, gradient_extrapolation, field.shape.spatial.names, base_widths) else: raise ValueError(type) shifted_components = [shift(padded_component, needed_shifts, stack_dim=None, pad=False, dims=dim) for padded_component, dim in zip(padded_components, spatial_dims)] result_components = [sum([value * shift_ for value, shift_ in zip(values, shifted_component)]) / field.dx.vector[dim] for shifted_component, dim in zip(shifted_components, field.shape.spatial.names)] if type == CenteredGrid: result = stack(result_components, stack_dim) else: assert stack_dim.name == 'vector', f"spatial_gradient with type=StaggeredGrid requires stack_dim.name == 'vector' but got '{stack_dim.name}'" result = StaggeredGrid(math.stack([component.values for component in result_components], channel(vector=spatial_dims)), bounds=field.bounds, extrapolation=gradient_extrapolation) result = result.with_extrapolation(gradient_extrapolation) if implicit: implicit.x0 = result result = solve_linear(_lhs_for_implicit_scheme, result, solve=implicit, values_rhs=values_rhs, needed_shifts_rhs=needed_shifts_rhs, stack_dim=stack_dim, staggered_output=type != CenteredGrid) if type == CenteredGrid and gradient_extrapolation == math.extrapolation.NONE: result = result.with_bounds(Box(field.bounds.lower - field.dx, field.bounds.upper + field.dx)) else: result = result.with_bounds(field.bounds) return result
def sqrt(x) ‑> phi.math._tensors.Tensor
-
Computes sqrt(x) of the
Tensor
orPhiTreeNode
x
.Expand source code
def sqrt(x) -> Tensor or PhiTreeNode: """ Computes *sqrt(x)* of the `Tensor` or `phi.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.sqrt)
def stack(fields, dim: phi.math._shape.Shape, dim_bounds: phi.geom._box.Box = None)
-
Stacks the given
SampledField
s alongdim
.See Also:
concat()
.Args
fields
- List of matching
SampledField
instances. dim
- Stack dimension as
Shape
. Size is ignored. dim_bounds
Box
defining the physical size fordim
.
Returns
SampledField
matching stacked fields.Expand source code
def stack(fields, dim: Shape, dim_bounds: Box = None): """ Stacks the given `SampledField`s along `dim`. See Also: `concat()`. Args: fields: List of matching `SampledField` instances. dim: Stack dimension as `Shape`. Size is ignored. dim_bounds: `Box` defining the physical size for `dim`. Returns: `SampledField` matching stacked fields. """ assert all(isinstance(f, SampledField) for f in fields), f"All fields must be SampledFields of the same type but got {fields}" assert all(isinstance(f, type(fields[0])) for f in fields), f"All fields must be SampledFields of the same type but got {fields}" if any(f.extrapolation != fields[0].extrapolation for f in fields): raise NotImplementedError("Concatenating extrapolations not supported") if isinstance(fields[0], Grid): values = math.stack([f.values for f in fields], dim) if spatial(dim): if dim_bounds is None: dim_bounds = Box(**{dim.name: len(fields)}) return type(fields[0])(values, extrapolation=fields[0].extrapolation, bounds=fields[0].bounds * dim_bounds) else: return fields[0].with_values(values) elif isinstance(fields[0], PointCloud): elements = geom.stack([f.elements for f in fields], dim) values = math.stack([f.values for f in fields], dim) return PointCloud(elements=elements, values=values, extrapolation=fields[0].extrapolation, add_overlapping=fields[0]._add_overlapping, bounds=fields[0]._bounds) raise NotImplementedError(type(fields[0]))
def stagger(field: phi.field._grid.CenteredGrid, face_function: Callable, extrapolation: float, type: type = phi.field._grid.StaggeredGrid)
-
Creates a new grid by evaluating
face_function
given two neighbouring cells. One layer of missing cells is inferred from the extrapolation.This method returns a Field of type
type
which must be either StaggeredGrid or CenteredGrid. When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells. When returning a CenteredGrid, the new grid has the same resolution asfield
.Args
field
- centered grid
face_function
- function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
extrapolation
- extrapolation mode of the returned grid. Has no effect on the values.
type
- one of (StaggeredGrid, CenteredGrid)
field
- CenteredGrid:
face_function
- Callable:
extrapolation
- math.extrapolation.Extrapolation:
type
- type: (Default value = StaggeredGrid)
Returns
grid of type matching the
type
argumentExpand source code
def stagger(field: CenteredGrid, face_function: Callable, extrapolation: float or math.extrapolation.Extrapolation, type: type = StaggeredGrid): """ Creates a new grid by evaluating `face_function` given two neighbouring cells. One layer of missing cells is inferred from the extrapolation. This method returns a Field of type `type` which must be either StaggeredGrid or CenteredGrid. When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells. When returning a CenteredGrid, the new grid has the same resolution as `field`. Args: field: centered grid face_function: function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor extrapolation: extrapolation mode of the returned grid. Has no effect on the values. type: one of (StaggeredGrid, CenteredGrid) field: CenteredGrid: face_function: Callable: extrapolation: math.extrapolation.Extrapolation: type: type: (Default value = StaggeredGrid) Returns: grid of type matching the `type` argument """ extrapolation = as_extrapolation(extrapolation) all_lower = [] all_upper = [] if type == StaggeredGrid: for dim in field.resolution.names: valid_lo, valid_up = extrapolation.valid_outer_faces(dim) if valid_lo and valid_up: width_lower, width_upper = {dim: (1, 0)}, {dim: (0, 1)} elif valid_lo and not valid_up: width_lower, width_upper = {dim: (1, -1)}, {dim: (0, 0)} elif not valid_lo and valid_up: width_lower, width_upper = {dim: (0, 0)}, {dim: (-1, 1)} else: width_lower, width_upper = {dim: (0, -1)}, {dim: (-1, 0)} all_lower.append(math.pad(field.values, width_lower, field.extrapolation, bounds=field.bounds)) all_upper.append(math.pad(field.values, width_upper, field.extrapolation, bounds=field.bounds)) all_upper = math.stack(all_upper, channel('vector')) all_lower = math.stack(all_lower, channel('vector')) values = face_function(all_lower, all_upper) result = StaggeredGrid(values, bounds=field.bounds, extrapolation=extrapolation) assert result.shape.spatial == field.shape.spatial return result elif type == CenteredGrid: left, right = math.shift(field.values, (-1, 1), padding=field.extrapolation, stack_dim=channel('vector')) values = face_function(left, right) return CenteredGrid(values, bounds=field.bounds, extrapolation=extrapolation) else: raise ValueError(type)
def stop_gradient(x)
-
Disables gradients for the given tensor. This may switch off the gradients for
x
itself or create a copy ofx
with disabled gradients.Implementations:
- PyTorch:
x.detach()
- TensorFlow:
tf.stop_gradient
- Jax:
jax.lax.stop_gradient
Args
x
Tensor
orPhiTreeNode
for which gradients should be disabled.
Returns
Copy of
x
.Expand source code
def stop_gradient(x): """ Disables gradients for the given tensor. This may switch off the gradients for `x` itself or create a copy of `x` with disabled gradients. Implementations: * PyTorch: [`x.detach()`](https://pytorch.org/docs/stable/autograd.html#torch.Tensor.detach) * TensorFlow: [`tf.stop_gradient`](https://www.tensorflow.org/api_docs/python/tf/stop_gradient) * Jax: [`jax.lax.stop_gradient`](https://jax.readthedocs.io/en/latest/_autosummary/jax.lax.stop_gradient.html) Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` for which gradients should be disabled. Returns: Copy of `x`. """ if isinstance(x, Tensor): return x._op1(lambda native: choose_backend(native).stop_gradient(native)) elif isinstance(x, PhiTreeNode): nest, values = disassemble_tree(x) new_values = [stop_gradient(v) for v in values] return assemble_tree(nest, new_values) else: return wrap(choose_backend(x).stop_gradient(x))
- PyTorch:
def support(field: phi.field._field.SampledField, list_dim: phi.math._shape.Shape = (nonzeroⁱ=None)) ‑> phi.math._tensors.Tensor
-
Returns the points at which the field values are non-zero.
Args
field
SampledField
list_dim
- Dimension to list the non-zero values.
Returns
Tensor
with shape(list_dim, vector)
Expand source code
def support(field: SampledField, list_dim: Shape or str = instance('nonzero')) -> Tensor: """ Returns the points at which the field values are non-zero. Args: field: `SampledField` list_dim: Dimension to list the non-zero values. Returns: `Tensor` with shape `(list_dim, vector)` """ return field.points[math.nonzero(field.values, list_dim=list_dim)]
def to_float(x) ‑> phi.math._tensors.Tensor
-
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 usingwith math.precision()
.See the
phi.math
module documentation at https://tum-pbs.github.io/PhiFlow/Math.htmlSee Also:
cast()
.Args
x
Tensor
orPhiTreeNode
to convert
Returns
Tensor
orPhiTreeNode
matchingx
.Expand source code
def to_float(x) -> Tensor or PhiTreeNode: """ 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 `phi.math` module documentation at https://tum-pbs.github.io/PhiFlow/Math.html See Also: `cast()`. Args: x: `Tensor` or `phi.math.magic.PhiTreeNode` to convert Returns: `Tensor` or `phi.math.magic.PhiTreeNode` matching `x`. """ return _backend_op1(x, Backend.to_float)
def to_int32(x) ‑> phi.math._tensors.Tensor
-
Converts the
Tensor
orPhiTreeNode
x
to 32-bit integer.Expand source code
def to_int32(x) -> Tensor or PhiTreeNode: """ Converts the `Tensor` or `phi.math.magic.PhiTreeNode` `x` to 32-bit integer. """ return _backend_op1(x, Backend.to_int32)
def to_int64(x) ‑> phi.math._tensors.Tensor
-
Converts the
Tensor
orPhiTreeNode
x
to 64-bit integer.Expand source code
def to_int64(x) -> Tensor or PhiTreeNode: """ Converts the `Tensor` or `phi.math.magic.PhiTreeNode` `x` to 64-bit integer. """ return _backend_op1(x, Backend.to_int64)
def unstack(value, dim: Union[str, tuple, list, set, phi.math._shape.Shape, Callable])
-
Un-stacks a
Sliceable
along one or multiple dimensions.If multiple dimensions are given, the order of elements will be according to the dimension order in
dim
, i.e. elements along the last dimension will be neighbors in the returnedtuple
.See Also:
slice_()
.Args
value
Shapable
, such asTensor
dim
- Dimensions as
Shape
or comma-separatedstr
or dimension type, i.e.channel
,spatial
,instance
,batch
.
Returns
tuple
ofTensor
objects.Examples
>>> unstack(expand(0, spatial(x=5)), 'x') (0.0, 0.0, 0.0, 0.0, 0.0)
Expand source code
def unstack(value, dim: DimFilter): """ Un-stacks a `Sliceable` along one or multiple dimensions. If multiple dimensions are given, the order of elements will be according to the dimension order in `dim`, i.e. elements along the last dimension will be neighbors in the returned `tuple`. See Also: `phi.math.slice`. Args: value: `phi.math.magic.Shapable`, such as `phi.math.Tensor` dim: Dimensions as `Shape` or comma-separated `str` or dimension type, i.e. `channel`, `spatial`, `instance`, `batch`. Returns: `tuple` of `Tensor` objects. Examples: >>> unstack(expand(0, spatial(x=5)), 'x') (0.0, 0.0, 0.0, 0.0, 0.0) """ assert isinstance(value, Sliceable) and isinstance(value, Shaped), f"Cannot unstack {type(value).__name__}. Must be Sliceable and Shaped, see https://tum-pbs.github.io/PhiFlow/phi/math/magic.html" dims = shape(value).only(dim) assert dims.rank > 0, "unstack() requires at least one dimension" if dims.rank == 1: if hasattr(value, '__unstack__'): result = value.__unstack__(dims.names) if result is not NotImplemented: assert isinstance(result, tuple), f"__unstack__ must return a tuple but got {type(result)}" assert all([isinstance(item, Sliceable) for item in result]), f"__unstack__ must return a tuple of Sliceable objects but not all items were sliceable in {result}" return result return tuple([slice_(value, {dims.name: i}) for i in range(dims.size)]) else: # multiple dimensions if hasattr(value, '__pack_dims__'): packed_dim = batch('_unstack') value_packed = value.__pack_dims__(dims.names, packed_dim, pos=None) if value_packed is not NotImplemented: return unstack(value_packed, packed_dim) unstack_dim = _any_uniform_dim(dims) first_unstacked = unstack(value, unstack_dim) inner_unstacked = [unstack(v, dims.without(unstack_dim)) for v in first_unstacked] return sum(inner_unstacked, ())
def upsample2x(grid: ~GridType) ‑> ~GridType
-
Increases the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.
See Also:
downsample2x()
.Args
grid
CenteredGrid
orStaggeredGrid
.
Returns
Grid
of same type asgrid
.Expand source code
def upsample2x(grid: GridType) -> GridType: """ Increases the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation. See Also: `downsample2x()`. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: `Grid` of same type as `grid`. """ if isinstance(grid, CenteredGrid): values = math.upsample2x(grid.values, grid.extrapolation) return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation) elif isinstance(grid, StaggeredGrid): raise NotImplementedError() else: raise ValueError(type(grid))
def vec_abs(field: phi.field._field.SampledField)
-
See
vec_abs()
Expand source code
def vec_length(field: SampledField): """ See `phi.math.vec_abs()` """ assert isinstance(field, SampledField), f"SampledField required but got {type(field).__name__}" if isinstance(field, StaggeredGrid): field = field.at_centers() return field.with_values(math.vec_abs(field.values))
def vec_length(field: phi.field._field.SampledField)
-
See
vec_abs()
Expand source code
def vec_length(field: SampledField): """ See `phi.math.vec_abs()` """ assert isinstance(field, SampledField), f"SampledField required but got {type(field).__name__}" if isinstance(field, StaggeredGrid): field = field.at_centers() return field.with_values(math.vec_abs(field.values))
def vec_squared(field: phi.field._field.SampledField)
-
See
vec_squared()
Expand source code
def vec_squared(field: SampledField): """ See `phi.math.vec_squared()` """ if isinstance(field, StaggeredGrid): field = field.at_centers() return field.with_values(math.vec_squared(field.values))
def where(mask: phi.field._field.Field, field_true: phi.field._field.Field, field_false: phi.field._field.Field) ‑> ~SampledFieldType
-
Element-wise where operation. Picks the value of
field_true
wheremask=1 / True
and the value offield_false
wheremask=0 / False
.The fields are automatically resampled if necessary, preferring the sample points of
mask()
. At least one of the arguments must be aSampledField
.Args
Returns
Expand source code
def where(mask: Field or Geometry or float, field_true: Field or float, field_false: Field or float) -> SampledFieldType: """ Element-wise where operation. Picks the value of `field_true` where `mask=1 / True` and the value of `field_false` where `mask=0 / False`. The fields are automatically resampled if necessary, preferring the sample points of `mask`. At least one of the arguments must be a `SampledField`. Args: mask: `Field` or `Geometry` object. field_true: `Field` field_false: `Field` Returns: `SampledField` """ field_true, field_false, mask = _auto_resample(field_true, field_false, mask) values = math.where(mask.values, field_true.values, field_false.values) return field_true.with_values(values)
def write(field: phi.field._field.SampledField, file: str)
-
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
orTensor
of string type. Iffile
is a tensor, the dimensions offield
are matched to the dimensions offile
. Dimensions offile
that are missing infield
result in data duplication. Dimensions offield
that are missing infile
result in larger files.
Expand source code
def write(field: SampledField, file: str or Tensor): """ Writes a field to disc using a NumPy file format. Depending on `file`, the data may be split up into multiple files. All characteristics of the field are serialized so that it can be fully restored using `read()`. See Also: `read()` Args: field: Field to be saved. file: Single file as `str` or `Tensor` of string type. If `file` is a tensor, the dimensions of `field` are matched to the dimensions of `file`. Dimensions of `file` that are missing in `field` result in data duplication. Dimensions of `field` that are missing in `file` result in larger files. """ if isinstance(file, str): write_single_field(field, file) elif isinstance(file, Tensor): if file.rank == 0: write_single_field(field, file.native()) else: dim = file.shape.names[0] files = file.unstack(dim) fields = field.dimension(dim).unstack(file.shape.get_size(dim)) for field_, file_ in zip(fields, files): write(field_, file_) else: raise ValueError(file)
Classes
class AngularVelocity (location: phi.math._tensors.Tensor, strength: phi.math._tensors.Tensor = 1.0, falloff: Callable = None, component: str = None)
-
Model of a single vortex or set of vortices. The falloff of the velocity magnitude can be controlled.
Without a specified falloff, the velocity increases linearly with the distance from the vortex center. This is the case with rotating rigid bodies, for example.
Expand source code
class AngularVelocity(Field): """ Model of a single vortex or set of vortices. The falloff of the velocity magnitude can be controlled. Without a specified falloff, the velocity increases linearly with the distance from the vortex center. This is the case with rotating rigid bodies, for example. """ def __init__(self, location: Tensor or tuple or list or Number, strength: Tensor or Number = 1.0, falloff: Callable = None, component: str = None): location = wrap(location) strength = wrap(strength) assert location.shape.channel.names == ('vector',), "location must have a single channel dimension called 'vector'" assert location.shape.spatial.is_empty, "location tensor cannot have any spatial dimensions" assert not instance(location), "AngularVelocity does not support instance dimensions" self.location = location self.strength = strength self.falloff = falloff self.component = component spatial_names = location.vector.item_names assert spatial_names is not None, "location.vector must list spatial dimensions as item names" self._shape = location.shape & spatial(**{dim: 1 for dim in spatial_names}) def _sample(self, geometry: Geometry, **kwargs) -> Tensor: points = geometry.center distances = points - self.location strength = self.strength if self.falloff is None else self.strength * self.falloff(distances) velocity = math.cross_product(strength, distances) velocity = math.sum(velocity, self.location.shape.batch.without(points.shape)) if self.component: velocity = velocity.vector[self.component] return velocity @property def shape(self) -> Shape: return self._shape def __getitem__(self, item: dict): assert all(dim == 'vector' for dim in item), f"Cannot slice AngularVelocity with {item}" if 'vector' in item: assert item['vector'] == 0 or self.component is None component = self.shape.spatial.names[item['vector']] return AngularVelocity(self.location, self.strength, self.falloff, component) else: return self
Ancestors
- phi.field._field.Field
Instance variables
var shape : phi.math._shape.Shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self) -> Shape: return self._shape
class CenteredGrid (values: Any = 0.0, extrapolation: Any = 0.0, bounds: phi.geom._box.Box = None, resolution: int = None, **resolution_: int)
-
N-dimensional grid with values sampled at the cell centers. A centered grid is defined through its
CenteredGrid.values
Tensor
, itsCenteredGrid.bounds
Box
describing the physical size, and itsCenteredGrid.extrapolation
(Extrapolation
).Centered grids support batch, spatial and channel dimensions.
See Also:
StaggeredGrid
,Grid
,SampledField
,Field
, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlArgs
values
-
Values to use for the grid. Has to be one of the following:
Geometry
: sets inside values to 1, outside to 0Field
: resamples the Field to the staggered sample pointsNumber
: uses the value for all sample pointstuple
orlist
: interprets the sequence as vector, used for all sample pointsTensor
compatible with grid dims: uses tensor values as grid values- Function
values(x)
wherex
is aTensor
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
,Tensor
,Extrapolation
. bounds
- Physical size and location of the grid as
Box
. If the resolution is determined throughresolution
ofvalues
, afloat
can be passed forbounds
to create a unit box. resolution
- Grid resolution as purely spatial
Shape
. Ifbounds
is given as aBox
, the resolution may be specified as anint
to be equal along all axes. **resolution_
- Spatial dimensions as keyword arguments. Typically either
resolution
orspatial_dims
are specified.
Expand source code
class CenteredGrid(Grid): """ N-dimensional grid with values sampled at the cell centers. A centered grid is defined through its `CenteredGrid.values` `phi.math.Tensor`, its `CenteredGrid.bounds` `phi.geom.Box` describing the physical size, and its `CenteredGrid.extrapolation` (`phi.math.extrapolation.Extrapolation`). Centered grids support batch, spatial and channel dimensions. See Also: `StaggeredGrid`, `Grid`, `SampledField`, `Field`, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ def __init__(self, values: Any = 0., extrapolation: Any = 0., bounds: Box or float = None, resolution: int or Shape = None, **resolution_: int or Tensor): """ Args: values: Values to use for the grid. Has to be one of the following: * `phi.geom.Geometry`: sets inside values to 1, outside to 0 * `Field`: resamples the Field to the staggered sample points * `Number`: uses the value for all sample points * `tuple` or `list`: interprets the sequence as vector, used for all sample points * `phi.math.Tensor` compatible with grid dims: uses tensor values as grid values * Function `values(x)` where `x` is a `phi.math.Tensor` representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function. extrapolation: The grid extrapolation determines the value outside the `values` tensor. Allowed types: `float`, `phi.math.Tensor`, `phi.math.extrapolation.Extrapolation`. bounds: Physical size and location of the grid as `phi.geom.Box`. If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box. resolution: Grid resolution as purely spatial `phi.math.Shape`. If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes. **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified. """ if resolution is None and not resolution_: assert isinstance(values, math.Tensor), "Grid resolution must be specified when 'values' is not a Tensor." resolution = values.shape.spatial bounds = _get_bounds(bounds, resolution) elements = GridCell(resolution, bounds) else: resolution = _get_resolution(resolution, resolution_, bounds) bounds = _get_bounds(bounds, resolution) elements = GridCell(resolution, bounds) if isinstance(values, math.Tensor): values = math.expand(values, resolution) elif isinstance(values, Geometry): values = reduce_sample(values, elements) elif isinstance(values, Field): values = reduce_sample(values, elements) elif callable(values): values = _sample_function(values, elements) else: if isinstance(values, (tuple, list)) and len(values) == resolution.rank: values = math.tensor(values, channel(vector=resolution.names)) values = math.expand(math.tensor(values), resolution) if values.dtype.kind not in (float, complex): values = math.to_float(values) assert resolution.spatial_rank == bounds.spatial_rank, f"Resolution {resolution} does not match bounds {bounds}" Grid.__init__(self, elements, values, extrapolation, values.shape.spatial, bounds) def __getitem__(self, item): item = slicing_dict(self, item) if not item: return self values = self._values[item] extrapolation = self._extrapolation[item] keep_dims = [dim for dim in self.resolution.names if dim not in item or not isinstance(item[dim], int)] bounds = self.elements[item].bounds[{'vector': keep_dims}] return CenteredGrid(values, bounds=bounds, extrapolation=extrapolation) def _sample(self, geometry: Geometry, **kwargs) -> Tensor: if geometry == self.bounds: return math.mean(self._values, self._resolution) if isinstance(geometry, GeometryStack): sampled = [self._sample(g, **kwargs) for g in geometry.geometries] return math.stack(sampled, geometry.geometries.shape) if isinstance(geometry, GridCell): if self.elements == geometry: return self.values elif math.close(self.dx, geometry.size): if all([math.close(offset, geometry.half_size) or math.close(offset, 0) for offset in math.abs(self.bounds.lower - geometry.bounds.lower)]): dyadic_interpolated = self._dyadic_interplate(geometry.resolution, geometry.bounds, **kwargs) if dyadic_interpolated is not NotImplemented: return dyadic_interpolated if 'order' in kwargs and kwargs['order'] != 2: raise NotImplementedError(f"Only 6th-order implicit and 2nd-order resampling supported but got order={kwargs['order']}") fast_resampled = self._shift_resample(geometry.resolution, geometry.bounds) if fast_resampled is not NotImplemented: return fast_resampled points = geometry.center local_points = self.box.global_to_local(points) * self.resolution - 0.5 resampled_values = math.grid_sample(self.values, local_points, self.extrapolation, bounds=self.bounds) if isinstance(self._extrapolation, FieldEmbedding): if isinstance(geometry, GridCell) and ((geometry.bounds.upper <= self.bounds.upper).all or (geometry.bounds.lower >= self.bounds.lower).all): # geometry is a subgrid of self return resampled_values else: # otherwise we also sample the extrapolation Field ext_values = self._extrapolation.field._sample(geometry, **kwargs) inside = self.bounds.lies_inside(points) return math.where(inside, resampled_values, ext_values) return resampled_values def _dyadic_interplate(self, resolution: Shape, bounds: Box, order=2, implicit: Solve = None): offsets = bounds.lower - self.bounds.lower interpolation_dirs = [0 if math.close(offset, 0) else int(math.sign(offset)) for offset in offsets] return _dyadic_interpolate(self.values, interpolation_dirs, self.extrapolation, order, implicit) def _shift_resample(self, resolution: Shape, bounds: Box, threshold=1e-5, max_padding=20): assert math.all_available(bounds.lower, bounds.upper), "Shift resampling requires 'bounds' to be available." lower = math.to_int32(math.ceil(math.maximum(0, self.box.lower - bounds.lower) / self.dx - threshold)) upper = math.to_int32(math.ceil(math.maximum(0, bounds.upper - self.box.upper) / self.dx - threshold)) total_padding = (math.sum(lower) + math.sum(upper)).numpy() if total_padding > max_padding and self.extrapolation.native_grid_sample_mode: return NotImplemented elif total_padding > 0: from phi.field import pad padded = pad(self, {dim: (int(lower[i]), int(upper[i])) for i, dim in enumerate(self.shape.spatial.names)}) grid_box, grid_resolution, grid_values = padded.box, padded.resolution, padded.values else: grid_box, grid_resolution, grid_values = self.box, self.resolution, self.values origin_in_local = grid_box.global_to_local(bounds.lower) * grid_resolution data = math.sample_subgrid(grid_values, origin_in_local, resolution) return data def closest_values(self, points: Geometry): local_points = self.box.global_to_local(points.center) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation)
Ancestors
- phi.field._grid.Grid
- phi.field._field.SampledField
- phi.field._field.Field
Methods
def closest_values(self, points: phi.geom._geom.Geometry)
-
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension namedvector
. 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 inpoints
, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions'closest_<dim>'
where<dim>
refers to the name of a spatial dimension.Expand source code
def closest_values(self, points: Geometry): local_points = self.box.global_to_local(points.center) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation)
class Field
-
Base class for all fields.
Important implementations:
- CenteredGrid
- StaggeredGrid
- PointCloud
- Noise
See the
phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlExpand source code
class Field: """ Base class for all fields. Important implementations: * CenteredGrid * StaggeredGrid * PointCloud * Noise See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ @property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ raise NotImplementedError @property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ raise NotImplementedError @property def bounds(self) -> Box: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ raise NotImplementedError def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor: """ For internal use only. Use `sample()` instead. """ raise NotImplementedError(self) def at(self, representation: 'SampledField', keep_extrapolation=False, **kwargs) -> 'SampledFieldType': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ return resample(self, representation, keep_extrapolation, **kwargs) def __matmul__(self, other: 'SampledField'): # value @ representation # Deprecated. Use `resample(value, field)` instead. warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) return self.at(other, keep_extrapolation=False) def __rmatmul__(self, other): # values @ representation if not isinstance(self, SampledField): return NotImplemented if isinstance(other, (Geometry, Number, tuple, list)): return self.with_values(other) return NotImplemented def __rshift__(self, other): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) return self.at(other, keep_extrapolation=False) def __rrshift__(self, other): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) if not isinstance(self, SampledField): return NotImplemented if isinstance(other, (Geometry, float, int, complex, tuple, list)): return self.with_values(other) return NotImplemented def __getitem__(self, item) -> 'Field': """ Access a slice of the Field. The returned `Field` may be of a different type than `self`. Args: item: `dict` mapping dimensions (`str`) to selections (`int` or `slice`) or other supported type, such as `int` or `str`. Returns: Sliced `Field`. """ raise NotImplementedError(self) def __getattr__(self, name: str) -> BoundDim: return BoundDim(self, name) def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name) def __repr__(self): return f"{self.__class__.__name__} {self.shape}"
Subclasses
- phi.field._angular_velocity.AngularVelocity
- phi.field._field.SampledField
- phi.field._mask.HardGeometryMask
- phi.field._noise.Noise
Instance variables
var bounds : phi.geom._box.Box
-
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
.Expand source code
@property def bounds(self) -> Box: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ raise NotImplementedError
var shape : phi.math._shape.Shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ raise NotImplementedError
var spatial_rank : int
-
Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the
data
.Expand source code
@property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ raise NotImplementedError
Methods
def at(self, representation: SampledField, keep_extrapolation=False, **kwargs) ‑> ~SampledFieldType
-
Short for
resample()(self, representation)
See Also
resample()
.Returns
Field object of same type as
representation
Expand source code
def at(self, representation: 'SampledField', keep_extrapolation=False, **kwargs) -> 'SampledFieldType': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ return resample(self, representation, keep_extrapolation, **kwargs)
def dimension(self, name: str)
-
Returns a reference to one of the dimensions of this field.
The dimension reference can be used the same way as a
Tensor
dimension reference. Notable properties and methods of a dimension reference are: indexing using[index]
,unstack()
,size
,exists
,is_batch
,is_spatial
,is_channel
.A shortcut to calling this function is the syntax
field.<dim_name>
which callsfield.dimension(<dim_name>)
.Args
name
- dimension name
Returns
dimension reference
Expand source code
def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name)
class Grid
-
Base class for
CenteredGrid
andStaggeredGrid
.Args
elements
- Geometry object specifying the sample points and sizes
values
- values corresponding to elements
extrapolation
- values outside elements
Expand source code
class Grid(SampledField): """ Base class for `CenteredGrid` and `StaggeredGrid`. """ def __init__(self, elements: Geometry, values: Tensor, extrapolation: float or Extrapolation, resolution: Shape or int, bounds: Box or float): assert isinstance(bounds, Box) assert isinstance(resolution, Shape) if bounds.size.vector.item_names is None: with NUMPY: bounds = bounds.shifted(math.zeros(channel(vector=spatial(values).names))) SampledField.__init__(self, elements, values, extrapolation, bounds) assert values.shape.spatial_rank == elements.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.spatial_rank == bounds.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.instance_rank == 0, f"Instance dimensions not supported for grids. Got values with shape {values.shape}" self._resolution = resolution def closest_values(self, points: Geometry): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ raise NotImplementedError(self) def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor: raise NotImplementedError(self) def with_values(self, values): if isinstance(values, math.Tensor): bounds = self.bounds.project(*values.shape.spatial.names) return type(self)(values, extrapolation=self.extrapolation, bounds=bounds) else: return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds, resolution=self._resolution) def with_extrapolation(self, extrapolation: Extrapolation): return type(self)(self.values, extrapolation=extrapolation, bounds=self.bounds) def with_bounds(self, bounds: Box): return type(self)(self.values, extrapolation=self.extrapolation, bounds=bounds) def __value_attrs__(self): return '_values', '_extrapolation' def __variable_attrs__(self): return '_values', def __expand__(self, dims: Shape, **kwargs) -> 'Grid': return self.with_values(math.expand(self.values, dims, **kwargs)) def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'Grid': for dim in dims: if dim in self._resolution: return NotImplemented values = math.rename_dims(self.values, dims, new_dims) extrapolation = math.rename_dims(self.extrapolation, dims, new_dims, **kwargs) bounds = math.rename_dims(self.bounds, dims, new_dims, **kwargs) return type(self)(values, extrapolation=extrapolation, bounds=bounds, resolution=self._resolution) def __eq__(self, other): if not type(self) == type(other): return False if not (self._bounds == other._bounds and self._resolution == other._resolution and self._extrapolation == other._extrapolation): return False if self.values is None: return other.values is None if other.values is None: return False if not math.all_available(self.values) or not math.all_available(other.values): # tracers involved if math.all_available(self.values) != math.all_available(other.values): return False else: # both tracers return self.values.shape == other.values.shape return bool((self.values == other.values).all) def __getitem__(self, item) -> 'Grid': raise NotImplementedError(self) @property def shape(self): return self._resolution & self._values.shape.non_spatial @property def bounds(self) -> Box: return self._bounds @property def box(self) -> Box: return self._bounds @property def resolution(self) -> Shape: return self._resolution @property def dx(self) -> Tensor: return self.bounds.size / self.resolution def __repr__(self): if self._values is not None: return f"{self.__class__.__name__}[{self.shape.non_spatial & self.resolution}, size={self.box.size}, extrapolation={self._extrapolation}]" else: return f"{self.__class__.__name__}[{self.resolution}, size={self.box.size}, extrapolation={self._extrapolation}]" def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ return self.values
Ancestors
- phi.field._field.SampledField
- phi.field._field.Field
Subclasses
- phi.field._grid.CenteredGrid
- phi.field._grid.StaggeredGrid
Instance variables
var bounds : phi.geom._box.Box
-
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
.Expand source code
@property def bounds(self) -> Box: return self._bounds
var box : phi.geom._box.Box
-
Expand source code
@property def box(self) -> Box: return self._bounds
var dx : phi.math._tensors.Tensor
-
Expand source code
@property def dx(self) -> Tensor: return self.bounds.size / self.resolution
var resolution : phi.math._shape.Shape
-
Expand source code
@property def resolution(self) -> Shape: return self._resolution
var shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self): return self._resolution & self._values.shape.non_spatial
Methods
def closest_values(self, points: phi.geom._geom.Geometry)
-
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension namedvector
. 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 inpoints
, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions'closest_<dim>'
where<dim>
refers to the name of a spatial dimension.Expand source code
def closest_values(self, points: Geometry): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ raise NotImplementedError(self)
def uniform_values(self)
-
Returns a uniform tensor containing
values
.For periodic grids, which always have a uniform value tensor, `values' is returned directly. If
values
is not uniform, it is padded as inStaggeredGrid.staggered_tensor()
.Expand source code
def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ return self.values
def with_bounds(self, bounds: phi.geom._box.Box)
-
Expand source code
def with_bounds(self, bounds: Box): return type(self)(self.values, extrapolation=self.extrapolation, bounds=bounds)
def with_extrapolation(self, extrapolation: Extrapolation)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_extrapolation(self, extrapolation: Extrapolation): return type(self)(self.values, extrapolation=extrapolation, bounds=self.bounds)
def with_values(self, values)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_values(self, values): if isinstance(values, math.Tensor): bounds = self.bounds.project(*values.shape.spatial.names) return type(self)(values, extrapolation=self.extrapolation, bounds=bounds) else: return type(self)(values, extrapolation=self.extrapolation, bounds=self.bounds, resolution=self._resolution)
class HardGeometryMask (geometry: phi.geom._geom.Geometry)
-
Deprecated since version 1.3. Use
mask()
orresample()
instead.Expand source code
class HardGeometryMask(Field): """ Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry): warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2) assert isinstance(geometry, Geometry) self.geometry = geometry @property def shape(self): return self.geometry.shape.non_channel def _sample(self, geometry: Geometry, **kwargs) -> Tensor: return math.to_float(self.geometry.lies_inside(geometry.center)) def __getitem__(self, item: dict): return HardGeometryMask(self.geometry[item])
Ancestors
- phi.field._field.Field
Subclasses
- phi.field._mask.SoftGeometryMask
Instance variables
var shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self): return self.geometry.shape.non_channel
class Noise (*shape: phi.math._shape.Shape, scale=10.0, smoothness=1.0, **channel_dims)
-
Generates random noise fluctuations which can be configured in physical size and smoothness. Each time values are sampled from a Noise field, a new noise field is generated.
Noise is typically used as an initializer for CenteredGrids or StaggeredGrids.
Args
shape
- Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid.
scale
- Size of noise fluctuations in physical units.
smoothness
- Determines how quickly high frequencies die out.
**dims
- Additional dimensions, added to
shape
.
Expand source code
class Noise(Field): """ Generates random noise fluctuations which can be configured in physical size and smoothness. Each time values are sampled from a Noise field, a new noise field is generated. Noise is typically used as an initializer for CenteredGrids or StaggeredGrids. """ def __init__(self, *shape: math.Shape, scale=10., smoothness=1.0, **channel_dims): """ Args: shape: Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid. scale: Size of noise fluctuations in physical units. smoothness: Determines how quickly high frequencies die out. **dims: Additional dimensions, added to `shape`. """ self.scale = scale self.smoothness = smoothness self._shape = math.concat_shapes(*shape, channel(**channel_dims)) @property def shape(self): return self._shape def _sample(self, geometry: Geometry, **kwargs) -> Tensor: if isinstance(geometry, GridCell): return self.grid_sample(geometry.resolution, geometry.grid_size) raise NotImplementedError(f"{type(geometry)} not supported. Only GridCell allowed.") def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None): shape = (self._shape if shape is None else shape) & resolution for dim in channel(self._shape): if dim.item_names[0] is None: warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning) shape &= channel(**{dim.name: resolution.names}) rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape)) # Note: there is no complex32 # --- Compute 1 / k^2 --- k_vec = math.fftfreq(resolution) * resolution / math.tensor(size) * math.tensor(self.scale) # in physical units k2 = math.vec_squared(k_vec) lowest_frequency = 0.1 weight_mask = math.to_float(k2 > lowest_frequency) inv_k2 = math.divide_no_nan(1, k2) # --- Compute result --- fft = rndj * inv_k2 ** self.smoothness * weight_mask array = math.real(math.ifft(fft)) array /= math.std(array, dim=array.shape.non_batch) array -= math.mean(array, dim=array.shape.non_batch) array = math.to_float(array) return array def __getitem__(self, item: dict): new_shape = self.shape.after_gather(item) return Noise(new_shape, scale=self.scale, smoothness=self.smoothness) def __repr__(self): return f"{self._shape}, scale={self.scale}, smoothness={self.smoothness}"
Ancestors
- phi.field._field.Field
Instance variables
var shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self): return self._shape
Methods
def grid_sample(self, resolution: phi.math._shape.Shape, size, shape: phi.math._shape.Shape = None)
-
Expand source code
def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None): shape = (self._shape if shape is None else shape) & resolution for dim in channel(self._shape): if dim.item_names[0] is None: warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning) shape &= channel(**{dim.name: resolution.names}) rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape)) # Note: there is no complex32 # --- Compute 1 / k^2 --- k_vec = math.fftfreq(resolution) * resolution / math.tensor(size) * math.tensor(self.scale) # in physical units k2 = math.vec_squared(k_vec) lowest_frequency = 0.1 weight_mask = math.to_float(k2 > lowest_frequency) inv_k2 = math.divide_no_nan(1, k2) # --- Compute result --- fft = rndj * inv_k2 ** self.smoothness * weight_mask array = math.real(math.ifft(fft)) array /= math.std(array, dim=array.shape.non_batch) array -= math.mean(array, dim=array.shape.non_batch) array = math.to_float(array) return array
class PointCloud (elements: Union[phi.math._tensors.Tensor, phi.geom._geom.Geometry], values: Any = 1.0, extrapolation: Union[Extrapolation, float] = 0.0, add_overlapping=False, bounds: phi.geom._box.Box = None)
-
A
PointCloud
comprises:elements
: aGeometry
representing all points or volumesvalues
: aTensor
representing the values corresponding toelements
extrapolation
: anExtrapolation
defining the field value outside ofvalues
The points / elements of the
PointCloud
are listed along instance or spatial dimensions ofelements
. These dimensions are automatically added tovalues
if not already present.When sampling or resampling a
PointCloud
, the following keyword arguments can be specified.soft
: default=False. IfTrue
, interpolates smoothly from 1 to 0 between the inside and outside of elements. IfFalse
, only the center position of the new representation elements is checked against the point cloud elements.scatter
: default=False. IfTrue
, 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 whensoft=True
. See the description inGeometry.approximate_fraction_inside()
.
See the
phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlArgs
elements
Tensor
orGeometry
object specifying the sample points and sizesvalues
- values corresponding to elements
extrapolation
- values outside elements
add_overlapping
- True: values of overlapping geometries are summed. False: values between overlapping geometries are interpolated
bounds
- (optional) size of the fixed domain in which the points should get visualized. None results in max and min coordinates of points.
Expand source code
class PointCloud(SampledField): """ A `PointCloud` comprises: * `elements`: a `Geometry` representing all points or volumes * `values`: a `Tensor` representing the values corresponding to `elements` * `extrapolation`: an `Extrapolation` defining the field value outside of `values` The points / elements of the `PointCloud` are listed along *instance* or *spatial* dimensions of `elements`. These dimensions are automatically added to `values` if not already present. When sampling or resampling a `PointCloud`, the following keyword arguments can be specified. * `soft`: default=False. If `True`, interpolates smoothly from 1 to 0 between the inside and outside of elements. If `False`, only the center position of the new representation elements is checked against the point cloud elements. * `scatter`: default=False. If `True`, scattering will be used to sample the point cloud onto grids. Then, each element of the point cloud can only affect a single cell. This is only recommended when the points are much smaller than the cells. * `outside_handling`: default='discard'. One of `'discard'`, `'clamp'`, `'undefined'`. * `balance`: default=0.5. Only used when `soft=True`. See the description in `phi.geom.Geometry.approximate_fraction_inside()`. See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ def __init__(self, elements: Union[Tensor, Geometry], values: Any = 1., extrapolation: Union[Extrapolation, float] = 0., add_overlapping=False, bounds: Box = None): """ Args: elements: `Tensor` or `Geometry` object specifying the sample points and sizes values: values corresponding to elements extrapolation: values outside elements add_overlapping: True: values of overlapping geometries are summed. False: values between overlapping geometries are interpolated bounds: (optional) size of the fixed domain in which the points should get visualized. None results in max and min coordinates of points. """ SampledField.__init__(self, elements, expand(wrap(values), non_batch(elements).non_channel), extrapolation, bounds) assert self._extrapolation is PERIODIC or isinstance(self._extrapolation, ConstantExtrapolation), f"Unsupported extrapolation for PointCloud: {self._extrapolation}" self._add_overlapping = add_overlapping @property def shape(self): return self._elements.shape.without('vector') & self._values.shape def __getitem__(self, item): item = slicing_dict(self, item) if not item: return self elements = self.elements[{dim: selection for dim, selection in item.items() if dim != 'vector'}] values = self._values[item] extrapolation = self._extrapolation[item] return PointCloud(elements, values, extrapolation, self._add_overlapping, self._bounds) def with_elements(self, elements: Geometry): return PointCloud(elements=elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds) def shifted(self, delta): return self.with_elements(self.elements.shifted(delta)) def with_values(self, values): return PointCloud(elements=self.elements, values=values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds) def with_extrapolation(self, extrapolation: Extrapolation): return PointCloud(elements=self.elements, values=self.values, extrapolation=extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds) def with_bounds(self, bounds: Box): return PointCloud(elements=self.elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=bounds) def __value_attrs__(self): return '_values', '_extrapolation' def __variable_attrs__(self): return '_values', '_elements' def __expand__(self, dims: Shape, **kwargs) -> 'PointCloud': return self.with_values(math.expand(self.values, dims, **kwargs)) def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'PointCloud': elements = math.rename_dims(self.elements, dims, new_dims) values = math.rename_dims(self.values, dims, new_dims) extrapolation = math.rename_dims(self.extrapolation, dims, new_dims, **kwargs) return PointCloud(elements, values, extrapolation, self._add_overlapping, self._bounds) def __eq__(self, other): if not type(self) == type(other): return False # Check everything but __variable_attrs__ (values): elements type, extrapolation, add_overlapping if type(self.elements) is not type(other.elements): return False if self.extrapolation != other.extrapolation: return False if self._add_overlapping != other._add_overlapping: return False if self.values is None: return other.values is None if other.values is None: return False if not math.all_available(self.values) or not math.all_available(other.values): # tracers involved if math.all_available(self.values) != math.all_available(other.values): return False else: # both tracers return self.values.shape == other.values.shape return bool((self.values == other.values).all) @property def bounds(self) -> Box: if self._bounds is not None: return self._bounds else: from phi.field._field_math import data_bounds bounds = data_bounds(self.elements.center) radius = math.max(self.elements.bounding_radius()) return Box(bounds.lower - radius, bounds.upper + radius) def _sample(self, geometry: Geometry, soft=False, scatter=False, outside_handling='discard', balance=0.5) -> Tensor: if geometry == self.elements: return self.values if isinstance(geometry, GeometryStack): sampled = [self._sample(g, soft, scatter, outside_handling, balance) for g in geometry.geometries] return math.stack(sampled, geometry.geometries.shape) if self.extrapolation is extrapolation.PERIODIC: raise NotImplementedError("Periodic PointClouds not yet supported") if isinstance(geometry, GridCell) and scatter: assert not soft, "Cannot soft-sample when scatter=True" return self.grid_scatter(geometry.bounds, geometry.resolution, outside_handling) else: assert not isinstance(self._elements, Point), "Cannot sample Point-like elements with scatter=False" if may_vary_along(self._values, instance(self._values) & spatial(self._values)): raise NotImplementedError("Non-scatter resampling not yet supported for varying values") idx0 = (instance(self._values) & spatial(self._values)).first_index() outside = self._extrapolation.value if isinstance(self._extrapolation, ConstantExtrapolation) else 0 if soft: frac_inside = self.elements.approximate_fraction_inside(geometry, balance) return frac_inside * self._values[idx0] + (1 - frac_inside) * outside else: return math.where(self.elements.lies_inside(geometry.center), self._values[idx0], outside) def grid_scatter(self, bounds: Box, resolution: math.Shape, outside_handling: str): """ Approximately samples this field on a regular grid using math.scatter(). Args: outside_handling: `str` passed to `phi.math.scatter()`. bounds: physical dimensions of the grid resolution: grid resolution Returns: `CenteredGrid` """ closest_index = bounds.global_to_local(self.points) * resolution - 0.5 mode = 'add' if self._add_overlapping else 'mean' base = math.zeros(resolution) if isinstance(self._extrapolation, ConstantExtrapolation): base += self._extrapolation.value scattered = math.scatter(base, closest_index, self.values, mode=mode, outside_handling=outside_handling) return scattered def __repr__(self): try: return "PointCloud[%s]" % (self.shape,) except: return "PointCloud[invalid]" def __and__(self, other): assert isinstance(other, PointCloud) assert instance(self).rank == instance(other).rank == 1, f"Can only use & on PointClouds that have a single instance dimension but got shapes {self.shape} & {other.shape}" from ._field_math import concat return concat([self, other], instance(self))
Ancestors
- phi.field._field.SampledField
- phi.field._field.Field
Instance variables
var bounds : phi.geom._box.Box
-
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
.Expand source code
@property def bounds(self) -> Box: if self._bounds is not None: return self._bounds else: from phi.field._field_math import data_bounds bounds = data_bounds(self.elements.center) radius = math.max(self.elements.bounding_radius()) return Box(bounds.lower - radius, bounds.upper + radius)
var shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self): return self._elements.shape.without('vector') & self._values.shape
Methods
def grid_scatter(self, bounds: phi.geom._box.Box, resolution: phi.math._shape.Shape, outside_handling: str)
-
Approximately samples this field on a regular grid using math.scatter().
Args
outside_handling
str
passed toscatter()
.bounds
- physical dimensions of the grid
resolution
- grid resolution
Returns
Expand source code
def grid_scatter(self, bounds: Box, resolution: math.Shape, outside_handling: str): """ Approximately samples this field on a regular grid using math.scatter(). Args: outside_handling: `str` passed to `phi.math.scatter()`. bounds: physical dimensions of the grid resolution: grid resolution Returns: `CenteredGrid` """ closest_index = bounds.global_to_local(self.points) * resolution - 0.5 mode = 'add' if self._add_overlapping else 'mean' base = math.zeros(resolution) if isinstance(self._extrapolation, ConstantExtrapolation): base += self._extrapolation.value scattered = math.scatter(base, closest_index, self.values, mode=mode, outside_handling=outside_handling) return scattered
def shifted(self, delta)
-
Expand source code
def shifted(self, delta): return self.with_elements(self.elements.shifted(delta))
def with_bounds(self, bounds: phi.geom._box.Box)
-
Expand source code
def with_bounds(self, bounds: Box): return PointCloud(elements=self.elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=bounds)
def with_elements(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_elements(self, elements: Geometry): return PointCloud(elements=elements, values=self.values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
def with_extrapolation(self, extrapolation: Extrapolation)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_extrapolation(self, extrapolation: Extrapolation): return PointCloud(elements=self.elements, values=self.values, extrapolation=extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
def with_values(self, values)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_values(self, values): return PointCloud(elements=self.elements, values=values, extrapolation=self.extrapolation, add_overlapping=self._add_overlapping, bounds=self._bounds)
class SampledField (elements: Union[phi.math._tensors.Tensor, phi.geom._geom.Geometry], values: phi.math._tensors.Tensor, extrapolation: float, bounds: phi.geom._box.Box)
-
Base class for fields that are sampled at specific locations such as grids or point clouds.
Args
elements
- Geometry object specifying the sample points and sizes
values
- values corresponding to elements
extrapolation
- values outside elements
Expand source code
class SampledField(Field): """ Base class for fields that are sampled at specific locations such as grids or point clouds. """ def __init__(self, elements: Union[Geometry, Tensor], values: Tensor, extrapolation: float or Extrapolation or Field or None, bounds: Box or None): """ Args: elements: Geometry object specifying the sample points and sizes values: values corresponding to elements extrapolation: values outside elements """ if isinstance(elements, Tensor): elements = Point(elements) assert isinstance(elements, Geometry), elements assert isinstance(values, Tensor), f"Values must be a Tensor but got {values}." assert bounds is None or isinstance(bounds, Box), 'Invalid bounds.' self._bounds = bounds self._elements: Geometry = elements self._values: Tensor = values self._extrapolation: Extrapolation = as_extrapolation(extrapolation) @property def bounds(self) -> Box: raise NotImplementedError(self.__class__) def _sample(self, geometry: Geometry, **kwargs) -> math.Tensor: raise NotImplementedError(self.__class__) def with_values(self, values): """ Returns a copy of this field with `values` replaced. """ raise NotImplementedError(self) def with_extrapolation(self, extrapolation: Extrapolation): """ Returns a copy of this field with `values` replaced. """ raise NotImplementedError(self) @property def shape(self): raise NotImplementedError() @property def spatial_rank(self) -> int: return self._elements.spatial_rank def __getitem__(self: 'FieldType', item) -> 'FieldType': raise NotImplementedError(self) @staticmethod def __stack__(values: tuple, dim: Shape, **kwargs) -> 'FieldType': from ._field_math import stack return stack(values, dim, kwargs.get('bounds', None)) @staticmethod def __concat__(values: tuple, dim: str, **kwargs) -> 'FieldType': from ._field_math import concat return concat(values, dim) @property def elements(self) -> Geometry: """ Returns a geometrical representation of the discrete volume elements. The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions. For grids, the geometries are boxes while particle fields may be represented as spheres. If this Field has no discrete points, this method returns an empty geometry. """ return self._elements @property def points(self) -> Tensor: return self.elements.center @property def values(self) -> Tensor: return self._values data = values @property def extrapolation(self) -> Extrapolation: return self._extrapolation def __mul__(self, other): return self._op2(other, lambda d1, d2: d1 * d2) __rmul__ = __mul__ def __truediv__(self, other): return self._op2(other, lambda d1, d2: d1 / d2) def __rtruediv__(self, other): return self._op2(other, lambda d1, d2: d2 / d1) def __sub__(self, other): return self._op2(other, lambda d1, d2: d1 - d2) def __rsub__(self, other): return self._op2(other, lambda d1, d2: d2 - d1) def __add__(self, other): return self._op2(other, lambda d1, d2: d1 + d2) __radd__ = __add__ def __pow__(self, power, modulo=None): return self._op2(power, lambda f, p: f ** p) def __neg__(self): return self._op1(lambda x: -x) def __gt__(self, other): return self._op2(other, lambda x, y: x > y) def __ge__(self, other): return self._op2(other, lambda x, y: x >= y) def __lt__(self, other): return self._op2(other, lambda x, y: x < y) def __le__(self, other): return self._op2(other, lambda x, y: x <= y) def __abs__(self): return self._op1(lambda x: abs(x)) def _op1(self: 'SampledFieldType', operator: Callable) -> 'SampledFieldType': """ Perform an operation on the data of this field. Args: operator: function that accepts tensors and extrapolations and returns objects of the same type and dimensions Returns: Field of same type """ values = operator(self.values) extrapolation_ = operator(self._extrapolation) return self.with_values(values).with_extrapolation(extrapolation_) def _op2(self, other, operator) -> 'SampledField': if isinstance(other, Geometry): raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}") if isinstance(other, Field): other_values = reduce_sample(other, self._elements) values = operator(self._values, other_values) extrapolation_ = operator(self._extrapolation, other.extrapolation) return self.with_values(values).with_extrapolation(extrapolation_) else: if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank: other = math.tensor(other, self.points.shape['vector']) else: other = math.tensor(other) values = operator(self._values, other) return self.with_values(values)
Ancestors
- phi.field._field.Field
Subclasses
- phi.field._grid.Grid
- phi.field._point_cloud.PointCloud
Instance variables
var bounds : phi.geom._box.Box
-
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
.Expand source code
@property def bounds(self) -> Box: raise NotImplementedError(self.__class__)
var data : phi.math._tensors.Tensor
-
Expand source code
@property def values(self) -> Tensor: return self._values
var elements : phi.geom._geom.Geometry
-
Returns a geometrical representation of the discrete volume elements. The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions.
For grids, the geometries are boxes while particle fields may be represented as spheres.
If this Field has no discrete points, this method returns an empty geometry.
Expand source code
@property def elements(self) -> Geometry: """ Returns a geometrical representation of the discrete volume elements. The result is a tuple of Geometry objects, each of which can have additional spatial (but not batch) dimensions. For grids, the geometries are boxes while particle fields may be represented as spheres. If this Field has no discrete points, this method returns an empty geometry. """ return self._elements
var extrapolation : Extrapolation
-
Expand source code
@property def extrapolation(self) -> Extrapolation: return self._extrapolation
var points : phi.math._tensors.Tensor
-
Expand source code
@property def points(self) -> Tensor: return self.elements.center
var shape
-
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
Expand source code
@property def shape(self): raise NotImplementedError()
var spatial_rank : int
-
Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the
data
.Expand source code
@property def spatial_rank(self) -> int: return self._elements.spatial_rank
var values : phi.math._tensors.Tensor
-
Expand source code
@property def values(self) -> Tensor: return self._values
Methods
def with_extrapolation(self, extrapolation: Extrapolation)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_extrapolation(self, extrapolation: Extrapolation): """ Returns a copy of this field with `values` replaced. """ raise NotImplementedError(self)
def with_values(self, values)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_values(self, values): """ Returns a copy of this field with `values` replaced. """ raise NotImplementedError(self)
class Scene
-
Provides methods for reading and writing simulation data.
See the format documentation at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .
All data of a
Scene
is located inside a single directory with namesim_xxxxxx
wherexxxxxx
is theid
. 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, useScene.at()
. To list all scenes within a directory, useScene.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: str or math.Tensor): self._paths = math.wrap(paths) self._properties: dict or 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: Shape or None = None) -> 'Scene' or 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)))] 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: str or tuple or list or math.Tensor or 'Scene', id: int or math.Tensor or None = None) -> 'Scene': """ Creates a `Scene` for an existing directory. See Also: `Scene.create()`, `Scene.list()`. Args: directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`. id: (Optional) Scene `id`, will be determined from `directory` if not specified. Returns: `Scene` object for existing scene. """ if isinstance(directory, Scene): assert id is None, f"Got id={id} but directory is already a Scene." return directory if isinstance(directory, (tuple, list)): directory = math.wrap(directory, batch('scenes')) directory = math.map(lambda d: expanduser(d), math.wrap(directory)) if id is None: paths = directory else: id = math.wrap(id) paths = math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id) # test all exist for path in math.flatten(paths, flatten_batch=True): if not isdir(path): raise IOError(f"There is no scene at '{path}'") return Scene(paths) def subpath(self, name: str, create=False, create_parent=False) -> str or tuple: """ Resolves the relative path `name` with this `Scene` as the root folder. Args: name: Relative path with this `Scene` as the root folder. create: Whether to create a directory of that name. create_parent: Whether to create the parent directory. Returns: Relative path including the path to this `Scene`. In batch mode, returns a `tuple`, else a `str`. """ def single_subpath(path): path = join(path, name) if create_parent and not isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) if create and not isdir(path): os.mkdir(path) return path result = math.map(single_subpath, self._paths) if result.rank == 0: return result.native() else: return result def _init_properties(self): if self._properties is not None: return def read_json(path: str) -> dict: json_file = join(path, "description.json") if isfile(json_file): with open(json_file) as stream: props = json.load(stream) if '__tensors__' in props: for key in props['__tensors__']: props[key] = math.from_dict(props[key]) return props else: return {} if self._paths.shape.volume == 1: self._properties = read_json(self._paths.native()) else: self._properties = {} dicts = [read_json(p) for p in self._paths] keys = set(sum([tuple(d.keys()) for d in dicts], ())) for key in keys: assert all(key in d for d in dicts), f"Failed to create batched Scene because property '{key}' is present in some scenes but not all." if all([math.all(d[key] == dicts[0][key]) for d in dicts]): self._properties[key] = dicts[0][key] else: self._properties[key] = stack([d[key] for d in dicts], self._paths.shape) if '__tensors__' in self._properties: del self._properties['__tensors__'] def exist_properties(self): """ Checks whether the file `description.json` exists or has existed. """ if self._properties is not None: return True # must have been written or read else: json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json") return isfile(json_file) def exists_config(self): """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """ if isinstance(self.path, str): return isfile(join(self.path, "description.json")) else: return any(isfile(join(p, "description.json")) for p in self.path) @property def properties(self): self._init_properties() return self._properties @properties.setter def properties(self, dict): self._properties = dict with open(join(self.path, "description.json"), "w") as out: json.dump(self._properties, out, indent=2) def put_property(self, key, value): """ See `Scene.put_properties()`. """ self._init_properties() self._properties[key] = value self._write_properties() def put_properties(self, update: dict = None, **kw_updates): """ Updates the properties dictionary and stores it in `description.json` of all scene folders. Args: update: new values, must be JSON serializable. kw_updates: additional update as keyword arguments. This overrides `update`. """ self._init_properties() if update: self._properties.update(update) self._properties.update(kw_updates) self._write_properties() def _get_properties(self, index: dict): result = dict(self._properties) tensor_names = [] for key, value in self._properties.items(): if isinstance(value, math.Tensor): value = value[index] if value.rank == 0: value = value.dtype.kind(value) else: value = math.to_dict(value) tensor_names.append(key) result[key] = value if tensor_names: result['__tensors__'] = tuple(tensor_names) return result def _write_properties(self): for instance in self.paths.shape.meshgrid(): path = self.paths[instance].native() instance_properties = self._get_properties(instance) with open(join(path, "description.json"), "w") as out: json.dump(instance_properties, out, indent=2) def write(self, data: dict = None, frame=0, **kw_data): """ Writes fields to this scene. One NumPy file will be created for each `phi.field.Field` See Also: `Scene.read()`. Args: data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`. kw_data: Additional data, overrides elements in `data`. frame: Frame number. """ data = dict(data) if data else {} data.update(kw_data) for name, field in data.items(): self.write_field(field, name, frame) def write_field(self, field: SampledField, name: str, frame: int): """ Write a `SampledField` to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html . Args: field: single field or structure of Fields to save. name: Base file name. frame: Frame number as `int`, typically time step index. """ if not isinstance(field, SampledField): raise ValueError(f"Only SampledField instances can be saved but got {field}") name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) write(field, files) def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField: """ Reads a single `SampledField` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `SampledField` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend) read_array = read_field def read(self, *names: str, frame=0, convert_to_backend=True): """ Reads one or multiple fields from disc. See Also: `Scene.write()`. Args: names: Single field name or sequence of field names. frame: Frame number. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Single `phi.field.Field` or sequence of fields, depending on the type of `names`. """ if len(names) == 1 and isinstance(names[0], (tuple, list)): names = names[0] result = [self.read_array(name, frame, convert_to_backend) for name in names] return result[0] if len(names) == 1 else result @property def fieldnames(self) -> tuple: """ Determines all field names present in this `Scene`, independent of frame. """ return get_fieldnames(self.path) @property def frames(self): """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """ return get_frames(self.path, mode=set.union) @property def complete_frames(self): """ Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields. See Also: `Scene.frames` """ return get_frames(self.path, mode=set.intersection) def __repr__(self): return f"{self.paths:no-dtype}" def __eq__(self, other): return isinstance(other, Scene) and (other._paths == self._paths).all def copy_calling_script(self, full_trace=False, include_context_information=True): """ Copies the Python file that called this method into the `src` folder of this `Scene`. In batch mode, the script is copied to all scenes. Args: full_trace: Whether to include scripts that indirectly called this method. include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`. """ script_paths = [frame.filename for frame in inspect.stack()] script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths)) script_paths = set(script_paths) if full_trace else [script_paths[0]] self.subpath('src', create=True) for script_path in script_paths: if script_path.endswith('.py'): self.copy_src(script_path, only_external=False) elif 'ipython' in script_path: from IPython import get_ipython cells = get_ipython().user_ns['In'] blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)] text = "\n\n".join(blocks) self.copy_src_text('ipython.py', text) if include_context_information: for path in math.flatten(self._paths, flatten_batch=True): with open(join(path, 'src', 'context.json'), 'w') as context_file: json.dump({ 'phi_version': phi_version, 'argv': sys.argv }, context_file) def copy_src(self, script_path, only_external=True): for path in math.flatten(self._paths, flatten_batch=True): if not only_external or not _is_phi_file(script_path): shutil.copy(script_path, join(path, 'src', basename(script_path))) def copy_src_text(self, filename, text): for path in math.flatten(self._paths, flatten_batch=True): target = join(path, 'src', filename) with open(target, "w") as file: file.writelines(text) def mkdir(self): for path in math.flatten(self._paths, flatten_batch=True): isdir(path) or os.mkdir(path) def remove(self): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): shutil.rmtree(p)
Static methods
def at(directory: str, id: int = 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 ifid=None
. id
- (Optional) Scene
id
, will be determined fromdirectory
if not specified.
Returns
Scene
object for existing scene.Expand source code
@staticmethod def at(directory: str or tuple or list or math.Tensor or 'Scene', id: int or math.Tensor or None = None) -> 'Scene': """ Creates a `Scene` for an existing directory. See Also: `Scene.create()`, `Scene.list()`. Args: directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`. id: (Optional) Scene `id`, will be determined from `directory` if not specified. Returns: `Scene` object for existing scene. """ if isinstance(directory, Scene): assert id is None, f"Got id={id} but directory is already a Scene." return directory if isinstance(directory, (tuple, list)): directory = math.wrap(directory, batch('scenes')) directory = math.map(lambda d: expanduser(d), math.wrap(directory)) if id is None: paths = directory else: id = math.wrap(id) paths = math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id) # test all exist for path in math.flatten(paths, flatten_batch=True): if not isdir(path): raise IOError(f"There is no scene at '{path}'") return Scene(paths)
def create(parent_directory: str, shape: phi.math._shape.Shape = (), name='sim', copy_calling_script=True, **dimensions) ‑> phi.field._scene.Scene
-
Creates a new
Scene
or a batch of new scenes insideparent_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
withis_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. SeeScene.copy_calling_script()
. dimensions
- Additional batch dimensions
Returns
Single
Scene
object representing the new scene(s).Expand source code
@staticmethod def create(parent_directory: str, shape: math.Shape = math.EMPTY_SHAPE, name='sim', copy_calling_script=True, **dimensions) -> 'Scene': """ Creates a new `Scene` or a batch of new scenes inside `parent_directory`. See Also: `Scene.at()`, `Scene.list()`. Args: parent_directory: Directory to hold the new `Scene`. If it doesn't exist, it will be created. shape: Determines number of scenes to create. Multiple scenes will be represented by a `Scene` with `is_batch=True`. name: Name of the directory (excluding index). Default is `'sim'`. copy_calling_script: Whether to copy the Python file that invoked this method into the `src` folder of all created scenes. See `Scene.copy_calling_script()`. dimensions: Additional batch dimensions Returns: Single `Scene` object representing the new scene(s). """ shape = shape & math.batch(**dimensions) parent_directory = expanduser(parent_directory) abs_dir = abspath(parent_directory) if not isdir(abs_dir): os.makedirs(abs_dir) next_id = 0 else: indices = [int(f[len(name)+1:]) for f in os.listdir(abs_dir) if f.startswith(f"{name}_")] next_id = max([-1] + indices) + 1 ids = unpack_dim(wrap(tuple(range(next_id, next_id + shape.volume))), 'vector', shape) paths = math.map(lambda id_: join(parent_directory, f"{name}_{id_:06d}"), ids) scene = Scene(paths) scene.mkdir() if copy_calling_script: try: scene.copy_calling_script() except IOError as err: warnings.warn(f"Failed to copy calling script to scene during Scene.create(): {err}", RuntimeWarning) return scene
def list(parent_directory: str, name='sim', include_other: bool = False, dim: phi.math._shape.Shape = None) ‑> phi.field._scene.Scene
-
Lists all scenes inside the given directory.
See Also:
Scene.at()
,Scene.create()
.Args
parent_directory
- Directory that contains scene folders.
name
- Name of the directory (excluding index). Default is
'sim'
. include_other
- Whether folders that do not match the scene format should also be treated as scenes.
dim
- Stack dimension. If None, returns tuple of
Scene
objects. Otherwise, returns a scene batch with this dimension.
Returns
tuple
of scenes.Expand source code
@staticmethod def list(parent_directory: str, name='sim', include_other: bool = False, dim: Shape or None = None) -> 'Scene' or 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)))] if dim is None: return tuple(Scene(join(parent_directory, n)) for n in names) else: paths = math.wrap([join(parent_directory, n) for n in names], dim) return Scene(paths)
def stack(*scenes: Scene, dim: phi.math._shape.Shape = (batchᵇ=None)) ‑> phi.field._scene.Scene
-
Expand source code
@staticmethod def stack(*scenes: 'Scene', dim: Shape = batch('batch')) -> 'Scene': return Scene(math.stack([s._paths for s in scenes], dim))
Instance variables
var complete_frames
-
Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields.
See Also:
Scene.frames
Expand source code
@property def complete_frames(self): """ Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields. See Also: `Scene.frames` """ return get_frames(self.path, mode=set.intersection)
var fieldnames : tuple
-
Determines all field names present in this
Scene
, independent of frame.Expand source code
@property def fieldnames(self) -> tuple: """ Determines all field names present in this `Scene`, independent of frame. """ return get_fieldnames(self.path)
var frames
-
Determines all frame numbers present in this
Scene
, independent of field names. SeeScene.complete_frames
.Expand source code
@property def frames(self): """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """ return get_frames(self.path, mode=set.union)
var is_batch
-
Expand source code
@property def is_batch(self): return self._paths.rank > 0
var path : str
-
Relative path of the scene directory. This property only exists for single scenes, not scene batches.
Expand source code
@property def path(self) -> str: """ Relative path of the scene directory. This property only exists for single scenes, not scene batches. """ assert not self.is_batch, "Scene.path is not defined for scene batches." return self._paths.native()
var paths : phi.math._tensors.Tensor
-
Expand source code
@property def paths(self) -> math.Tensor: return self._paths
var properties
-
Expand source code
@property def properties(self): self._init_properties() return self._properties
var shape
-
Expand source code
@property def shape(self): return self._paths.shape
Methods
def copy_calling_script(self, full_trace=False, include_context_information=True)
-
Copies the Python file that called this method into the
src
folder of thisScene
.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
intocontext.json
.
Expand source code
def copy_calling_script(self, full_trace=False, include_context_information=True): """ Copies the Python file that called this method into the `src` folder of this `Scene`. In batch mode, the script is copied to all scenes. Args: full_trace: Whether to include scripts that indirectly called this method. include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`. """ script_paths = [frame.filename for frame in inspect.stack()] script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths)) script_paths = set(script_paths) if full_trace else [script_paths[0]] self.subpath('src', create=True) for script_path in script_paths: if script_path.endswith('.py'): self.copy_src(script_path, only_external=False) elif 'ipython' in script_path: from IPython import get_ipython cells = get_ipython().user_ns['In'] blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)] text = "\n\n".join(blocks) self.copy_src_text('ipython.py', text) if include_context_information: for path in math.flatten(self._paths, flatten_batch=True): with open(join(path, 'src', 'context.json'), 'w') as context_file: json.dump({ 'phi_version': phi_version, 'argv': sys.argv }, context_file)
def copy_src(self, script_path, only_external=True)
-
Expand source code
def copy_src(self, script_path, only_external=True): for path in math.flatten(self._paths, flatten_batch=True): if not only_external or not _is_phi_file(script_path): shutil.copy(script_path, join(path, 'src', basename(script_path)))
def copy_src_text(self, filename, text)
-
Expand source code
def copy_src_text(self, filename, text): for path in math.flatten(self._paths, flatten_batch=True): target = join(path, 'src', filename) with open(target, "w") as file: file.writelines(text)
def exist_properties(self)
-
Checks whether the file
description.json
exists or has existed.Expand source code
def exist_properties(self): """ Checks whether the file `description.json` exists or has existed. """ if self._properties is not None: return True # must have been written or read else: json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json") return isfile(json_file)
def exists_config(self)
-
Tests if the configuration file description.json exists. In batch mode, tests if any configuration exists.
Expand source code
def exists_config(self): """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """ if isinstance(self.path, str): return isfile(join(self.path, "description.json")) else: return any(isfile(join(p, "description.json")) for p in self.path)
def mkdir(self)
-
Expand source code
def mkdir(self): for path in math.flatten(self._paths, flatten_batch=True): isdir(path) or os.mkdir(path)
def put_properties(self, update: dict = None, **kw_updates)
-
Updates the properties dictionary and stores it in
description.json
of all scene folders.Args
update
- new values, must be JSON serializable.
kw_updates
- additional update as keyword arguments. This overrides
update
.
Expand source code
def put_properties(self, update: dict = None, **kw_updates): """ Updates the properties dictionary and stores it in `description.json` of all scene folders. Args: update: new values, must be JSON serializable. kw_updates: additional update as keyword arguments. This overrides `update`. """ self._init_properties() if update: self._properties.update(update) self._properties.update(kw_updates) self._write_properties()
def put_property(self, key, value)
-
Expand source code
def put_property(self, key, value): """ See `Scene.put_properties()`. """ self._init_properties() self._properties[key] = value self._write_properties()
def read(self, *names: str, frame=0, convert_to_backend=True)
-
Reads one or multiple fields from disc.
See Also:
Scene.write()
.Args
names
- Single field name or sequence of field names.
frame
- Frame number.
convert_to_backend
- Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.
Returns
Single
Field
or sequence of fields, depending on the type ofnames
.Expand source code
def read(self, *names: str, frame=0, convert_to_backend=True): """ Reads one or multiple fields from disc. See Also: `Scene.write()`. Args: names: Single field name or sequence of field names. frame: Frame number. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Single `phi.field.Field` or sequence of fields, depending on the type of `names`. """ if len(names) == 1 and isinstance(names[0], (tuple, list)): names = names[0] result = [self.read_array(name, frame, convert_to_backend) for name in names] return result[0] if len(names) == 1 else result
def read_array(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.SampledField
-
Reads a single
SampledField
from files contained in thisScene
(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
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField: """ Reads a single `SampledField` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `SampledField` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend)
def read_field(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.SampledField
-
Reads a single
SampledField
from files contained in thisScene
(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
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> SampledField: """ Reads a single `SampledField` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `SampledField` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend)
def remove(self)
-
Deletes the scene directory and all contained files.
Expand source code
def remove(self): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): shutil.rmtree(p)
def subpath(self, name: str, create=False, create_parent=False) ‑> str
-
Resolves the relative path
name
with thisScene
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 atuple
, else astr
.Expand source code
def subpath(self, name: str, create=False, create_parent=False) -> str or tuple: """ Resolves the relative path `name` with this `Scene` as the root folder. Args: name: Relative path with this `Scene` as the root folder. create: Whether to create a directory of that name. create_parent: Whether to create the parent directory. Returns: Relative path including the path to this `Scene`. In batch mode, returns a `tuple`, else a `str`. """ def single_subpath(path): path = join(path, name) if create_parent and not isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) if create and not isdir(path): os.mkdir(path) return path result = math.map(single_subpath, self._paths) if result.rank == 0: return result.native() else: return result
def write(self, data: dict = None, frame=0, **kw_data)
-
Writes fields to this scene. One NumPy file will be created for each
Field
See Also:
Scene.read()
.Args
Expand source code
def write(self, data: dict = None, frame=0, **kw_data): """ Writes fields to this scene. One NumPy file will be created for each `phi.field.Field` See Also: `Scene.read()`. Args: data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`. kw_data: Additional data, overrides elements in `data`. frame: Frame number. """ data = dict(data) if data else {} data.update(kw_data) for name, field in data.items(): self.write_field(field, name, frame)
def write_field(self, field: phi.field._field.SampledField, name: str, frame: int)
-
Write a
SampledField
to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .Args
field
- single field or structure of Fields to save.
name
- Base file name.
frame
- Frame number as
int
, typically time step index.
Expand source code
def write_field(self, field: SampledField, name: str, frame: int): """ Write a `SampledField` to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html . Args: field: single field or structure of Fields to save. name: Base file name. frame: Frame number as `int`, typically time step index. """ if not isinstance(field, SampledField): raise ValueError(f"Only SampledField instances can be saved but got {field}") name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) write(field, files)
class GeometryMask (geometry: phi.geom._geom.Geometry, balance: phi.math._tensors.Tensor = 0.5)
-
Deprecated since version 1.3. Use
mask()
orresample()
instead.Expand source code
class SoftGeometryMask(HardGeometryMask): """ Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry, balance: Tensor or 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: phi.math._tensors.Tensor = 0.5)
-
Deprecated since version 1.3. Use
mask()
orresample()
instead.Expand source code
class SoftGeometryMask(HardGeometryMask): """ Deprecated since version 1.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry, balance: Tensor or float = 0.5): warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2) super().__init__(geometry) self.balance = balance def _sample(self, geometry: Geometry, **kwargs) -> Tensor: return self.geometry.approximate_fraction_inside(geometry, self.balance) def __getitem__(self, item: dict): return SoftGeometryMask(self.geometry[item], self.balance)
Ancestors
- phi.field._mask.HardGeometryMask
- phi.field._field.Field
class StaggeredGrid (values: Any = 0.0, extrapolation: float = 0, bounds: phi.geom._box.Box = None, resolution: phi.math._shape.Shape = None, **resolution_: int)
-
N-dimensional grid whose vector components are sampled at the respective face centers. A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation.
Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components.
See Also:
CenteredGrid
,Grid
,SampledField
,Field
, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlArgs
values
-
Values to use for the grid. Has to be one of the following:
Geometry
: sets inside values to 1, outside to 0Field
: resamples the Field to the staggered sample pointsNumber
: uses the value for all sample pointstuple
orlist
: interprets the sequence as vector, used for all sample pointsTensor
with staggered shape: uses tensor values as grid values. Must contain avector
dimension with each slice consisting of one more element along the dimension they describe. Usestack()
to manually create this non-uniform tensor.- Function
values(x)
wherex
is aTensor
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
,Tensor
,Extrapolation
. bounds
- Physical size and location of the grid as
Box
. If the resolution is determined throughresolution
ofvalues
, afloat
can be passed forbounds
to create a unit box. resolution
- Grid resolution as purely spatial
Shape
. Ifbounds
is given as aBox
, the resolution may be specified as anint
to be equal along all axes. **resolution_
- Spatial dimensions as keyword arguments. Typically either
resolution
orspatial_dims
are specified.
Expand source code
class StaggeredGrid(Grid): """ N-dimensional grid whose vector components are sampled at the respective face centers. A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation. Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components. See Also: `CenteredGrid`, `Grid`, `SampledField`, `Field`, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ def __init__(self, values: Any = 0., extrapolation: float or Extrapolation = 0, bounds: Box or float = None, resolution: Shape or int = None, **resolution_: int or Tensor): """ Args: values: Values to use for the grid. Has to be one of the following: * `phi.geom.Geometry`: sets inside values to 1, outside to 0 * `Field`: resamples the Field to the staggered sample points * `Number`: uses the value for all sample points * `tuple` or `list`: interprets the sequence as vector, used for all sample points * `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. extrapolation: The grid extrapolation determines the value outside the `values` tensor. Allowed types: `float`, `phi.math.Tensor`, `phi.math.extrapolation.Extrapolation`. bounds: Physical size and location of the grid as `phi.geom.Box`. If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box. resolution: Grid resolution as purely spatial `phi.math.Shape`. If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes. **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified. """ extrapolation = as_extrapolation(extrapolation) if resolution is None and not resolution_: assert isinstance(values, Tensor), "Grid resolution must be specified when 'values' is not a Tensor." if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in spatial(values).names): # non-uniform values required if values.shape.is_uniform: values = unstack_staggered_tensor(values, extrapolation) resolution = resolution_from_staggered_tensor(values, extrapolation) else: resolution = spatial(values) bounds = _get_bounds(bounds, resolution) bounds = bounds or Box(math.const_vec(0, resolution), math.wrap(resolution, channel('vector'))) elements = staggered_elements(resolution, bounds, extrapolation) else: resolution = _get_resolution(resolution, resolution_, bounds) bounds = _get_bounds(bounds, resolution) elements = staggered_elements(resolution, bounds, extrapolation) if isinstance(values, math.Tensor): if not spatial(values): values = expand_staggered(values, resolution, extrapolation) if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in resolution.names): # non-uniform values required if values.shape.is_uniform: values = unstack_staggered_tensor(values, extrapolation) else: # Keep dim order from data and check it matches resolution assert set(resolution_from_staggered_tensor(values, extrapolation)) == set(resolution), f"Failed to create StaggeredGrid: values {values.shape} do not match given resolution {resolution} for extrapolation {extrapolation}. See https://tum-pbs.github.io/PhiFlow/Staggered_Grids.html" elif isinstance(values, Geometry): values = reduce_sample(values, elements) elif isinstance(values, Field): values = reduce_sample(values, elements) elif callable(values): values = _sample_function(values, elements) if elements.shape.shape.rank > 1: # Different number of X and Y faces assert isinstance(values, TensorStack), f"values function must return a staggered Tensor but returned {type(values)}" assert 'staggered_direction' in values.shape if 'vector' in values.shape: values = math.stack([values.staggered_direction[i].vector[i] for i in range(resolution.rank)], channel(vector=resolution)) else: values = values.staggered_direction.as_channel('vector') else: values = expand_staggered(math.tensor(values), resolution, extrapolation) if values.dtype.kind not in (float, complex): values = math.to_float(values) assert resolution.spatial_rank == bounds.spatial_rank, f"Resolution {resolution} does not match bounds {bounds}" Grid.__init__(self, elements, values, extrapolation, resolution, bounds) @property def cells(self): return GridCell(self.resolution, self.bounds) def with_extrapolation(self, extrapolation: Extrapolation): extrapolation = as_extrapolation(extrapolation) if all([extrapolation.valid_outer_faces(dim) == self.extrapolation.valid_outer_faces(dim) for dim in self.resolution.names]): return StaggeredGrid(self.values, extrapolation=extrapolation, bounds=self.bounds) else: values = [] for dim, component in zip(self.shape.spatial.names, self.values.unstack('vector')): old_lo, old_hi = [int(v) for v in self.extrapolation.valid_outer_faces(dim)] new_lo, new_hi = [int(v) for v in extrapolation.valid_outer_faces(dim)] widths = (new_lo - old_lo, new_hi - old_hi) values.append(math.pad(component, {dim: widths}, self.extrapolation, bounds=self.bounds)) values = math.stack(values, channel(vector=self.resolution)) return StaggeredGrid(values, extrapolation, bounds=self.bounds) def _sample(self, geometry: Geometry, **kwargs) -> Tensor: channels = [sample(component, geometry, **kwargs) for component in self.vector.unstack()] return math.stack(channels, geometry.shape['vector']) def closest_values(self, points: Geometry): if 'staggered_direction' in points.shape: points_ = points.unstack('staggered_direction') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['vector']) def at_centers(self) -> CenteredGrid: """ Interpolates the staggered values to the cell centers. Returns: `CenteredGrid` sampled at cell centers. """ return CenteredGrid(self, resolution=self.resolution, bounds=self.bounds, extrapolation=self.extrapolation) def __getitem__(self, item): item = slicing_dict(self, item) if not item: return self if 'vector' in item: selection = item['vector'] if isinstance(selection, int): item['vector'] = self.resolution.names[selection] values = self._values[{dim: sel for dim, sel in item.items() if dim not in self.shape.spatial}] for dim, sel in item.items(): if dim in self.shape.spatial: raise AssertionError("Cannot slice StaggeredGrid along spatial dimensions.") # sel = slice(sel, sel + 1) if isinstance(sel, int) else sel # values = [] # for vdim, val in zip(self.shape.spatial.names, self.values.unstack('vector')): # if vdim == dim: # values.append(val[{dim: slice(sel.start, sel.stop + 1)}]) # else: # values.append(val[{dim: sel}]) # values = math.stack(values, channel('vector')) extrapolation = self._extrapolation[item] bounds = GridCell(self._resolution, self._bounds)[item].bounds if 'vector' in item: selection = item['vector'] if isinstance(selection, str) and ',' in selection: selection = parse_dim_order(selection) if isinstance(selection, str): # single item name item_names = self.shape.get_item_names('vector', fallback_spatial=True) assert selection in item_names, f"Accessing field.vector['{selection}'] failed. Item names are {item_names}." selection = item_names.index(selection) if isinstance(selection, int): dim = self.resolution.names[selection] comp_cells = GridCell(self.resolution, bounds).stagger(dim, *self.extrapolation.valid_outer_faces(dim)) return CenteredGrid(values, bounds=comp_cells.bounds, extrapolation=extrapolation) else: assert isinstance(selection, slice) and not selection.start and not selection.stop return StaggeredGrid(values, bounds=bounds, extrapolation=extrapolation) def uniform_values(self): if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor() def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `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`. """ padded = [] for dim, component in zip(self.resolution.names, math.unstack(self.values, 'vector')): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result def _op2(self, other, operator): if isinstance(other, StaggeredGrid) and self.bounds == other.bounds and self.shape.spatial == other.shape.spatial: values = operator(self._values, other.values) extrapolation_ = operator(self._extrapolation, other.extrapolation) return StaggeredGrid(values=values, extrapolation=extrapolation_, bounds=self.bounds) else: return SampledField._op2(self, other, operator)
Ancestors
- phi.field._grid.Grid
- phi.field._field.SampledField
- phi.field._field.Field
Instance variables
var cells
-
Expand source code
@property def cells(self): return GridCell(self.resolution, self.bounds)
Methods
def at_centers(self) ‑> phi.field._grid.CenteredGrid
-
Interpolates the staggered values to the cell centers.
Returns
CenteredGrid
sampled at cell centers.Expand source code
def at_centers(self) -> CenteredGrid: """ Interpolates the staggered values to the cell centers. Returns: `CenteredGrid` sampled at cell centers. """ return CenteredGrid(self, resolution=self.resolution, bounds=self.bounds, extrapolation=self.extrapolation)
def closest_values(self, points: phi.geom._geom.Geometry)
-
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension namedvector
. 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 inpoints
, a 2^d cube of points is determined where d is the number of spatial dimensions of this field. These values are stacked along the new dimensions'closest_<dim>'
where<dim>
refers to the name of a spatial dimension.Expand source code
def closest_values(self, points: Geometry): if 'staggered_direction' in points.shape: points_ = points.unstack('staggered_direction') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['vector'])
def staggered_tensor(self) ‑> phi.math._tensors.Tensor
-
Stacks all component grids into a single uniform
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 gridresolution
in every spatial dimension.Returns
Uniform
Tensor
.Expand source code
def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ padded = [] for dim, component in zip(self.resolution.names, math.unstack(self.values, 'vector')): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result
def uniform_values(self)
-
Returns a uniform tensor containing
values
.For periodic grids, which always have a uniform value tensor, `values' is returned directly. If
values
is not uniform, it is padded as inStaggeredGrid.staggered_tensor()
.Expand source code
def uniform_values(self): if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor()
def with_extrapolation(self, extrapolation: Extrapolation)
-
Returns a copy of this field with
values
replaced.Expand source code
def with_extrapolation(self, extrapolation: Extrapolation): extrapolation = as_extrapolation(extrapolation) if all([extrapolation.valid_outer_faces(dim) == self.extrapolation.valid_outer_faces(dim) for dim in self.resolution.names]): return StaggeredGrid(self.values, extrapolation=extrapolation, bounds=self.bounds) else: values = [] for dim, component in zip(self.shape.spatial.names, self.values.unstack('vector')): old_lo, old_hi = [int(v) for v in self.extrapolation.valid_outer_faces(dim)] new_lo, new_hi = [int(v) for v in extrapolation.valid_outer_faces(dim)] widths = (new_lo - old_lo, new_hi - old_hi) values.append(math.pad(component, {dim: widths}, self.extrapolation, bounds=self.bounds)) values = math.stack(values, channel(vector=self.resolution)) return StaggeredGrid(values, extrapolation, bounds=self.bounds)