Module phi.field
The fields module provides a number of data structures and functions to represent continuous, spatially varying data.
All fields are subclasses of Field
which provides abstract functions for sampling field values at physical locations.
The most important field types are:
CenteredGrid()
embeds a tensor in the physical space. Uses linear interpolation between grid points.StaggeredGrid()
samples the vector components at face centers instead of at cell centers.Noise
is a function that produces a procedurally generated noise field
Use grid()
to create a Field
from data or by sampling another Field
or Geometry
.
Alternatively, the phi.physics.Domain
class provides convenience methods for grid creation.
All fields can be sampled at physical locations or volumes using sample()
or reduce_sample()
.
See the phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html
Functions
def CenteredGrid(values: Any = 0.0,
boundary: Any = 0.0,
bounds: phi.geom._box.Box = None,
resolution: int = None,
extrapolation: Any = None,
convert=True,
**resolution_: int) ‑> phi.field._field.Field-
Expand source code
def CenteredGrid(values: Any = 0., boundary: Any = 0., bounds: Box or float = None, resolution: int or Shape = None, extrapolation: Any = None, convert=True, **resolution_: int or Tensor) -> Field: """ Create an n-dimensional grid with values sampled at the cell centers. A centered grid is defined through its `CenteredGrid.values` `phi.math.Tensor`, its `CenteredGrid.bounds` `phi.geom.Box` describing the physical size, and its `CenteredGrid.extrapolation` (`phi.math.extrapolation.Extrapolation`). Centered grids support batch, spatial and channel dimensions. See Also: `StaggeredGrid`, `Grid`, `Field`, `Field`, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html Args: values: Values to use for the grid. Has to be one of the following: * `phi.geom.Geometry`: sets inside values to 1, outside to 0 * `Field`: resamples the Field to the staggered sample points * `Number`: uses the value for all sample points * `tuple` or `list`: interprets the sequence as vector, used for all sample points * `phi.math.Tensor` compatible with grid dims: uses tensor values as grid values * Function `values(x)` where `x` is a `phi.math.Tensor` representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function. extrapolation: The grid extrapolation determines the value outside the `values` tensor. Allowed types: `float`, `phi.math.Tensor`, `phi.math.extrapolation.Extrapolation`. bounds: Physical size and location of the grid as `phi.geom.Box`. If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box. resolution: Grid resolution as purely spatial `phi.math.Shape`. If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes. **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified. convert: Whether to convert `values` to the default backend. """ extrapolation = boundary if extrapolation is None else extrapolation if resolution is None and not resolution_: assert isinstance(values, math.Tensor), "Grid resolution must be specified when 'values' is not a Tensor." resolution = values.shape.spatial elements = UniformGrid(resolution, bounds) else: resolution = _get_resolution(resolution, resolution_, bounds) elements = UniformGrid(resolution, bounds) if isinstance(values, math.Tensor): values = math.expand(values, resolution) elif isinstance(values, (Field, FieldInitializer, Geometry)): values = sample(values, elements) elif callable(values): values = sample_function(values, elements, 'center', extrapolation) else: if isinstance(values, (tuple, list)) and len(values) == resolution.rank: values = math.tensor(values, channel(vector=resolution.names)) values = math.expand(math.tensor(values, convert=convert), resolution) if values.dtype.kind not in (float, complex): values = math.to_float(values) assert resolution.spatial_rank == elements.bounds.spatial_rank, f"Resolution {resolution} does not match bounds {bounds}" assert values.shape.spatial_rank == elements.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.spatial_rank == elements.bounds.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.instance_rank == 0, f"Instance dimensions not supported for grids. Got values with shape {values.shape}" return Field(elements, values, extrapolation)
Create an n-dimensional grid with values sampled at the cell centers. A centered grid is defined through its
CenteredGrid.values
phi.math.Tensor
, itsCenteredGrid.bounds
Box
describing the physical size, and itsCenteredGrid.extrapolation
(phi.math.extrapolation.Extrapolation
).Centered grids support batch, spatial and channel dimensions.
See Also:
StaggeredGrid()
,Field
,Field
,Field
, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.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 pointsphi.math.Tensor
compatible with grid dims: uses tensor values as grid values- Function
values(x)
wherex
is aphi.math.Tensor
representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function.
extrapolation
- The grid extrapolation determines the value outside the
values
tensor. Allowed types:float
,phi.math.Tensor
,phi.math.extrapolation.Extrapolation
. bounds
- Physical size and location of the grid as
Box
. If the resolution is determined throughresolution
ofvalues
, afloat
can be passed forbounds
to create a unit box. resolution
- Grid resolution as purely spatial
phi.math.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. convert
- Whether to convert
values
to the default backend.
def PointCloud(elements: phiml.math._tensors.Tensor | phi.geom._geom.Geometry | float,
values: Any = 1.0,
extrapolation: phiml.math.extrapolation.Extrapolation | float = 0.0,
bounds: phi.geom._box.Box = None,
variable_attrs=('values', 'geometry'),
value_attrs=('values',)) ‑> phi.field._field.Field-
Expand source code
def PointCloud(elements: Union[Tensor, Geometry, float], values: Any = 1., extrapolation: Union[Extrapolation, float] = 0., bounds: Box = None, variable_attrs=('values', 'geometry'), value_attrs=('values',)) -> Field: """ A `PointCloud` comprises: * `elements`: a `Geometry` representing all points or volumes * `values`: a `Tensor` representing the values corresponding to `elements` * `extrapolation`: an `Extrapolation` defining the field value outside of `values` The points / elements of the `PointCloud` are listed along *instance* or *spatial* dimensions of `elements`. These dimensions are automatically added to `values` if not already present. When sampling or resampling a `PointCloud`, the following keyword arguments can be specified. * `soft`: default=False. If `True`, interpolates smoothly from 1 to 0 between the inside and outside of elements. If `False`, only the center position of the new representation elements is checked against the point cloud elements. * `scatter`: default=False. If `True`, scattering will be used to sample the point cloud onto grids. Then, each element of the point cloud can only affect a single cell. This is only recommended when the points are much smaller than the cells. * `outside_handling`: default='discard'. One of `'discard'`, `'clamp'`, `'undefined'`. * `balance`: default=0.5. Only used when `soft=True`. See the description in `phi.geom.Geometry.approximate_fraction_inside()`. See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html Args: elements: `Tensor` or `Geometry` object specifying the sample points and sizes values: values corresponding to elements extrapolation: values outside elements bounds: Deprecated. Has no use since 2.5. """ if bounds is not None: warnings.warn("bounds argument is deprecated since 2.5 and will be ignored.", stacklevel=2) # if dual(values): # assert dual(values).rank == 1, f"PointCloud cannot convert values with more than 1 dual dimension." # non_dual_name = dual(values).name[1:] # indices = math.stored_indices(values)[non_dual_name] # values = math.stored_values(values) # elements = elements[{non_dual_name: indices}] if isinstance(elements, (int, float)) and elements == 0: assert 'vector' in shape(values), f"When constructing a PointCloud from the origin 0, values must have a 'vector' dimension" elements = values * 0 if isinstance(elements, Tensor): elements = geom.Point(elements) result = Field(elements, values, extrapolation, variable_attrs, value_attrs) assert result.boundary is PERIODIC or isinstance(result.boundary, ConstantExtrapolation), f"Unsupported extrapolation for PointCloud: {result._boundary}" return result
A
PointCloud()
comprises:elements
: 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
bounds
- Deprecated. Has no use since 2.5.
def StaggeredGrid(values: Any = 0.0,
boundary: float = 0,
bounds: phi.geom._box.Box = None,
resolution: phiml.math._shape.Shape = None,
extrapolation: float = None,
convert=True,
**resolution_: int) ‑> phi.field._field.Field-
Expand source code
def StaggeredGrid(values: Any = 0., boundary: float or Extrapolation = 0, bounds: Box or float = None, resolution: Shape or int = None, extrapolation: float or Extrapolation = None, convert=True, **resolution_: int or Tensor) -> Field: """ N-dimensional grid whose vector components are sampled at the respective face centers. A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation. Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components. See Also: `CenteredGrid`, `Grid`, `Field`, `Field`, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html Args: values: Values to use for the grid. Has to be one of the following: * `phi.geom.Geometry`: sets inside values to 1, outside to 0 * `Field`: resamples the Field to the staggered sample points * `Number`: uses the value for all sample points * `tuple` or `list`: interprets the sequence as vector, used for all sample points * `phi.math.Tensor` with staggered shape: uses tensor values as grid values. Must contain a `vector` dimension with each slice consisting of one more element along the dimension they describe. Use `phi.math.stack()` to manually create this non-uniform tensor. * Function `values(x)` where `x` is a `phi.math.Tensor` representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function. boundary: The grid extrapolation determines the value outside the `values` tensor. Allowed types: `float`, `phi.math.Tensor`, `phi.math.extrapolation.Extrapolation`. bounds: Physical size and location of the grid as `phi.geom.Box`. If the resolution is determined through `resolution` of `values`, a `float` can be passed for `bounds` to create a unit box. resolution: Grid resolution as purely spatial `phi.math.Shape`. If `bounds` is given as a `Box`, the resolution may be specified as an `int` to be equal along all axes. convert: Whether to convert `values` to the default backend. **resolution_: Spatial dimensions as keyword arguments. Typically either `resolution` or `spatial_dims` are specified. """ extrapolation = boundary if extrapolation is None else extrapolation extrapolation = as_boundary(extrapolation, UniformGrid) if resolution is None and not resolution_: assert isinstance(values, Tensor), "Grid resolution must be specified when 'values' is not a Tensor." if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in spatial(values).names): # non-uniform values required if '~vector' not in values.shape: values = unstack_staggered_tensor(values, extrapolation) resolution = resolution_from_staggered_tensor(values, extrapolation) else: resolution = spatial(values) bounds = bounds or Box(math.const_vec(0, resolution), math.wrap(resolution, channel('vector'))) elements = UniformGrid(resolution, bounds) else: resolution = _get_resolution(resolution, resolution_, bounds) elements = UniformGrid(resolution, bounds) if isinstance(values, math.Tensor): if not spatial(values): values = expand_staggered(values, resolution, extrapolation) if not all(extrapolation.valid_outer_faces(d)[0] != extrapolation.valid_outer_faces(d)[1] for d in resolution.names): # non-uniform values required if '~vector' not in values.shape: # legacy behavior: we are given a padded staggered tensor values = unstack_staggered_tensor(values, extrapolation) resolution = resolution_from_staggered_tensor(values, extrapolation) elements = UniformGrid(resolution, bounds) else: # Keep dim order from data and check it matches resolution assert set(resolution_from_staggered_tensor(values, extrapolation)) == set(resolution), f"Failed to create StaggeredGrid: values {values.shape} do not match given resolution {resolution} for extrapolation {extrapolation}. See https://tum-pbs.github.io/PhiFlow/Staggered_Grids.html" elif isinstance(values, (Geometry, Field, FieldInitializer)): values = sample(values, elements, at='face', boundary=extrapolation, dot_face_normal=elements) elif callable(values): values = sample_function(values, elements, 'face', extrapolation) if elements.shape.shape.rank > 1: # Different number of X and Y faces assert isinstance(values, TensorStack), f"values function must return a staggered Tensor but returned {type(values)}" assert '~vector' in values.shape if 'vector' in values.shape: values = math.stack([values[{'vector': i, '~vector': i}] for i in range(resolution.rank)], dual(vector=resolution)) else: values = expand_staggered(math.tensor(values, convert=convert), resolution, extrapolation) if values.dtype.kind not in (float, complex): values = math.to_float(values) assert resolution.spatial_rank == elements.bounds.spatial_rank, f"Resolution {resolution} does not match bounds {elements.bounds}" if 'vector' in values.shape: values = rename_dims(values, 'vector', dual(vector=values.vector.item_names)) assert values.shape.spatial_rank == elements.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.spatial_rank == elements.bounds.spatial_rank, f"Spatial dimensions of values ({values.shape}) do not match elements {elements}" assert values.shape.instance_rank == 0, f"Instance dimensions not supported for grids. Got values with shape {values.shape}" return Field(elements, values, extrapolation)
N-dimensional grid whose vector components are sampled at the respective face centers. A staggered grid is defined through its values tensor, its bounds describing the physical size, and its extrapolation.
Staggered grids support batch and spatial dimensions but only one channel dimension for the staggered vector components.
See Also:
CenteredGrid()
,Field
,Field
,Field
, module documentation at https://tum-pbs.github.io/PhiFlow/Fields.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 pointsphi.math.Tensor
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. Usephi.math.stack()
to manually create this non-uniform tensor.- Function
values(x)
wherex
is aphi.math.Tensor
representing the physical location. The spatial dimensions of the grid will be passed as batch dimensions to the function.
boundary
- The grid extrapolation determines the value outside the
values
tensor. Allowed types:float
,phi.math.Tensor
,phi.math.extrapolation.Extrapolation
. bounds
- Physical size and location of the grid as
Box
. If the resolution is determined throughresolution
ofvalues
, afloat
can be passed forbounds
to create a unit box. resolution
- Grid resolution as purely spatial
phi.math.Shape
. Ifbounds
is given as aBox
, the resolution may be specified as anint
to be equal along all axes. convert
- Whether to convert
values
to the default backend. **resolution_
- Spatial dimensions as keyword arguments. Typically either
resolution
orspatial_dims
are specified.
def abs(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def abs_(x: TensorOrTree) -> TensorOrTree: """ Computes *||x||<sub>1</sub>*. Complex `x` result in matching precision float values. *Note*: The gradient of this operation is undefined for *x=0*. TensorFlow and PyTorch return 0 while Jax returns 1. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` Returns: Absolute value of `x` of same type as `x`. """ return _backend_op1(x, Backend.abs)
Computes ||x||1. Complex
x
result in matching precision float values.Note: The gradient of this operation is undefined for x=0. TensorFlow and PyTorch return 0 while Jax returns 1.
Args
x
Tensor
orphiml.math.magic.PhiTreeNode
Returns
Absolute value of
x
of same type asx
. def as_boundary(obj: phiml.math.extrapolation.Extrapolation | phiml.math._tensors.Tensor | float | phi.field._field.Field | None) ‑> phiml.math.extrapolation.Extrapolation
-
Expand source code
def as_boundary(obj: Union[Extrapolation, Tensor, float, Field, None], _geometry=None) -> Extrapolation: """ Returns an `Extrapolation` representing `obj`. Args: obj: One of * `float` or `Tensor`: Extrapolate with a constant value * `Extrapolation`: Use as-is. * `Field`: Sample values from `obj`, embedding another field inside `obj`. Returns: `Extrapolation` """ return obj.as_boundary() if isinstance(obj, Field) else math.extrapolation.as_extrapolation(obj)
Returns an
Extrapolation
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
def assert_close(*fields: phi.field._field.Field,
rel_tolerance: float = 1e-05,
abs_tolerance: float = 0,
msg: str = '',
verbose: bool = True)-
Expand source code
def assert_close(*fields: Field or Tensor or Number, rel_tolerance: float = 1e-5, abs_tolerance: float = 0, msg: str = "", verbose: bool = True): """ Raises an AssertionError if the `values` of the given fields are not close. See `phi.math.assert_close()`. """ f0 = next(filter(lambda t: isinstance(t, Field), fields)) values = [(f @ f0).values if isinstance(f, Field) else math.wrap(f) for f in fields] math.assert_close(*values, rel_tolerance=rel_tolerance, abs_tolerance=abs_tolerance, msg=msg, verbose=verbose)
Raises an AssertionError if the
values
of the given fields are not close. Seephi.math.assert_close()
. def bake_extrapolation(grid: phi.field._field.Field) ‑> phi.field._field.Field
-
Expand source code
def bake_extrapolation(grid: Field) -> Field: """ Pads `grid` with its current extrapolation. For `StaggeredGrid`s, the resulting grid will have a consistent shape, independent of the original extrapolation. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: Padded grid with extrapolation `phi.math.extrapolation.NONE`. """ if grid.extrapolation == math.extrapolation.NONE: return grid if grid.is_grid and grid.is_staggered: padded = [] for dim in grid.vector.item_names: lower, upper = grid.extrapolation.valid_outer_faces(dim) value = grid.vector[dim].values padded.append(math.pad(value, {dim: (0 if lower else 1, 0 if upper else 1)}, grid.extrapolation[{'vector': dim}], bounds=grid.bounds)) return StaggeredGrid(math.stack(padded, dual(vector=grid.shape.spatial)), bounds=grid.bounds, extrapolation=math.extrapolation.NONE) elif grid.is_grid: return pad(grid, 1).with_extrapolation(math.extrapolation.NONE) else: raise ValueError(f"Not a valid grid: {grid}")
Pads
grid
with its current extrapolation. ForStaggeredGrid()
s, the resulting grid will have a consistent shape, independent of the original extrapolation.Args
grid
CenteredGrid()
orStaggeredGrid()
.
Returns
Padded grid with extrapolation
phi.math.extrapolation.NONE
. def cast(x: ~MagicType, dtype: phiml.backend._dtype.DType | type) ‑> ~OtherMagicType
-
Expand source code
def cast(x: MagicType, dtype: Union[DType, type]) -> OtherMagicType: """ Casts `x` to a different data type. Implementations: * NumPy: [`x.astype()`](numpy.ndarray.astype) * PyTorch: [`x.to()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.to) * TensorFlow: [`tf.cast`](https://www.tensorflow.org/api_docs/python/tf/cast) * Jax: [`jax.numpy.array`](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html) See Also: `to_float`, `to_int32`, `to_int64`, `to_complex`. Args: x: `Tensor` dtype: New data type as `phiml.math.DType`, e.g. `DType(int, 16)`. Returns: `Tensor` with data type `dtype` """ if not isinstance(dtype, DType): dtype = DType.as_dtype(dtype) if hasattr(x, '__cast__'): return x.__cast__(dtype) elif isinstance(x, (Number, bool)): return dtype.kind(x) elif isinstance(x, PhiTreeNode): attrs = {key: getattr(x, key) for key in value_attributes(x)} new_attrs = {k: cast(v, dtype) for k, v in attrs.items()} return copy_with(x, **new_attrs) try: backend = choose_backend(x) return backend.cast(x, dtype) except NoBackendFound: if dtype.kind == bool: return bool(x) raise ValueError(f"Cannot cast object of type '{type(x).__name__}'")
Casts
x
to a different data type.Implementations:
- 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
phiml.math.DType
, e.g.DType(int, 16)
.
Returns
Tensor
with data typedtype
- NumPy:
def ceil(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def ceil(x: TensorOrTree) -> TensorOrTree: """ Computes *⌈x⌉* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.ceil)
Computes ⌈x⌉ of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def center_of_mass(density: phi.field._field.Field)
-
Expand source code
def center_of_mass(density: Field): """ Compute the center of mass of a density field. Args: density: Scalar `Field` Returns: `Tensor` holding only batch dimensions. """ assert 'vector' not in density.shape return mean(density.points * density) / mean(density)
Compute the center of mass of a density field.
Args
density
- Scalar
Field
Returns
Tensor
holding only batch dimensions. def concat(fields: Sequence[phi.field._field.Field], dim: str) ‑> phi.field._field.Field
-
Expand source code
def concat(fields: Sequence[Field], dim: str or Shape) -> Field: """ Concatenates the given `Field`s along `dim`. See Also: `stack()`. Args: fields: List of matching `Field` instances. dim: Concatenation dimension as `Shape`. Size is ignored. Returns: `Field` matching concatenated fields. """ assert all(isinstance(f, Field) for f in fields) assert all(isinstance(f, type(fields[0])) for f in fields) if any(f.extrapolation != fields[0].extrapolation for f in fields): raise NotImplementedError("Concatenating extrapolations not supported") if fields[0].is_grid: values = math.concat([f.values for f in fields], dim) return fields[0].with_values(values) elif fields[0].is_point_cloud or fields[0].is_graph: geometry = geom.concat([f.geometry for f in fields], dim) values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim) return PointCloud(elements=geometry, values=values, extrapolation=fields[0].extrapolation) elif fields[0].is_mesh: assert all([f.geometry == fields[0].geometry for f in fields]) values = math.concat([math.expand(f.values, f.shape.only(dim)) for f in fields], dim) return Field(fields[0].geometry, values, fields[0].extrapolation) raise NotImplementedError(type(fields[0]))
def convert(x, backend: phiml.backend._backend.Backend = None, use_dlpack=True)
-
Expand source code
def convert(x, backend: Backend = None, use_dlpack=True): """ Convert the native representation of a `Tensor` or `phiml.math.magic.PhiTreeNode` to the native format of `backend`. *Warning*: This operation breaks the automatic differentiation chain. See Also: `phiml.math.backend.convert()`. Args: x: `Tensor` to convert. If `x` is a `phiml.math.magic.PhiTreeNode`, its variable attributes are converted. backend: Target backend. If `None`, uses the current default backend, see `phiml.math.backend.default_backend()`. Returns: `Tensor` with native representation belonging to `backend`. """ if isinstance(x, Tensor): return x._op1(lambda native: b_convert(native, backend, use_dlpack=use_dlpack)) elif isinstance(x, PhiTreeNode): return tree_map(convert, x, backend=backend, use_dlpack=use_dlpack) else: return b_convert(x, backend, use_dlpack=use_dlpack)
Convert the native representation of a
Tensor
orphiml.math.magic.PhiTreeNode
to the native format ofbackend
.Warning: This operation breaks the automatic differentiation chain.
See Also:
phiml.math.backend.convert()
.Args
x
Tensor
to convert. Ifx
is aphiml.math.magic.PhiTreeNode
, its variable attributes are converted.backend
- Target backend. If
None
, uses the current default backend, seephiml.math.backend.default_backend()
.
Returns
Tensor
with native representation belonging tobackend
. def cos(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def cos(x: TensorOrTree) -> TensorOrTree: """ Computes *cos(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.cos)
Computes cos(x) of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def curl(field: phi.field._field.Field, at='corner')
-
Expand source code
def curl(field: Field, at='corner'): """ Computes the finite-difference curl of the give 2D `StaggeredGrid`. Args: field: `Field` at: Either `center` or `face`. """ assert 'vector' in field.shape, f"curl requires a vector field but got {field}" assert field.spatial_rank in (2, 3), "curl is only defined in 2 and 3 spatial dimensions." if field.is_grid and field.is_staggered and field.spatial_rank == 2 and at == 'corner': x, y = field.vector.item_names values = field.with_boundary(None).values vx = math.pad(values.vector.dual[x], {y: (1, 1)}, field.extrapolation[{'vector': y}]) vy = math.pad(values.vector.dual[y], {x: (1, 1)}, field.extrapolation[{'vector': x}]) vy_dx = math.spatial_gradient(vy, dims=x, dx=field.dx[x], padding=None, stack_dim=None, difference='forward') vx_dy = math.spatial_gradient(vx, dims=y, dx=field.dx[y], padding=None, stack_dim=None, difference='forward') curl_val = vy_dx - vx_dy corners = UniformGrid(field.resolution + 1, Box(field.bounds.lower - field.dx / 2, field.bounds.upper + field.dx / 2)) return Field(corners, curl_val, field.boundary.spatial_gradient()) elif field.is_grid and field.is_centered and field.spatial_rank == 2 and at == 'corner': x, y = field.vector.item_names values = pad(field, 1).values diag_basis = wrap([(1, 1), (1, -1)], channel(diag='pos,neg'), dual(vector=[x, y])) diag_comp = diag_basis @ values ll = diag_comp[{x: slice(-1), y: slice(-1), 'diag': 'neg'}] ul = diag_comp[{x: slice(-1), y: slice(1, None), 'diag': 'pos'}] lr = diag_comp[{x: slice(1, None), y: slice(-1), 'diag': 'pos'}] ur = diag_comp[{x: slice(1, None), y: slice(1, None), 'diag': 'neg'}] curl_val = ll - ul + lr - ur corners = UniformGrid(field.resolution + 1, Box(field.bounds.lower - field.dx / 2, field.bounds.upper + field.dx / 2)) return Field(corners, curl_val, field.boundary.spatial_gradient()) # if field.is_grid and not field.is_staggered and field.spatial_rank == 2: # if 'vector' not in field.shape and at == 'face': # # 2D curl of scalar field # grad = math.spatial_gradient(field.values, dx=field.dx, difference='forward', padding=None, stack_dim=channel('vector')) # result = grad.vector[::-1] * (1, -1) # (d/dy, -d/dx) # bounds = Box(field.bounds.lower + 0.5 * field.dx, field.bounds.upper - 0.5 * field.dx) # lose 1 cell per dimension # return StaggeredGrid(result, bounds=bounds, extrapolation=field.extrapolation.spatial_gradient()) # if 'vector' in field.shape and at == 'center': # # 2D curl of vector field # x, y = field.resolution.names # vy_dx = math.spatial_gradient(field.values.vector[1], dx=field.dx[0], padding=field.extrapolation, dims=x, stack_dim=None) # vx_dy = math.spatial_gradient(field.values.vector[0], dx=field.dx[1], padding=field.extrapolation, dims=y, stack_dim=None) # c = vy_dx - vx_dy # return field.with_values(c) # elif field.is_grid and field.is_staggered and field.spatial_rank == 2: # if at == 'center': # values = bake_extrapolation(field).values # x_padded = math.pad(values.vector['x'], {'y': (1, 1)}, field.extrapolation[{'vector': 'x'}], bounds=field.bounds) # y_padded = math.pad(values.vector['y'], {'x': (1, 1)}, field.extrapolation[{'vector': 'y'}], bounds=field.bounds) # vx_dy = math.spatial_gradient(x_padded, field.dx, 'forward', None, dims='y', stack_dim=None) # vy_dx = math.spatial_gradient(y_padded, field.dx, 'forward', None, dims='x', stack_dim=None) # result = vy_dx - vx_dy # return CenteredGrid(result, field.extrapolation.spatial_gradient(), field.bounds) raise NotImplementedError("Only 2D curl at corner currently supported")
Computes the finite-difference curl of the give 2D
StaggeredGrid()
.Args
field
Field
at
- Either
center
orface
.
def divergence(field: phi.field._field.Field,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: phi.field._field.Field = None,
implicitness: int = None) ‑>CenteredGrid() at 0x7fe3745509a0> -
Expand source code
def divergence(field: Field, order=2, implicit: Solve = None, upwind: Field = None, implicitness: int = None) -> CenteredGrid: """ Computes the divergence of a grid using finite differences. This function can operate in two modes depending on the type of `field`: * `CenteredGrid` approximates the divergence at cell centers using central differences * `StaggeredGrid` exactly computes the divergence at cell centers Args: field: vector field as `CenteredGrid` or `StaggeredGrid` order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. implicitness: specifies the size of the implicit stencil in case an implicit treatment is used upwind: For unstructured meshes only. Whether to use upwind interpolation. Returns: Divergence field as `CenteredGrid` """ if field.is_mesh: field = field.at_faces(boundary=NONE, order=order, upwind=upwind) div = field.geometry.integrate_flux(field.values, divide_volume=True) return Field(field.geometry, div, field.boundary.spatial_gradient()) if order == 2: if field.is_staggered: field = bake_extrapolation(field) components = [] for dim in field.shape.spatial.names: div_dim = math.spatial_gradient(field.vector[dim].values, field.dx, 'forward', None, dims=dim, stack_dim=None) components.append(div_dim) data = math.sum(components, dim='0') return CenteredGrid(data, bounds=field.bounds, extrapolation=field.extrapolation.spatial_gradient()) elif field.is_centered: left, right = shift(field, (-1, 1), stack_dim=batch('div_')) grad = (right - left) / (field.dx * 2) components = [grad.vector[i].div_[i] for i in grad.div_.item_names] result = sum(components) return result else: components = [ spatial_gradient(f, dims=dim, at='center', order=order, implicit=implicit, implicitness=implicitness, stack_dim="sum:b").sum[0] for f, dim in zip(field.vector, field.shape.only(spatial).names)] return sum(components)
Computes the divergence of a grid using finite differences.
This function can operate in two modes depending on the type of
field
:CenteredGrid()
approximates the divergence at cell centers using central 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. implicitness
- specifies the size of the implicit stencil in case an implicit treatment is used
upwind
- For unstructured meshes only. Whether to use upwind interpolation.
Returns
Divergence field as
CenteredGrid()
def downsample2x(grid: phi.field._field.Field) ‑> phi.field._field.Field
-
Expand source code
def downsample2x(grid: Field) -> Field: """ Reduces the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation. See Also: `upsample2x()`. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: `Grid` of same type as `grid`. """ if grid.is_grid and grid.is_centered: values = math.downsample2x(grid.values, grid.extrapolation) return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation) elif grid.is_grid and grid.is_staggered: grid_ = grid.with_boundary(extrapolation.NONE) values = {} for dim in grid.vector.item_names: odd_discarded = grid_.values[{'~vector': dim, dim: slice(None, None, 2)}] others_interpolated = math.downsample2x(odd_discarded, grid.extrapolation, dims=grid.shape.spatial.without(dim)) values[dim] = others_interpolated return StaggeredGrid(math.stack(values, dual('vector')), None, grid.bounds).with_extrapolation(grid.extrapolation) else: raise ValueError(grid)
Reduces the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.
See Also:
upsample2x()
.Args
grid
CenteredGrid()
orStaggeredGrid()
.
Returns
Field
of same type asgrid
. def exp(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def exp(x: TensorOrTree) -> TensorOrTree: """ Computes *exp(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.exp)
Computes exp(x) of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def finite_fill(grid: phi.field._field.Field, distance=1, diagonal=True) ‑> phi.field._field.Field
-
Expand source code
def finite_fill(grid: Field, distance=1, diagonal=True) -> Field: """ Extrapolates values of `grid` which are marked by nonzero values in `valid` using `phi.math.masked_fill(). If `values` is a StaggeredGrid, its components get extrapolated independently. Args: grid: Grid holding the values for extrapolation and possible non-finite values to be filled. distance: Number of extrapolation steps, i.e. how far a cell can be from the closest finite value to get filled. diagonal: Whether to extrapolate values to their diagonal neighbors per step. Returns: grid: Grid with extrapolated values. valid: binary Grid marking all valid values after extrapolation. """ if grid.is_grid and grid.is_centered: new_values = math.finite_fill(grid.values, distance=distance, diagonal=diagonal, padding=grid.extrapolation) return grid.with_values(new_values) elif grid.is_grid and grid.is_staggered: new_values = [finite_fill(c, distance=distance, diagonal=diagonal).values for c in grid.vector] return grid.with_values(math.stack(new_values, channel(grid).as_dual())) else: raise ValueError(grid)
Extrapolates values of
grid
which are marked by nonzero values 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.
def floor(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def floor(x: TensorOrTree) -> TensorOrTree: """ Computes *⌊x⌋* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.floor)
Computes ⌊x⌋ of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def fourier_laplace(grid: phi.field._field.Field, times=1) ‑> phi.field._field.Field
-
Expand source code
def fourier_laplace(grid: Field, times=1) -> Field: """ See `phi.math.fourier_laplace()` """ assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC values = math.fourier_laplace(grid.values, dx=grid.dx, times=times) return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
See
phi.math.fourier_laplace()
def fourier_poisson(grid: phi.field._field.Field, times=1) ‑> phi.field._field.Field
-
Expand source code
def fourier_poisson(grid: Field, times=1) -> Field: """ See `phi.math.fourier_poisson()` """ assert grid.extrapolation.spatial_gradient() == math.extrapolation.PERIODIC values = math.fourier_poisson(grid.values, dx=grid.dx, times=times) return type(grid)(values=values, bounds=grid.bounds, extrapolation=grid.extrapolation)
See
phi.math.fourier_poisson()
def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-05, ignore_mean=False, n=2) ‑> phiml.math._tensors.Tensor
-
Expand source code
def frequency_loss(x, frequency_falloff: float = 100, threshold=1e-5, ignore_mean=False, n=2) -> Tensor: """ Penalizes the squared `values` in frequency (Fourier) space. Lower frequencies are weighted more strongly then higher frequencies, depending on `frequency_falloff`. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` Values to penalize, typically `actual - target`. frequency_falloff: Large values put more emphasis on lower frequencies, 1.0 weights all frequencies equally. *Note*: The total loss is not normalized. Varying the value will result in losses of different magnitudes. threshold: Frequency amplitudes below this value are ignored. Setting this to zero may cause infinities or NaN values during backpropagation. ignore_mean: If `True`, does not penalize the mean value (frequency=0 component). Returns: Scalar loss value """ assert n in (1, 2) if isinstance(x, Tensor): if ignore_mean: x -= math.mean(x, x.shape.non_batch) k_squared = math.sum_(math.fftfreq(x.shape.spatial) ** 2, channel) weights = math.exp(-0.5 * k_squared * frequency_falloff ** 2) diff_fft = abs_square(math.fft(x) * weights) diff_fft = math.sqrt(math.maximum(diff_fft, threshold)) return l2_loss(diff_fft) if n == 2 else l1_loss(diff_fft) elif isinstance(x, PhiTreeNode): losses = [frequency_loss(getattr(x, a), frequency_falloff, threshold, ignore_mean, n) for a in value_attributes(x)] return sum(losses) else: raise ValueError(x)
Penalizes the squared
values
in frequency (Fourier) space. Lower frequencies are weighted more strongly then higher frequencies, depending onfrequency_falloff
.Args
x
Tensor
orphiml.math.magic.PhiTreeNode
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
def functional_gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Expand source code
def gradient(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the gradient of `f`. Example: ```python def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = gradient(loss_function, 'x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and gradient of `f` if `get_output=True`, else just the gradient of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=True)
Creates a function which computes the gradient of
f
.Example:
def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = gradient(loss_function, 'x,y', get_output=True)(x, y)
Functional gradients are implemented for the following backends:
- 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
. - PyTorch:
def gradient(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Expand source code
def gradient(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the gradient of `f`. Example: ```python def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = gradient(loss_function, 'x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and gradient of `f` if `get_output=True`, else just the gradient of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=True)
Creates a function which computes the gradient of
f
.Example:
def loss_function(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = gradient(loss_function, 'x', get_output=False)(x, y) (loss, prediction), (dx, dy) = gradient(loss_function, 'x,y', get_output=True)(x, y)
Functional gradients are implemented for the following backends:
- 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
. - PyTorch:
def imag(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def imag(x: TensorOrTree) -> TensorOrTree: """ Returns the imaginary part of `x`. If `x` does not store complex numbers, returns a zero tensor with the same shape and dtype as this tensor. See Also: `real()`, `conjugate()`. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` or native tensor. Returns: Imaginary component of `x` if `x` is complex, zeros otherwise. """ return _backend_op1(x, Backend.imag)
Returns the imaginary part of
x
. 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
orphiml.math.magic.PhiTreeNode
or native tensor.
Returns
Imaginary component of
x
ifx
is complex, zeros otherwise. def integrate(field: phi.field._field.Field, region: phi.geom._geom.Geometry, **kwargs) ‑> phiml.math._tensors.Tensor
-
Expand source code
def integrate(field: Field, region: Geometry, **kwargs) -> Tensor: """ Computes *∫<sub>R</sub> f(x) dx<sup>d</sup>* , where *f* denotes the `Field`, *R* the `region` and *d* the number of spatial dimensions (`d=field.shape.spatial_rank`). Depending on the `sample` implementation for `field`, the integral may be a rough approximation. This method is currently only implemented for `CenteredGrid`. Args: field: `Field` to integrate. region: Region to integrate over. **kwargs: Specify numerical scheme. Returns: Integral as `phi.Tensor` """ if not field.is_grid and not field.is_staggered: raise NotImplementedError() return sample(field, region, **kwargs) * region.volume
Computes ∫R f(x) dxd , where f denotes the
Field
, R 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
def is_finite(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def is_finite(x: TensorOrTree) -> TensorOrTree: """ Returns a `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """ return _backend_op1(x, Backend.isfinite)
Returns a
Tensor
orphiml.math.magic.PhiTreeNode
matchingx
with valuesTrue
wherex
has a finite value andFalse
otherwise. def isfinite(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def is_finite(x: TensorOrTree) -> TensorOrTree: """ Returns a `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x` with values `True` where `x` has a finite value and `False` otherwise. """ return _backend_op1(x, Backend.isfinite)
Returns a
Tensor
orphiml.math.magic.PhiTreeNode
matchingx
with valuesTrue
wherex
has a finite value andFalse
otherwise. def jacobian(f: Callable, wrt: str = None, get_output=True) ‑> Callable
-
Expand source code
def jacobian(f: Callable, wrt: str = None, get_output=True) -> Callable: """ Creates a function which computes the Jacobian matrix of `f`. For scalar functions, consider using `gradient()` instead. Example: ```python def f(x, y): prediction = f(x) loss = math.l2_loss(prediction - y) return loss, prediction dx = jacobian(loss_function, wrt='x', get_output=False)(x, y) (loss, prediction), (dx, dy) = jacobian(loss_function, wrt='x,y', get_output=True)(x, y) ``` Functional gradients are implemented for the following backends: * PyTorch: [`torch.autograd.grad`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.grad) / [`torch.autograd.backward`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) * TensorFlow: [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape) * Jax: [`jax.grad`](https://jax.readthedocs.io/en/latest/jax.html#jax.grad) When the gradient function is invoked, `f` is called with tensors that track the gradient. For PyTorch, `arg.requires_grad = True` for all positional arguments of `f`. Args: f: Function to be differentiated. `f` must return a floating point `Tensor` with rank zero. It can return additional tensors which are treated as auxiliary data and will be returned by the gradient function if `return_values=True`. All arguments for which the gradient is computed must be of dtype float or complex. get_output: Whether the gradient function should also return the return values of `f`. wrt: Comma-separated parameter names of `f` with respect to which the gradient should be computed. If not specified, the gradient will be computed w.r.t. the first positional argument (highly discouraged). Returns: Function with the same arguments as `f` that returns the value of `f`, auxiliary data and Jacobian of `f` if `get_output=True`, else just the Jacobian of `f`. """ f_params, wrt = simplify_wrt(f, wrt) return GradientFunction(f, f_params, wrt, get_output, is_f_scalar=False)
Creates a function which computes the Jacobian matrix of
f
. For scalar functions, consider usinggradient()
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
. - PyTorch:
def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) ‑> Callable
-
Expand source code
def jit_compile(f: Callable = None, auxiliary_args: str = '', forget_traces: bool = None) -> Callable: """ Compiles a graph based on the function `f`. The graph compilation is performed just-in-time (jit), e.g. when the returned function is called for the first time. The traced function will compute the same result as `f` but may run much faster. Some checks may be disabled in the compiled function. Can be used as a decorator: ```python @math.jit_compile def my_function(x: math.Tensor) -> math.Tensor: ``` Invoking the returned function may invoke re-tracing / re-compiling `f` after the first call if either * it is called with a different number of arguments, * the tensor arguments have different dimension names or types (the dimension order also counts), * any `Tensor` arguments require a different backend than previous invocations, * `phiml.math.magic.PhiTreeNode` positional arguments do not match in non-variable properties. Compilation is implemented for the following backends: * PyTorch: [`torch.jit.trace`](https://pytorch.org/docs/stable/jit.html) * TensorFlow: [`tf.function`](https://www.tensorflow.org/guide/function) * Jax: [`jax.jit`](https://jax.readthedocs.io/en/latest/notebooks/quickstart.html#using-jit-to-speed-up-functions) Jit-compilations cannot be nested, i.e. you cannot call `jit_compile()` while another function is being compiled. An exception to this is `jit_compile_linear()` which can be called from within a jit-compiled function. See Also: `jit_compile_linear()` Args: f: Function to be traced. All positional arguments must be of type `Tensor` or `phiml.math.magic.PhiTreeNode` returning a single `Tensor` or `phiml.math.magic.PhiTreeNode`. auxiliary_args: Comma-separated parameter names of arguments that are not relevant to backpropagation. forget_traces: If `True`, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces. Returns: Function with similar signature and return values as `f`. """ if f is None: kwargs = {k: v for k, v in locals().items() if v is not None} return partial(jit_compile, **kwargs) auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip()) return f if isinstance(f, (JitFunction, LinearFunction)) and f.auxiliary_args == auxiliary_args else JitFunction(f, auxiliary_args, forget_traces or False)
Compiles a graph based on the function
f
. The graph compilation is performed just-in-time (jit), e.g. when the returned function is called for the first time.The traced function will compute the same result as
f
but may run much faster. Some checks may be disabled in the compiled function.Can be used as a decorator:
@math.jit_compile def my_function(x: math.Tensor) -> math.Tensor:
Invoking the returned function may invoke re-tracing / re-compiling
f
after the first call if either- it is called with a different number of arguments,
- the tensor arguments have different dimension names or types (the dimension order also counts),
- any
Tensor
arguments require a different backend than previous invocations, phiml.math.magic.PhiTreeNode
positional arguments do not match in non-variable properties.
Compilation is implemented for the following backends:
- 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
orphiml.math.magic.PhiTreeNode
returning a singleTensor
orphiml.math.magic.PhiTreeNode
. auxiliary_args
- Comma-separated parameter names of arguments that are not relevant to backpropagation.
forget_traces
- If
True
, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces.
Returns
Function with similar signature and return values as
f
. def jit_compile_linear(f: Callable[[~X], ~Y] = None,
auxiliary_args: str = None,
forget_traces: bool = None) ‑> LinearFunction[X, Y]-
Expand source code
def jit_compile_linear(f: Callable[[X], Y] = None, auxiliary_args: str = None, forget_traces: bool = None) -> 'LinearFunction[X, Y]': """ Compile an optimized representation of the linear function `f`. For backends that support sparse tensors, a sparse matrix will be constructed for `f`. Can be used as a decorator: ```python @math.jit_compile_linear def my_linear_function(x: math.Tensor) -> math.Tensor: ``` Unlike `jit_compile()`, `jit_compile_linear()` can be called during a regular jit compilation. See Also: `jit_compile()` Args: f: Function that is linear in its positional arguments. All positional arguments must be of type `Tensor` and `f` must return a `Tensor`. auxiliary_args: Which parameters `f` is not linear in. These arguments are treated as conditioning arguments and will cause re-tracing on change. forget_traces: If `True`, only remembers the most recent compiled instance of this function. Upon tracing with new instance (due to changed shapes or auxiliary args), deletes the previous traces. Returns: `LinearFunction` with similar signature and return values as `f`. """ if f is None: kwargs = {k: v for k, v in locals().items() if v is not None} return partial(jit_compile_linear, **kwargs) if isinstance(f, JitFunction): f = f.f # cannot trace linear function from jitted version if isinstance(auxiliary_args, str): auxiliary_args = set(s.strip() for s in auxiliary_args.split(',') if s.strip()) else: assert auxiliary_args is None f_params = function_parameters(f) auxiliary_args = f_params[1:] return f if isinstance(f, LinearFunction) and f.auxiliary_args == auxiliary_args else LinearFunction(f, auxiliary_args, forget_traces or False)
Compile an optimized representation of the linear function
f
. For backends that support sparse tensors, a sparse matrix will be constructed 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
. def l1_loss(x,
reduce: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function non_batch>) ‑> phiml.math._tensors.Tensor-
Expand source code
def l1_loss(x, reduce: DimFilter = math.non_batch) -> Tensor: """ Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>1</sub>*, summing over all non-batch dimensions. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` or 0D or 1D native tensor. For `phiml.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed. reduce: Dimensions to reduce as `DimFilter`. Returns: loss: `Tensor` """ if isinstance(x, Tensor): return math.sum_(abs(x), reduce) elif isinstance(x, PhiTreeNode): return sum([l1_loss(getattr(x, a), reduce) for a in value_attributes(x)]) else: try: backend = math.choose_backend(x) shape = backend.staticshape(x) if len(shape) == 0: return abs(x) elif len(shape) == 1: return backend.sum(abs(x)) else: raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-ML tensors.") except math.NoBackendFound: raise ValueError(x)
Computes ∑i ||xi||1, summing over all non-batch dimensions.
Args
x
Tensor
orphiml.math.magic.PhiTreeNode
or 0D or 1D native tensor. Forphiml.math.magic.PhiTreeNode
objects, only value the sum over all value attributes is computed.reduce
- Dimensions to reduce as
DimFilter
.
Returns
loss
Tensor
def l2_loss(x,
reduce: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function non_batch>) ‑> phiml.math._tensors.Tensor-
Expand source code
def l2_loss(x, reduce: DimFilter = math.non_batch) -> Tensor: """ Computes *∑<sub>i</sub> ||x<sub>i</sub>||<sub>2</sub><sup>2</sup> / 2*, summing over all non-batch dimensions. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` or 0D or 1D native tensor. For `phiml.math.magic.PhiTreeNode` objects, only value the sum over all value attributes is computed. reduce: Dimensions to reduce as `DimFilter`. Returns: loss: `Tensor` """ if isinstance(x, Tensor): if x.dtype.kind == complex: x = abs(x) return math.sum_(x ** 2, reduce) * 0.5 elif isinstance(x, PhiTreeNode): return sum([l2_loss(getattr(x, a), reduce) for a in value_attributes(x)]) else: try: backend = math.choose_backend(x) shape = backend.staticshape(x) if len(shape) == 0: return x ** 2 * 0.5 elif len(shape) == 1: return backend.sum(x ** 2) * 0.5 else: raise ValueError("l2_loss is only defined for 0D and 1D native tensors. For higher-dimensional data, use Φ-ML tensors.") except math.NoBackendFound: raise ValueError(x)
Computes ∑i ||xi||22 / 2, summing over all non-batch dimensions.
Args
x
Tensor
orphiml.math.magic.PhiTreeNode
or 0D or 1D native tensor. Forphiml.math.magic.PhiTreeNode
objects, only value the sum over all value attributes is computed.reduce
- Dimensions to reduce as
DimFilter
.
Returns
loss
Tensor
def laplace(u: phi.field._field.Field,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: phi.field._field.Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
implicitness: int = None,
weights: phiml.math._tensors.Tensor | phi.field._field.Field = None,
upwind: phi.field._field.Field = None,
correct_skew=True) ‑> phi.field._field.Field-
Expand source code
def laplace(u: Field, axes: DimFilter = spatial, gradient: Field = None, order=2, implicit: math.Solve = None, implicitness: int = None, weights: Union[Tensor, Field] = None, upwind: Field = None, correct_skew=True) -> Field: """ Spatial Laplace operator for scalar grid. For grids, uses a finite difference scheme specified by `order` and `implicit`. For unstructured meshes, the scheme is specified via `order` and `upwind`. Args: u: n-dimensional grid or mesh. axes: The second derivative along these dimensions is summed over weights: (Optional) Multiply the axis terms by these factors before summation. Must be a `phi.math.Tensor` or `phi.field.Field` with a single channel dimension that lists all laplace axes by name. gradient: Only used by FVM at the moment. Approximate gradient of `u`, e.g. ∇u of the previous time step. If `None`, approximates the gradient as `(u_neighbor - u_self) / distance`. order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit (inherited from `phi.field.laplace()`). For FVM, the order is used when interpolating `v` and `prev_v` to cell faces if needed. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. implicitness: specifies the size of the implicit stencil in case an implicit treatment is used upwind: FVM only. Whether to use upwind interpolation. correct_skew: If `True`, adds a correction term for cell skewness. This requires `gradient` to be passed. Returns: laplacian field as `CenteredGrid` """ if implicitness is None: implicitness = 0 if implicit is None else 2 elif implicitness != 0: assert implicit is not None, "for implicit treatment a `Solve` is required" axes_names = u.shape.only(axes).names if isinstance(weights, Field): weights = weights.at(u).values if weights is not None: if channel(weights): assert set(channel(weights).item_names[0]) >= set(axes_names), f"the channel dim of weights must contain all laplace dims {axes_names} but only has {channel(weights).item_names}" # --- Mesh --- if u.is_mesh: if weights is not None and 'vector' in shape(weights): raise NotImplementedError(f"laplace on meshes is not yet supported with vector-valued weights") neighbor_val = u.mesh.pad_boundary(u.values, mode=u.boundary) nb_distances = u.mesh.neighbor_distances connecting_grad = (u.mesh.connectivity * neighbor_val - u.values) / nb_distances # (T_N - T_P) / d_PN if correct_skew and gradient is not None: # skewness correction assert dual(gradient).names == ('~vector',), f"gradient must contain one dual dim '~vector' listing the gradient components but got {gradient.shape}" gradient = gradient.at_faces(boundary=NONE, order=order, upwind=upwind).values nb_offsets = u.mesh.neighbor_offsets n1 = (u.face_normals.vector @ nb_offsets.vector) * nb_offsets / nb_distances ** 2 # (n·d_PN) d_PN / d_PN^2 n2 = u.face_normals - n1 ortho_correction = gradient @ n2 grad = connecting_grad * math.vec_length(n1) + ortho_correction else: assert not correct_skew, f"FVM skew correction only available when gradient is specified. Pass gradient or set correct_skew=False" grad = connecting_grad laplace_values = u.mesh.integrate_surface(grad) / u.mesh.volume # 1/V ∑_f ∇T ν A result = weights * laplace_values if weights is not None else laplace_values return Field(u.mesh, result, u.boundary - u.boundary) # --- Grid --- laplace_ext = u.extrapolation.spatial_gradient().spatial_gradient() laplace_dims = u.shape.only(axes).names if u.vector.exists and (u.is_centered or order > 2): fields = [f for f in u.vector] else: fields = [u] result = [] for f in fields: if order == 2: result.append(math.map_d2c(math.laplace)(f.values, dx=f.dx, padding=f.extrapolation, dims=axes, weights=weights, padding_kwargs={'bounds': f.bounds})) # uses ghost cells else: result_components = [perform_finite_difference_operation(f.values, dim, 2, f.dx.vector[dim], f.extrapolation, laplace_ext, 'center', order, implicit, implicitness) for dim in laplace_dims] if weights is not None: if channel(weights): result_components = [c * weights[ax] for c, ax in zip(result_components, axes_names)] else: result_components = [c * weights for c in result_components] result.append(sum(result_components)) if u.vector.exists and (u.is_centered or order > 2): if u.is_staggered: result = math.stack(result, dual(vector=u.vector.item_names)) else: result = math.stack(result, channel(vector=u.vector.item_names)) else: result = result[0] return u.with_values(result).with_extrapolation(laplace_ext)
Spatial Laplace operator for scalar grid.
For grids, uses a finite difference scheme specified by
order
andimplicit
. For unstructured meshes, the scheme is specified viaorder
andupwind
.Args
u
- n-dimensional grid or mesh.
axes
- The second derivative along these dimensions is summed over
weights
- (Optional) Multiply the axis terms by these factors before summation.
Must be a
phi.math.Tensor
orField
with a single channel dimension that lists all laplace axes by name. gradient
- Only used by FVM at the moment. Approximate gradient of
u
, e.g. ∇u of the previous time step. IfNone
, approximates the gradient as(u_neighbor - u_self) / distance
. order
- Spatial order of accuracy.
Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution.
Supported: 2 explicit, 4 explicit, 6 implicit (inherited from
laplace()
). For FVM, the order is used when interpolatingv
andprev_v
to cell faces if needed. implicit
- When a
Solve
object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. implicitness
- specifies the size of the implicit stencil in case an implicit treatment is used
upwind
- FVM only. Whether to use upwind interpolation.
correct_skew
- If
True
, adds a correction term for cell skewness. This requiresgradient()
to be passed.
Returns
laplacian field as
CenteredGrid()
def mask(obj: phi.field._field.Field) ‑> phi.field._field.Field
-
Expand source code
def mask(obj: Field or Geometry) -> Field: """ Returns a `Field` that masks the inside (or non-zero values when `obj` is a grid) of a physical object. The mask takes the value 1 inside the object and 0 outside. For `CenteredGrid` and `StaggeredGrid`, the mask labels non-zero non-NaN entries as 1 and all other values as 0 Returns: `Grid` type or `PointCloud` """ if isinstance(obj, Geometry): return Field(obj, 1, 0) assert isinstance(obj, Field), f"obj must be a Geometry or Field but got {type(obj)}" if obj.is_grid and not obj.is_staggered: values = math.cast(obj.values != 0, int) return obj.with_values(values) elif obj.is_staggered: raise NotImplementedError else: return Field(obj.elements, 1, math.extrapolation.remove_constant_offset(obj.extrapolation))
Returns a
Field
that masks the inside (or non-zero values 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
Field
type orPointCloud()
def maximum(f1: phi.field._field.Field, f2: phi.field._field.Field)
-
Expand source code
def maximum(f1: Field or Geometry or float, f2: Field or Geometry or float): """ Element-wise maximum. One of the given fields needs to be an instance of `Field` and the the result will be sampled at the corresponding points. If both are `Fields` but have different points, `f1` takes priority. Args: f1: `Field` or `Geometry` or constant. f2: `Field` or `Geometry` or constant. Returns: `Field` """ f1, f2 = _auto_resample(f1, f2) return f1.with_values(math.maximum(f1.values, f2.values))
def mean(field: phi.field._field.Field, dim=<function <lambda>>) ‑> phiml.math._tensors.Tensor
-
Expand source code
def mean(field: Field, dim=lambda s: s.non_channel.non_batch) -> Tensor: """ Computes the mean value by reducing all spatial / instance dimensions. Args: field: `Field` Returns: `phi.Tensor` """ result = math.mean(field.values, dim=dim) if (instance(field.geometry) & spatial(field.geometry)) in result.shape: return field.with_values(result) return result
Computes the mean value by reducing all spatial / instance dimensions.
Args
field
Field
Returns
phi.Tensor
def minimize(f: Callable[[~X], ~Y], solve: phiml.math._optimize.Solve[~X, ~Y]) ‑> ~X
-
Expand source code
def minimize(f: Callable[[X], Y], solve: Solve[X, Y]) -> X: """ Finds a minimum of the scalar function *f(x)*. The `method` argument of `solve` determines which optimizer is used. All optimizers supported by `scipy.optimize.minimize` are supported, see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html . Additionally a gradient descent solver with adaptive step size can be used with `method='GD'`. `math.minimize()` is limited to backends that support `jacobian()`, i.e. PyTorch, TensorFlow and Jax. To obtain additional information about the performed solve, use a `SolveTape`. See Also: `solve_nonlinear()`. Args: f: Function whose output is subject to minimization. All positional arguments of `f` are optimized and must be `Tensor` or `phiml.math.magic.PhiTreeNode`. If `solve.x0` is a `tuple` or `list`, it will be passed to *f* as varargs, `f(*x0)`. To minimize a subset of the positional arguments, define a new (lambda) function depending only on those. The first return value of `f` must be a scalar float `Tensor` or `phiml.math.magic.PhiTreeNode`. solve: `Solve` object to specify method type, parameters and initial guess for `x`. Returns: x: solution, the minimum point `x`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the optimization failed prematurely. """ solve = solve.with_defaults('optimization') assert (solve.rel_tol == 0).all, f"rel_tol must be zero for minimize() but got {solve.rel_tol}" assert solve.preprocess_y is None, "minimize() does not allow preprocess_y" x0_nest, x0_tensors = disassemble_tree(solve.x0, cache=True, attr_type=variable_attributes) x0_tensors = [to_float(t) for t in x0_tensors] backend = choose_backend_t(*x0_tensors, prefer_default=True) batch_dims = merge_shapes(*[batch(t) for t in x0_tensors]) x0_natives = [] x0_native_shapes = [] for t in x0_tensors: t = cached(t) if t.shape.is_uniform: x0_natives.append(reshaped_native(t, [batch_dims, t.shape.non_batch])) x0_native_shapes.append(t.shape.non_batch) else: for ut in unstack(t, t.shape.shape.without('dims')): x0_natives.append(reshaped_native(ut, [batch_dims, ut.shape.non_batch])) x0_native_shapes.append(ut.shape.non_batch) x0_flat = backend.concat(x0_natives, -1) def unflatten_assemble(x_flat, additional_dims: Shape = EMPTY_SHAPE, convert=True): partial_tensors = [] i = 0 for x0_native, t_shape in zip(x0_natives, x0_native_shapes): vol = backend.staticshape(x0_native)[-1] flat_native = x_flat[..., i:i + vol] partial_tensor = reshaped_tensor(flat_native, [*additional_dims, batch_dims, t_shape], convert=convert) partial_tensors.append(partial_tensor) i += vol # --- assemble non-uniform tensors --- x_tensors = [] for t in x0_tensors: if t.shape.is_uniform: x_tensors.append(partial_tensors.pop(0)) else: stack_dims = t.shape.shape.without('dims') x_tensors.append(stack(partial_tensors[:stack_dims.volume], stack_dims)) partial_tensors = partial_tensors[stack_dims.volume:] x = assemble_tree(x0_nest, x_tensors, attr_type=variable_attributes) return x def native_function(x_flat): x = unflatten_assemble(x_flat) if isinstance(x, (tuple, list)): y = f(*x) else: y = f(x) _, y_tensors = disassemble_tree(y, cache=False) loss_tensor = y_tensors[0] assert not non_batch(loss_tensor), f"Failed to minimize '{f.__name__}' because it returned a non-scalar output {shape(loss_tensor)}. Reduce all non-batch dimensions, e.g. using math.l2_loss()" extra_batch = loss_tensor.shape.without(batch_dims) if extra_batch: # output added more batch dims. We should expand the initial guess if extra_batch.volume > 1: raise NewBatchDims(loss_tensor.shape, extra_batch) else: loss_tensor = loss_tensor[next(iter(extra_batch.meshgrid()))] loss_native = reshaped_native(loss_tensor, [batch_dims], force_expand=False) return loss_tensor.sum, (loss_native,) atol = backend.to_float(reshaped_native(solve.abs_tol, [batch_dims])) maxi = reshaped_numpy(solve.max_iterations, [batch_dims]) trj = _SOLVE_TAPES and any(t.should_record_trajectory_for(solve) for t in _SOLVE_TAPES) t = time.perf_counter() try: ret = backend.minimize(solve.method, native_function, x0_flat, atol, maxi, trj) except NewBatchDims as new_dims: # try again with expanded initial guess warnings.warn(f"Function returned objective value with dims {new_dims.output_shape} but initial guess was missing {new_dims.missing}. Trying again with expanded initial guess.", RuntimeWarning, stacklevel=2) x0 = expand(solve.x0, new_dims.missing) solve = copy_with(solve, x0=x0) return minimize(f, solve) t = time.perf_counter() - t if not trj: assert isinstance(ret, SolveResult) converged = reshaped_tensor(ret.converged, [batch_dims]) diverged = reshaped_tensor(ret.diverged, [batch_dims]) x = unflatten_assemble(ret.x) iterations = reshaped_tensor(ret.iterations, [batch_dims]) function_evaluations = reshaped_tensor(ret.function_evaluations, [batch_dims]) residual = reshaped_tensor(ret.residual, [batch_dims]) result = SolveInfo(solve, x, residual, iterations, function_evaluations, converged, diverged, ret.method, ret.message, t) else: # trajectory assert isinstance(ret, (tuple, list)) and all(isinstance(r, SolveResult) for r in ret) converged = reshaped_tensor(ret[-1].converged, [batch_dims]) diverged = reshaped_tensor(ret[-1].diverged, [batch_dims]) x = unflatten_assemble(ret[-1].x) x_ = unflatten_assemble(numpy.stack([r.x for r in ret]), additional_dims=batch('trajectory'), convert=False) residual = stack([reshaped_tensor(r.residual, [batch_dims]) for r in ret], batch('trajectory')) iterations = reshaped_tensor(ret[-1].iterations, [batch_dims]) function_evaluations = stack([reshaped_tensor(r.function_evaluations, [batch_dims]) for r in ret], batch('trajectory')) result = SolveInfo(solve, x_, residual, iterations, function_evaluations, converged, diverged, ret[-1].method, ret[-1].message, t) for tape in _SOLVE_TAPES: tape._add(solve, trj, result) result.convergence_check(False) # raises ConvergenceException return x
Finds a minimum of the scalar function f(x). The
method
argument 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
orphiml.math.magic.PhiTreeNode
. 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
orphiml.math.magic.PhiTreeNode
. 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.
def minimum(f1: phi.field._field.Field, f2: phi.field._field.Field)
-
Expand source code
def minimum(f1: Field or Geometry or float, f2: Field or Geometry or float): """ Element-wise minimum. One of the given fields needs to be an instance of `Field` and the the result will be sampled at the corresponding points. If both are `Fields` but have different points, `f1` takes priority. Args: f1: `Field` or `Geometry` or constant. f2: `Field` or `Geometry` or constant. Returns: `Field` """ f1, f2 = _auto_resample(f1, f2) return f1.with_values(math.minimum(f1.values, f2.values))
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) ‑> phiml.math._tensors.Tensor | phi.field._field.Field
-
Expand source code
def native_call(f, *inputs, channels_last=None, channel_dim='vector', extrapolation=None) -> Union[Field, Tensor]: """ Similar to `phi.math.native_call()`. Args: f: Function to be called on native tensors of `inputs.values`. The function output must have the same dimension layout as the inputs and the batch size must be identical. *inputs: `Field` or `phi.Tensor` instances. extrapolation: (Optional) Extrapolation of the output field. If `None`, uses the extrapolation of the first input field. Returns: `Field` matching the first `Field` in `inputs`. """ input_tensors = [i.uniform_values() if isinstance(i, Field) else tensor(i) for i in inputs] values = math.native_call(f, *input_tensors, channels_last=channels_last, channel_dim=channel_dim) for i in inputs: if isinstance(i, Field): if not i.values.shape.is_uniform: values = unstack_staggered_tensor(values, i.boundary) result = i.with_values(values=values) if extrapolation is not None: result = result.with_extrapolation(extrapolation) return result else: raise AssertionError("At least one input must be a Field.")
Similar to
phi.math.native_call()
.Args
f
- Function to be called on native tensors of
inputs.values
. The function output must have the same dimension layout as the inputs and the batch size must be identical. *inputs
Field
orphi.Tensor
instances.extrapolation
- (Optional) Extrapolation of the output field. If
None
, uses the extrapolation of the first input field.
Returns
def normalize(field: phi.field._field.Field, norm: phi.field._field.Field, epsilon=1e-05)
-
Expand source code
def normalize(field: Field, norm: Field, epsilon=1e-5): """ Multiplies the values of `field` so that its sum matches the source. """ data = math.normalize_to(field.values, norm.values, epsilon) return field.with_values(data)
Multiplies the values of
field
so that its sum matches the source. def pack_dims(field: phi.field._field.Field,
dims: phiml.math._shape.Shape,
packed_dim: phiml.math._shape.Shape,
pos: int = None) ‑> phi.field._field.Field-
Expand source code
def pack_dims(field: Field, dims: Shape or tuple or list or str, packed_dim: Shape, pos: int or None = None) -> Field: """ Currently only supports grids and non-spatial dimensions. See Also: `phi.math.pack_dims()`. Args: field: `Field` Returns: `Field` of same type as `field`. """ if isinstance(field, Field): if spatial(field.shape.only(dims)): raise NotImplementedError("Packing spatial dimensions not supported for grids") return field.with_values(math.pack_dims(field.values, dims, packed_dim, pos)) else: raise NotImplementedError()
def pad(grid: phi.field._field.Field, widths: int | tuple | list | dict) ‑> phi.field._field.Field
-
Expand source code
def pad(grid: Field, widths: Union[int, tuple, list, dict]) -> Field: """ Pads a `Grid` using its extrapolation. Unlike `phi.math.pad()`, this function also affects the `bounds` of the grid, changing its size and origin depending on `widths`. Args: grid: `CenteredGrid` or `StaggeredGrid` widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: `Grid` of the same type as `grid` """ if isinstance(widths, int): widths = {axis: (widths, widths) for axis in grid.shape.spatial.names} elif isinstance(widths, (tuple, list)): widths = {axis: (width if isinstance(width, (tuple, list)) else (width, width)) for axis, width in zip(grid.shape.spatial.names, widths)} else: assert isinstance(widths, dict) widths_list = [widths[axis] if axis in widths.keys() else (0, 0) for axis in grid.shape.spatial.names] if grid.is_grid: if grid.is_staggered: data = math.pad(grid.values.vector.dual.as_channel(), widths, grid.extrapolation, bounds=grid.bounds).vector.as_dual() else: data = math.pad(grid.values, widths, grid.extrapolation, bounds=grid.bounds) w_lower = math.wrap([w[0] for w in widths_list]) w_upper = math.wrap([w[1] for w in widths_list]) bounds = Box(grid.bounds.lower - w_lower * grid.dx, grid.bounds.upper + w_upper * grid.dx) return create_similar_grid(grid, data, grid.extrapolation, bounds) raise NotImplementedError(f"{type(grid)} not supported. Only Grid instances allowed.")
Pads a
Field
using its extrapolation.Unlike
phi.math.pad()
, this function also affects 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
Field
of the same type asgrid
def read(file: str | phiml.math._tensors.Tensor, convert_to_backend=True) ‑> phi.field._field.Field
-
Expand source code
def read(file: Union[str, Tensor], convert_to_backend=True) -> Field: """ Loads a previously saved `Field` from disc. See Also: `write()`. Args: file: Single file as `str` or `Tensor` of string type. If `file` is a tensor, all contained files are loaded an stacked according to the dimensions of `file`. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Loaded `Field`. """ if isinstance(file, str): return read_single_field(file, convert_to_backend=convert_to_backend) if isinstance(file, Tensor): if file.rank == 0: return read_single_field(file.native(), convert_to_backend=convert_to_backend) else: dim = file.shape[0] files = math.unstack(file, dim.name) fields = [read(file_, convert_to_backend=convert_to_backend) for file_ in files] return stack(fields, dim) else: raise ValueError(file)
Loads a previously saved
Field
from disc.See Also:
write()
.Args
file
- Single file as
str
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
Field
. def real(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def real(x: TensorOrTree) -> TensorOrTree: """ See Also: `imag()`, `conjugate()`. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` or native tensor. Returns: Real component of `x`. """ return _backend_op1(x, Backend.real)
See Also:
imag()
,conjugate()
.Args
x
Tensor
orphiml.math.magic.PhiTreeNode
or native tensor.
Returns
Real component of
x
. def reduce_sample(field: phi.field._field.Field | phi.geom._geom.Geometry | phi.field._field.FieldInitializer | Callable,
geometry: phi.geom._geom.Geometry,
**kwargs) ‑> phiml.math._tensors.Tensor-
Expand source code
def reduce_sample(field: Union[Field, Geometry, FieldInitializer, Callable], geometry: Geometry or Field or Tensor, **kwargs) -> math.Tensor: """Alias for `sample()` with `dot_face_normal=field.geometry`.""" can_reduce = dual(field.values) in geometry.shape at = 'face' if dual(geometry) else 'center' return sample(field, geometry, at, field.boundary, dot_face_normal=field.geometry if can_reduce else None, **kwargs)
Alias for
sample()
withdot_face_normal=field.geometry
. def resample(value: phi.field._field.Field | phi.geom._geom.Geometry | phiml.math._tensors.Tensor | float | phi.field._field.FieldInitializer,
to: phi.field._field.Field | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs)-
Expand source code
def resample(value: Union[Field, Geometry, Tensor, float, FieldInitializer], to: Union[Field, Geometry], keep_boundary=False, **kwargs): """ Samples a `Field`, `Geometry` or value at the sample points of the field `to`. The result will approximate `value` on the data structure of `to`. Unlike `sample()`, this method returns a `Field` object, not a `Tensor`. Aliases: `value.at(to)`, (and the deprecated `value @ to`). See Also: `sample()`, `reduce_sample()`, `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields). Args: value: Object containing values to resample. This can be to: `Field` (`CenteredGrid`, `StaggeredGrid` or `PointCloud`) object defining the sample points. The current values of `to` are ignored. keep_boundary: Only available if `self` is a `Field`. If True, the resampled field will inherit the extrapolation from `self` instead of `representation`. This can result in non-compatible value tensors for staggered grids where the tensor size depends on the extrapolation type. **kwargs: Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points. Returns: Field object of same type as `representation` Examples: >>> grid = CenteredGrid(x=64, y=32) >>> field.resample(Noise(), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(1, to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(Box(x=1, y=2), to=grid) CenteredGrid[(xˢ=64, yˢ=32), size=(x=64, y=32), extrapolation=float64 0.0] >>> field.resample(grid, to=grid) == grid True """ assert isinstance(to, (Field, Geometry)), f"'to' must be a Field or Geomoetry but got {to}" if not isinstance(value, (Field, Geometry, FieldInitializer)): return to.with_values(value) if isinstance(value, Field) and keep_boundary: extrap = value.extrapolation elif isinstance(to, Field) and not keep_boundary: extrap = to.extrapolation else: raise AssertionError(f"Boundary cannot be determined, keep_boundary={keep_boundary}, value: {type(value)}, to: {type(to)}") resampled = sample(value, to, at=to.sampled_at if isinstance(to, Field) else 'center', boundary=extrap, dot_face_normal=to.geometry, **kwargs) return Field(to.geometry if isinstance(to, Field) else to, resampled, extrap)
Samples a
Field
,Geometry
or value at the sample points of the 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
Field
(CenteredGrid()
,StaggeredGrid()
orPointCloud()
) object defining the sample points. The current values ofto
are ignored.keep_boundary
- Only available if
self
is aField
. 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
def round(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def round_(x: TensorOrTree) -> TensorOrTree: """ Rounds the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to the closest integer. """ return _backend_op1(x, Backend.round)
Rounds the
Tensor
orphiml.math.magic.PhiTreeNode
x
to the closest integer. def safe_mul(x, y)
-
Expand source code
def safe_mul(x, y): """See `phiml.math.safe_mul()`""" x, y = _auto_resample(x, y) values = math.safe_mul(x.values, y.values) return x.with_values(values)
See
phiml.math.safe_mul()
def sample(field: phi.field._field.Field | phi.geom._geom.Geometry | phi.field._field.FieldInitializer | Callable,
geometry: phi.geom._geom.Geometry,
at: str = 'center',
boundary: phiml.math.extrapolation.Extrapolation | phiml.math._tensors.Tensor | numbers.Number = None,
dot_face_normal: phi.geom._geom.Geometry | None = None,
**kwargs) ‑> phiml.math._tensors.Tensor-
Expand source code
def sample(field: Union[Field, Geometry, FieldInitializer, Callable], geometry: Geometry or Field or Tensor, at: str = 'center', boundary: Union[Extrapolation, Tensor, Number] = None, dot_face_normal: Optional[Geometry] = None, **kwargs) -> math.Tensor: """ Computes the field value inside the volume of the (batched) `geometry`. The field value may be determined by integrating over the volume, sampling the central value or any other way. The batch dimensions of `geometry` are matched with this field. The `geometry` must not share any channel dimensions with this field. Spatial dimensions of `geometry` can be used to sample a grid of geometries. See Also: `Field.at()`, [Resampling overview](https://tum-pbs.github.io/PhiFlow/Fields.html#resampling-fields). Args: field: Source `Field` to sample. geometry: Single or batched `phi.geom.Geometry` or `Field` or location `Tensor`. When passing a `Field`, its `elements` are used as sample points. When passing a vector-valued `Tensor`, a `Point` geometry will be created. at: One of 'center', 'face', 'vertex' boundary: Target extrapolation. dot_face_normal: If not `None` and , `field` is a vector field and `at=='face'`, the dot product between sampled field vectors and the face normals is returned instead. **kwargs: Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points. Returns: Sampled values as a `phi.math.Tensor` """ # --- Process args --- assert at in ['center', 'face', 'vertex'] if at == 'face': assert boundary is not None, "boundaries must be given when sampling at faces" geometry = _get_geometry(geometry) boundary = as_boundary(boundary, geometry) if boundary is not None else None if dot_face_normal is True: dot_face_normal = geometry if isinstance(field, Geometry): from ._field_math import mask field = mask(field) if isinstance(field, FieldInitializer): values = field._sample(geometry, at, boundary, **kwargs) if dot_face_normal is not None and at == 'face' and channel(values): if _are_axis_aligned(dot_face_normal.face_normals): values = math.stack([values[{'vector': i, '~vector': i}] for i in range(geometry.spatial_rank)], dual(**geometry.shape['vector'].untyped_dict)) else: raise NotImplementedError field = Field(geometry, values, boundary) if callable(field): values = sample_function(field, geometry, at, boundary) field = Field(geometry, values, boundary) # --- Resample --- assert isinstance(field, Field), f"field must be a Field, Geometry or initializer but got {type(field)}" if at == 'center': if field.is_centered and field.sampled_elements.shallow_equals(geometry): return field.values if field.is_grid and not field.is_staggered: return sample_grid_at_centers(field, geometry, **kwargs) elif field.is_grid and field.is_staggered: return sample_staggered_grid(field, geometry, **kwargs) elif field.is_mesh and field.is_staggered: return math.finite_mean(field.values, dual) # ToDo weigh by face areas? elif field.is_mesh: return sample_mesh(field, geometry.center, **kwargs) else: return scatter_to_centers(field, geometry, **kwargs) elif at == 'face': if field.is_staggered and field.geometry.shallow_equals(geometry) and field.geometry.face_shape == geometry.face_shape and field.geometry.shallow_equals(dot_face_normal): return field.values elif dot_face_normal is not None and channel(field): if _are_axis_aligned(dot_face_normal.face_normals): components = unstack(field, field.shape.channel.name) faces = math.unstack(slice_off_constant_faces(geometry.faces, geometry.boundary_faces, boundary), dual) sampled = [sample(c, p, **kwargs) for c, p in zip(components, faces)] return math.stack(sampled, dual(dot_face_normal.face_shape)) else: raise NotImplementedError elif field.is_grid and field.is_centered: return sample_grid_at_faces(field, geometry, boundary, **kwargs) elif field.is_grid and field.is_staggered: faces = math.unstack(slice_off_constant_faces(geometry.faces, geometry.boundary_faces, boundary), dual) sampled = [sample(field, face, **kwargs) for face in faces] return math.stack(sampled, dual(geometry.face_shape)) elif field.is_mesh and field.is_centered and field.geometry.shallow_equals(geometry): return centroid_to_faces(field, boundary, **kwargs) elif field.is_mesh and field.is_staggered and field.geometry.shallow_equals(geometry): return field.with_boundary(boundary) else: return scatter_to_faces(field, geometry, boundary, **kwargs) # geom_ch = channel(geometry).without('vector') # assert all(dim not in field.shape for dim in geom_ch) # if geom_ch: # sampled = [field._sample(p, **kwargs) for p in geometry.unstack(geom_ch.name)] # return math.stack(sampled, geom_ch) elif at == 'vertex': raise NotImplementedError
Computes the field value inside the volume of the (batched)
geometry
.The field value may be determined by integrating over the volume, sampling the central value or any other way.
The batch dimensions of
geometry
are matched with this field. Thegeometry
must not share any channel dimensions with this field. Spatial dimensions ofgeometry
can be used to sample a grid of geometries.See Also:
Field.at()
, Resampling overview.Args
field
- Source
Field
to sample. geometry
- Single or batched
Geometry
orField
or locationTensor
. When passing aField
, itselements
are used as sample points. When passing a vector-valuedTensor
, aPoint
geometry will be created. at
- One of 'center', 'face', 'vertex'
boundary
- Target extrapolation.
dot_face_normal
- If not
None
and ,field
is a vector field andat=='face'
, the dot product between sampled field vectors and the face normals is returned instead. **kwargs
- Sampling arguments, e.g. to specify the numerical scheme. By default, linear interpolation is used. Grids also support 6th order implicit sampling at mid-points.
Returns
Sampled values as a
phi.math.Tensor
def shift(grid: phi.field._field.Field,
offsets: tuple,
stack_dim: phiml.math._shape.Shape | None = (shiftᶜ=None),
dims=<function spatial>,
pad=True)-
Expand source code
def shift(grid: Field, offsets: tuple, stack_dim: Optional[Shape] = channel('shift'), dims=spatial, pad=True): """ Wraps :func:`math.shift` for CenteredGrid. Args: grid: CenteredGrid: offsets: tuple: stack_dim: (Default value = 'shift') """ if pad: padding = grid.extrapolation new_bounds = grid.bounds else: padding = None max_lower_shift = min(offsets) if min(offsets) < 0 else 0 max_upper_shift = max(offsets) if max(offsets) > 0 else 0 w_lower = math.wrap([max_lower_shift if dim in dims else 0 for dim in grid.shape.spatial.names]) w_upper = math.wrap([max_upper_shift if dim in dims else 0 for dim in grid.shape.spatial.names]) new_bounds = Box(grid.bounds.lower - w_lower * grid.dx, grid.bounds.upper - w_upper * grid.dx) data = math.shift(grid.values, offsets, dims=dims, padding=padding, stack_dim=stack_dim) return [create_similar_grid(grid, data[i], grid.extrapolation, new_bounds) for i in range(len(offsets))]
Wraps :func:
math.shift
for CenteredGrid.Args
grid
- CenteredGrid:
offsets
- tuple:
stack_dim
- (Default value = 'shift')
def sign(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def sign(x: TensorOrTree) -> TensorOrTree: """ The sign of positive numbers is 1 and -1 for negative numbers. The sign of 0 is undefined. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` Returns: `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x`. """ return _backend_op1(x, Backend.sign)
The sign of positive numbers is 1 and -1 for negative numbers. The sign of 0 is undefined.
Args
x
Tensor
orphiml.math.magic.PhiTreeNode
Returns
Tensor
orphiml.math.magic.PhiTreeNode
matchingx
. def sin(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def sin(x: TensorOrTree) -> TensorOrTree: """ Computes *sin(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.sin)
Computes sin(x) of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def solve_linear(f: Callable[[~X], ~Y] | phiml.math._tensors.Tensor,
y: ~Y,
solve: phiml.math._optimize.Solve[~X, ~Y],
*f_args,
grad_for_f=False,
f_kwargs: dict = None,
**f_kwargs_) ‑> ~X-
Expand source code
def solve_linear(f: Union[Callable[[X], Y], Tensor], y: Y, solve: Solve[X, Y], *f_args, grad_for_f=False, f_kwargs: dict = None, **f_kwargs_) -> X: """ Solves the system of linear equations *f(x) = y* and returns *x*. This method will use the solver specified in `solve`. The following method identifiers are supported by all backends: * `'auto'`: Automatically choose a solver * `'CG'`: Conjugate gradient, only for symmetric and positive definite matrices. * `'CG-adaptive'`: Conjugate gradient with adaptive step size, only for symmetric and positive definite matrices. * `'biCG'` or `'biCG-stab(0)'`: Biconjugate gradient * `'biCG-stab'` or `'biCG-stab(1)'`: Biconjugate gradient stabilized, first order * `'biCG-stab(2)'`, `'biCG-stab(4)'`, ...: Biconjugate gradient stabilized, second or higher order * `'scipy-direct'`: SciPy direct solve always run oh the CPU using `scipy.sparse.linalg.spsolve`. * `'scipy-CG'`, `'scipy-GMres'`, `'scipy-biCG'`, `'scipy-biCG-stab'`, `'scipy-CGS'`, `'scipy-QMR'`, `'scipy-GCrotMK'`: SciPy iterative solvers always run oh the CPU, both in eager execution and JIT mode. For maximum performance, compile `f` using `jit_compile_linear()` beforehand. Then, an optimized representation of `f` (such as a sparse matrix) will be used to solve the linear system. **Caution:** The matrix construction may potentially be performed each time `solve_linear` is called if auxiliary arguments change. To prevent this, jit-compile the function that makes the call to `solve_linear`. To obtain additional information about the performed solve, perform the solve within a `SolveTape` context. The used implementation can be obtained as `SolveInfo.method`. The gradient of this operation will perform another linear solve with the parameters specified by `Solve.gradient_solve`. See Also: `solve_nonlinear()`, `jit_compile_linear()`. Args: f: One of the following: * Linear function with `Tensor` or `phiml.math.magic.PhiTreeNode` first parameter and return value. `f` can have additional auxiliary arguments and return auxiliary values. * Dense matrix (`Tensor` with at least one dual dimension) * Sparse matrix (Sparse `Tensor` with at least one dual dimension) * Native tensor (not yet supported) y: Desired output of `f(x)` as `Tensor` or `phiml.math.magic.PhiTreeNode`. solve: `Solve` object specifying optimization method, parameters and initial guess for `x`. *f_args: Positional arguments to be passed to `f` after `solve.x0`. These arguments will not be solved for. Supports vararg mode or pass all arguments as a `tuple`. f_kwargs: Additional keyword arguments to be passed to `f`. These arguments are treated as auxiliary arguments and can be of any type. Returns: x: solution of the linear system of equations `f(x) = y` as `Tensor` or `phiml.math.magic.PhiTreeNode`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the solve failed prematurely. """ assert solve.x0 is not None, "Please specify the initial guess as Solve(..., x0=initial_guess)" # --- Handle parameters --- f_kwargs = f_kwargs or {} f_kwargs.update(f_kwargs_) f_args = f_args[0] if len(f_args) == 1 and isinstance(f_args[0], tuple) else f_args # --- Get input and output tensors --- y_tree, y_tensors = disassemble_tree(y, cache=False, attr_type=value_attributes) x0_tree, x0_tensors = disassemble_tree(solve.x0, cache=False, attr_type=variable_attributes) assert len(x0_tensors) == len(y_tensors) == 1, "Only single-tensor linear solves are currently supported" if isinstance(y_tree, str) and y_tree == NATIVE_TENSOR and isinstance(x0_tree, str) and x0_tree == NATIVE_TENSOR: if callable(f): # assume batch + 1 dim rank = y_tensors[0].rank assert x0_tensors[0].rank == rank, f"y and x0 must have the same rank but got {y_tensors[0].shape.sizes} for y and {x0_tensors[0].shape.sizes} for x0" if rank == 0: y = wrap(y) x0 = wrap(solve.x0) else: y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector')) x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector')) solve = copy_with(solve, x0=x0) solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_) return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector') else: b = choose_backend(y, solve.x0, f) f_dims = b.staticshape(f) y_dims = b.staticshape(y) x_dims = b.staticshape(solve.x0) rank = len(f_dims) - 2 assert rank >= 0, f"f must be a matrix but got shape {f_dims}" f = wrap(f, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), dual('vector')) if len(x_dims) == len(f_dims): # matrix solve assert len(x_dims) == len(f_dims) assert x_dims[-2] == f_dims[-1] assert y_dims[-2] == f_dims[-2] y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), batch('extra_batch')) x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector'), batch('extra_batch')) solve = copy_with(solve, x0=x0) solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_) return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector,extra_batch') else: assert len(x_dims) == len(f_dims) - 1 assert x_dims[-1] == f_dims[-1] assert y_dims[-1] == f_dims[-2] y = wrap(y, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector')) x0 = wrap(solve.x0, *[batch(f'batch{i}') for i in range(rank - 1)], channel('vector')) solve = copy_with(solve, x0=x0) solution = solve_linear(f, y, solve, *f_args, grad_for_f=grad_for_f, f_kwargs=f_kwargs, **f_kwargs_) return solution.native(','.join([f'batch{i}' for i in range(rank - 1)]) + ',vector') backend = choose_backend_t(*y_tensors, *x0_tensors) prefer_explicit = backend.supports(Backend.sparse_coo_tensor) or backend.supports(Backend.csr_matrix) or grad_for_f if isinstance(f, Tensor) or (isinstance(f, LinearFunction) and prefer_explicit): # Matrix solve if isinstance(f, LinearFunction): x0 = math.convert(solve.x0, backend) matrix, bias = f.sparse_matrix_and_bias(x0, *f_args, **f_kwargs) else: matrix = f bias = 0 m_rank = _stored_matrix_rank(matrix) if solve.rank_deficiency is None: if m_rank is not None: estimated_deficiency = dual(matrix).volume - m_rank if (estimated_deficiency > 0).any: warnings.warn("Possible rank deficiency detected. Matrix might be singular which can lead to convergence problems. Please specify using Solve(rank_deficiency=...).") solve = copy_with(solve, rank_deficiency=0) else: solve = copy_with(solve, rank_deficiency=0) # no info or user input, assume not rank-deficient preconditioner = compute_preconditioner(solve.preconditioner, matrix, rank_deficiency=solve.rank_deficiency, target_backend=NUMPY if solve.method.startswith('scipy-') else backend, solver=solve.method) if solve.preconditioner is not None else None def _matrix_solve_forward(y, solve: Solve, matrix: Tensor, is_backprop=False): backend_matrix = native_matrix(matrix, choose_backend_t(*y_tensors, matrix)) pattern_dims_in = dual(matrix).as_channel().names pattern_dims_out = non_dual(matrix).names # batch dims can be sparse or batched matrices result = _linear_solve_forward(y, solve, backend_matrix, pattern_dims_in, pattern_dims_out, preconditioner, backend, is_backprop) return result # must return exactly `x` so gradient isn't computed w.r.t. other quantities _matrix_solve = attach_gradient_solve(_matrix_solve_forward, auxiliary_args=f'is_backprop,solve{",matrix" if matrix.default_backend == NUMPY else ""}', matrix_adjoint=grad_for_f) return _matrix_solve(y - bias, solve, matrix) else: # Matrix-free solve f_args = cached(f_args) solve = cached(solve) assert not grad_for_f, f"grad_for_f=True can only be used for math.jit_compile_linear functions but got '{f_name(f)}'. Please decorate the linear function with @jit_compile_linear" assert solve.preconditioner is None, f"Preconditioners not currently supported for matrix-free solves. Decorate '{f_name(f)}' with @math.jit_compile_linear to perform a matrix solve." def _function_solve_forward(y, solve: Solve, f_args: tuple, f_kwargs: dict = None, is_backprop=False): y_nest, (y_tensor,) = disassemble_tree(y, cache=False, attr_type=value_attributes) x0_nest, (x0_tensor,) = disassemble_tree(solve.x0, cache=False, attr_type=variable_attributes) # active_dims = (y_tensor.shape & x0_tensor.shape).non_batch # assumes batch dimensions are not active batches = (y_tensor.shape & x0_tensor.shape).batch def native_lin_f(native_x, batch_index=None): if batch_index is not None and batches.volume > 1: native_x = backend.tile(backend.expand_dims(native_x), [batches.volume, 1]) x = assemble_tree(x0_nest, [reshaped_tensor(native_x, [batches, non_batch(x0_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(x0_tensor)], convert=False)], attr_type=variable_attributes) y_ = f(x, *f_args, **f_kwargs) _, (y_tensor_,) = disassemble_tree(y_, cache=False, attr_type=value_attributes) assert set(non_batch(y_tensor_)) == set(non_batch(y_tensor)), f"Function returned dimensions {y_tensor_.shape} but right-hand-side has shape {y_tensor.shape}" y_native = reshaped_native(y_tensor_, [batches, non_batch(y_tensor)] if backend.ndims(native_x) >= 2 else [non_batch(y_tensor)]) # order like right-hand-side if batch_index is not None and batches.volume > 1: y_native = y_native[batch_index] return y_native result = _linear_solve_forward(y, solve, native_lin_f, pattern_dims_in=non_batch(x0_tensor).names, pattern_dims_out=non_batch(y_tensor).names, preconditioner=None, backend=backend, is_backprop=is_backprop) return result # must return exactly `x` so gradient isn't computed w.r.t. other quantities _function_solve = attach_gradient_solve(_function_solve_forward, auxiliary_args='is_backprop,f_kwargs,solve', matrix_adjoint=grad_for_f) return _function_solve(y, solve, f_args, f_kwargs=f_kwargs)
Solves the system of linear equations f(x) = y and returns x. This method will use the solver specified in
solve
. The following method identifiers are supported by all backends:'auto'
: Automatically choose a solver'CG'
: Conjugate gradient, only for symmetric and positive definite matrices.'CG-adaptive'
: Conjugate gradient with adaptive step size, only for symmetric and positive definite matrices.'biCG'
or'biCG-stab(0)'
: Biconjugate gradient'biCG-stab'
or'biCG-stab(1)'
: Biconjugate gradient stabilized, first order'biCG-stab(2)'
,'biCG-stab(4)'
, …: Biconjugate gradient stabilized, second or higher order'scipy-direct'
: SciPy direct solve always run oh the CPU usingscipy.sparse.linalg.spsolve
.'scipy-CG'
,'scipy-GMres'
,'scipy-biCG'
,'scipy-biCG-stab'
,'scipy-CGS'
,'scipy-QMR'
,'scipy-GCrotMK'
: SciPy iterative solvers always run oh the CPU, both in eager execution and JIT mode.
For maximum performance, compile
f
usingjit_compile_linear()
beforehand. Then, an optimized representation off
(such as a sparse matrix) will be used to solve the linear system.Caution: The matrix construction may potentially be performed each time
solve_linear()
is called if auxiliary arguments change. To prevent this, jit-compile the function that makes the call tosolve_linear()
.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
orphiml.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)
- Linear function with
y
- Desired output of
f(x)
asTensor
orphiml.math.magic.PhiTreeNode
. 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
orphiml.math.magic.PhiTreeNode
.
Raises
NotConverged
- If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
- If the solve failed prematurely.
def solve_nonlinear(f: Callable, y, solve: phiml.math._optimize.Solve) ‑> phiml.math._tensors.Tensor
-
Expand source code
def solve_nonlinear(f: Callable, y, solve: Solve) -> Tensor: """ Solves the non-linear equation *f(x) = y* by minimizing the norm of the residual. This method is limited to backends that support `jacobian()`, currently PyTorch, TensorFlow and Jax. To obtain additional information about the performed solve, use a `SolveTape`. See Also: `minimize()`, `solve_linear()`. Args: f: Function whose output is optimized to match `y`. All positional arguments of `f` are optimized and must be `Tensor` or `phiml.math.magic.PhiTreeNode`. The output of `f` must match `y`. y: Desired output of `f(x)` as `Tensor` or `phiml.math.magic.PhiTreeNode`. solve: `Solve` object specifying optimization method, parameters and initial guess for `x`. Returns: x: Solution fulfilling `f(x) = y` within specified tolerance as `Tensor` or `phiml.math.magic.PhiTreeNode`. Raises: NotConverged: If the desired accuracy was not be reached within the maximum number of iterations. Diverged: If the solve failed prematurely. """ def min_func(x): diff = f(x) - y l2 = l2_loss(diff) return l2 if solve.preprocess_y is not None: y = solve.preprocess_y(y) from ._nd import l2_loss solve = solve.with_defaults('solve') tol = math.maximum(solve.rel_tol * l2_loss(y), solve.abs_tol) min_solve = copy_with(solve, abs_tol=tol, rel_tol=0, preprocess_y=None) return minimize(min_func, min_solve)
Solves the non-linear equation f(x) = y by minimizing the norm of the residual.
This method is limited to backends that support
jacobian()
, currently PyTorch, TensorFlow and Jax.To obtain additional information about the performed solve, use a
SolveTape
.See Also:
minimize()
,solve_linear()
.Args
f
- Function whose output is optimized to match
y
. All positional arguments off
are optimized and must beTensor
orphiml.math.magic.PhiTreeNode
. The output off
must matchy
. y
- Desired output of
f(x)
asTensor
orphiml.math.magic.PhiTreeNode
. solve
Solve
object specifying optimization method, parameters and initial guess forx
.
Returns
x
- Solution fulfilling
f(x) = y
within specified tolerance asTensor
orphiml.math.magic.PhiTreeNode
.
Raises
NotConverged
- If the desired accuracy was not be reached within the maximum number of iterations.
Diverged
- If the solve failed prematurely.
def spatial_gradient(field: phi.field._field.Field,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
implicitness: int = None,
scheme=None,
upwind: phi.field._field.Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)-
Expand source code
def spatial_gradient(field: Field, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, implicitness: int = None, scheme=None, upwind: Field = None, gradient_extrapolation: Extrapolation = None): """ Finite difference spatial_gradient. This function can operate in two modes: * `type=CenteredGrid` approximates the spatial_gradient at cell centers using central differences * `type=StaggeredGrid` computes the spatial_gradient at face centers of neighbouring cells Args: field: centered grid of any number of dimensions (scalar field, vector field, tensor field) boundary: Boundary conditions of the gradient field. at: Either `'face'` or `'center'` dims: Along which dimensions to compute the spatial gradient. Only supported when `type==CenteredGrid`. stack_dim: Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions. The `field` must not have a dimension of the same name. order: Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit. implicit: When a `Solve` object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. implicitness: specifies the size of the implicit stencil in case an implicit treatment is used gradient_extrapolation: Alias for `boundary`. scheme: For unstructured meshes only. Currently only `'green-gauss'` is supported. upwind: For unstructured meshes only. Whether to use upwind interpolation. Returns: spatial_gradient field of type `type`. """ assert at in ['face', 'center'] stack_dim = auto(stack_dim) if gradient_extrapolation is not None: assert boundary is None, f"Cannot specify both boundary and gradient_extrapolation" boundary = gradient_extrapolation if boundary is None: boundary = field.extrapolation.spatial_gradient() if gradient_extrapolation is None: gradient_extrapolation = boundary if field.is_mesh and at == 'center': assert stack_dim not in field.shape, f"Gradient dimension is already part of field {field.shape}. Please use a different dimension" boundary = boundary or field.boundary.spatial_gradient() if scheme == 'green-gauss': return green_gauss_gradient(field, stack_dim=stack_dim, boundary=boundary, order=order, upwind=upwind) elif scheme == 'least-squares': return least_squares_gradient(field, stack_dim=stack_dim, boundary=boundary) raise NotImplementedError(scheme) if field.vector.exists: assert stack_dim.name != 'vector', "`stack_dim=vector` is inadmissible if the input is a vector grid" if field == StaggeredGrid: assert at == 'faces', "for a `StaggeredGrid` input only `type == StaggeredGrid` is possible" if at == 'faces': assert stack_dim.name == 'vector', f"spatial_gradient with type=StaggeredGrid requires stack_dim.name == 'vector' but got '{stack_dim.name}'" if gradient_extrapolation is None: gradient_extrapolation = field.extrapolation.spatial_gradient() if implicitness is None: implicitness = 0 if implicit is None else 2 elif implicitness != 0: assert implicit is not None, "for implicit treatment a `Solve` is required" grad_dims = field.shape.only(dims).names if stack_dim is None: assert len(grad_dims) == 1, "`stack_dim` `None` is only possible with single `grad_dim`" else: stack_dim = stack_dim.with_size(grad_dims) if order == 2: if at == 'center': values = math.spatial_gradient(field.values, field.dx.vector.as_channel(name=stack_dim.name), difference='central', padding=field.extrapolation, stack_dim=stack_dim) return CenteredGrid(values, bounds=field.bounds, extrapolation=gradient_extrapolation) elif at == 'face': assert stack_dim.name == 'vector' return stagger(field, lambda lower, upper: (upper - lower) / field.dx.vector.as_dual(), gradient_extrapolation) result_components = [ perform_finite_difference_operation(field.values, dim, 1, field.dx.vector[dim], field.extrapolation, gradient_extrapolation, at, order, implicit, implicitness) for dim in field.shape.only(grad_dims).names] if at == 'center': result = field.with_values(math.stack(result_components, stack_dim)) result = result.with_extrapolation(gradient_extrapolation) else: result = StaggeredGrid( math.stack(result_components, stack_dim.as_dual()), bounds=field.bounds, extrapolation=gradient_extrapolation) if at == 'center' and gradient_extrapolation == math.extrapolation.NONE: result = result.with_bounds(Box(field.bounds.lower - field.dx, field.bounds.upper + field.dx)) else: result = result.with_bounds(field.bounds) return result
Finite difference spatial_gradient.
This function can operate in two modes:
type=CenteredGrid
approximates the spatial_gradient at cell centers using central 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)
boundary
- Boundary conditions of the gradient field.
at
- Either
'face'
or'center'
dims
- Along which dimensions to compute the spatial gradient. Only supported when
type==CenteredGrid
. stack_dim
- Dimension to be added. This dimension lists the spatial_gradient w.r.t. the spatial dimensions.
The
field
must not have a dimension of the same name. order
- Spatial order of accuracy. Higher orders entail larger stencils and more computation time but result in more accurate results assuming a large enough resolution. Supported: 2 explicit, 4 explicit, 6 implicit.
implicit
- When a
Solve
object is passed, performs an implicit operation with the specified solver and tolerances. Otherwise, an explicit stencil is used. implicitness
- specifies the size of the implicit stencil in case an implicit treatment is used
gradient_extrapolation
- Alias for
boundary
. scheme
- For unstructured meshes only. Currently only
'green-gauss'
is supported. upwind
- For unstructured meshes only. Whether to use upwind interpolation.
Returns
spatial_gradient field of type
type
. def sqrt(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def sqrt(x: TensorOrTree) -> TensorOrTree: """ Computes *sqrt(x)* of the `Tensor` or `phiml.math.magic.PhiTreeNode` `x`. """ return _backend_op1(x, Backend.sqrt)
Computes sqrt(x) of the
Tensor
orphiml.math.magic.PhiTreeNode
x
. def stack(fields: Sequence[phi.field._field.Field],
dim: phiml.math._shape.Shape,
dim_bounds: phi.geom._box.Box = None)-
Expand source code
def stack(fields: Sequence[Field], dim: Shape, dim_bounds: Box = None): """ Stacks the given `Field`s along `dim`. See Also: `concat()`. Args: fields: List of matching `Field` instances. dim: Stack dimension as `Shape`. Size is ignored. dim_bounds: `Box` defining the physical size for `dim`. Returns: `Field` matching stacked fields. """ assert all(isinstance(f, Field) for f in fields), f"All fields must be Fields of the same type but got {fields}" assert all(isinstance(f, type(fields[0])) for f in fields), f"All fields must be Fields of the same type but got {fields}" if any([f.sampled_at != fields[0].sampled_at for f in fields]): return math.layout(fields, dim) if any(f.boundary != fields[0].boundary for f in fields): boundary = math.stack([f.boundary for f in fields], dim) else: boundary = fields[0].boundary if fields[0].is_grid: values = math.stack([f.values for f in fields], dim) if spatial(dim): if dim_bounds is None: dim_bounds = Box(**{dim.name: len(fields)}) return grid(values, boundary, fields[0].bounds * dim_bounds) else: return fields[0].with_values(values).with_boundary(boundary) else: values = math.stack([f.values for f in fields], dim) geometry = fields[0].geometry if all(f.geometry == fields[0].geometry for f in fields) else math.stack([f.geometry for f in fields], dim) return Field(geometry, values, boundary)
def stagger(field: phi.field._field.Field,
face_function: Callable,
boundary: float,
at='face',
dims=<function spatial>)-
Expand source code
def stagger(field: Field, face_function: Callable, boundary: float or math.extrapolation.Extrapolation, at='face', dims=spatial): """ Creates a new grid by evaluating `face_function` given two neighbouring cells. One layer of missing cells is inferred from the extrapolation. This method returns a Field of type `type` which must be either StaggeredGrid or CenteredGrid. When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells. When returning a CenteredGrid, the new grid has the same resolution as `field`. Args: field: Grid face_function: function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor boundary: extrapolation mode of the returned grid. Has no effect on the values. at: Where the result should be sampled, one of 'face', 'center' dims: Which dimensions to stagger. Defaults to all spatial axes. Returns: Grid sampled either at centers or faces depending on `at`. """ boundary = as_boundary(boundary, field.geometry) all_lower = [] all_upper = [] dims = field.shape.only(dims, reorder=True).names if at == 'face': for dim in dims: valid_lo, valid_up = boundary.valid_outer_faces(dim) if valid_lo and valid_up: width_lower, width_upper = {dim: (1, 0)}, {dim: (0, 1)} elif valid_lo and not valid_up: width_lower, width_upper = {dim: (1, -1)}, {dim: (0, 0)} elif not valid_lo and valid_up: width_lower, width_upper = {dim: (0, 0)}, {dim: (-1, 1)} else: width_lower, width_upper = {dim: (0, -1)}, {dim: (-1, 0)} comp = field.values.vector[dim] if 'vector' in channel(field) else field.values all_lower.append(math.pad(comp, width_lower, field.extrapolation, bounds=field.bounds)) all_upper.append(math.pad(comp, width_upper, field.extrapolation, bounds=field.bounds)) all_upper = math.stack(all_upper, dual(vector=dims)) all_lower = math.stack(all_lower, dual(vector=dims)) values = face_function(all_lower, all_upper) result = Field(field.geometry, values, boundary) assert result.resolution == field.resolution return result else: assert at == 'center', f"type must be 'face' or 'center' but got '{at}'" left, right = math.shift(field.values, (-1, 1), dims=dims, padding=field.extrapolation, stack_dim=channel('vector')) values = face_function(left, right) return CenteredGrid(values, boundary, field.bounds)
Creates a new grid by evaluating
face_function
given two neighbouring cells. One layer of missing cells is inferred from the extrapolation.This method returns a Field of type
type
which must be either StaggeredGrid or CenteredGrid. When returning a StaggeredGrid, the new values are sampled at the faces of neighbouring cells. When returning a CenteredGrid, the new grid has the same resolution asfield
.Args
field
- Grid
face_function
- function mapping (value1: Tensor, value2: Tensor) -> center_value: Tensor
boundary
- extrapolation mode of the returned grid. Has no effect on the values.
at
- Where the result should be sampled, one of 'face', 'center'
dims
- Which dimensions to stagger. Defaults to all spatial axes.
Returns
Grid sampled either at centers or faces depending on
at
. def stop_gradient(x)
-
Expand source code
def stop_gradient(x): """ Disables gradients for the given tensor. This may switch off the gradients for `x` itself or create a copy of `x` with disabled gradients. Implementations: * PyTorch: [`x.detach()`](https://pytorch.org/docs/stable/autograd.html#torch.Tensor.detach) * TensorFlow: [`tf.stop_gradient`](https://www.tensorflow.org/api_docs/python/tf/stop_gradient) * Jax: [`jax.lax.stop_gradient`](https://jax.readthedocs.io/en/latest/_autosummary/jax.lax.stop_gradient.html) Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` for which gradients should be disabled. Returns: Copy of `x`. """ if isinstance(x, Shape): return x return _backend_op1(x, Backend.stop_gradient, attr_type=variable_attributes)
Disables gradients for the given tensor. This may switch off the gradients for
x
itself or create a copy ofx
with disabled gradients.Implementations:
- PyTorch:
x.detach()
- TensorFlow:
tf.stop_gradient
- Jax:
jax.lax.stop_gradient
Args
x
Tensor
orphiml.math.magic.PhiTreeNode
for which gradients should be disabled.
Returns
Copy of
x
. - PyTorch:
def support(field: phi.field._field.Field,
list_dim: phiml.math._shape.Shape = (nonzeroⁱ=None)) ‑> phiml.math._tensors.Tensor-
Expand source code
def support(field: Field, list_dim: Shape or str = instance('nonzero')) -> Tensor: """ Returns the points at which the field values are non-zero. Args: field: `Field` list_dim: Dimension to list the non-zero values. Returns: `Tensor` with shape `(list_dim, vector)` """ return field.points[math.nonzero(field.values, list_dim=list_dim)]
Returns the points at which the field values are non-zero.
Args
field
Field
list_dim
- Dimension to list the non-zero values.
Returns
Tensor
with shape(list_dim, vector)
def to_float(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def to_float(x: TensorOrTree) -> TensorOrTree: """ Converts the given tensor to floating point format with the currently specified precision. The precision can be set globally using `math.set_global_precision()` and locally using `with math.precision()`. See the documentation at https://tum-pbs.github.io/PhiML/Data_Types.html See Also: `cast()`. Args: x: `Tensor` or `phiml.math.magic.PhiTreeNode` to convert Returns: `Tensor` or `phiml.math.magic.PhiTreeNode` matching `x`. """ return _backend_op1(x, Backend.to_float)
Converts the given tensor to floating point format with the currently specified precision.
The precision can be set globally using
math.set_global_precision()
and locally usingwith math.precision()
.See the documentation at https://tum-pbs.github.io/PhiML/Data_Types.html
See Also:
cast()
.Args
x
Tensor
orphiml.math.magic.PhiTreeNode
to convert
Returns
Tensor
orphiml.math.magic.PhiTreeNode
matchingx
. def to_int32(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def to_int32(x: TensorOrTree) -> TensorOrTree: """ Converts the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to 32-bit integer. """ return _backend_op1(x, Backend.to_int32)
Converts the
Tensor
orphiml.math.magic.PhiTreeNode
x
to 32-bit integer. def to_int64(x: ~TensorOrTree) ‑> ~TensorOrTree
-
Expand source code
def to_int64(x: TensorOrTree) -> TensorOrTree: """ Converts the `Tensor` or `phiml.math.magic.PhiTreeNode` `x` to 64-bit integer. """ return _backend_op1(x, Backend.to_int64)
Converts the
Tensor
orphiml.math.magic.PhiTreeNode
x
to 64-bit integer. def unstack(value, dim: str | Sequence | set | ForwardRef('Shape') | Callable | None) ‑> tuple
-
Expand source code
def unstack(value, dim: DimFilter) -> tuple: """ Un-stacks a `Sliceable` along one or multiple dimensions. If multiple dimensions are given, the order of elements will be according to the dimension order in `dim`, i.e. elements along the last dimension will be neighbors in the returned `tuple`. If no dimension is given or none of the given dimensions exists on `value`, returns a list containing only `value`. See Also: `phiml.math.slice`. Args: value: `phiml.math.magic.Shapable`, such as `phiml.math.Tensor` dim: Dimensions as `Shape` or comma-separated `str` or dimension type, i.e. `channel`, `spatial`, `instance`, `batch`. Returns: `tuple` of objects matching the type of `value`. Examples: >>> unstack(expand(0, spatial(x=5)), 'x') (0.0, 0.0, 0.0, 0.0, 0.0) """ assert isinstance(value, Sliceable) and isinstance(value, Shaped), f"Cannot unstack {type(value).__name__}. Must be Sliceable and Shaped, see https://tum-pbs.github.io/PhiML/phiml/math/magic.html" dims = shape(value).only(dim) if dims.rank == 0: return value, if dims.rank == 1: if hasattr(value, '__unstack__'): result = value.__unstack__(dims.names) if result is not NotImplemented: assert isinstance(result, tuple), f"__unstack__ must return a tuple but got {type(result)}" assert all([isinstance(item, Sliceable) for item in result]), f"__unstack__ must return a tuple of Sliceable objects but not all items were sliceable in {result}" return result return tuple([slice_(value, {dims.name: i}) for i in range(dims.size)]) else: # multiple dimensions if hasattr(value, '__pack_dims__'): packed_dim = batch('_unstack') value_packed = value.__pack_dims__(dims.names, packed_dim, pos=None) if value_packed is not NotImplemented: return unstack(value_packed, packed_dim) unstack_dim = _any_uniform_dim(dims) first_unstacked = unstack(value, unstack_dim) inner_unstacked = [unstack(v, dims.without(unstack_dim)) for v in first_unstacked] return sum(inner_unstacked, ())
Un-stacks a
Sliceable
along one or multiple dimensions.If multiple dimensions are given, the order of elements will be according to the dimension order in
dim
, i.e. elements along the last dimension will be neighbors in the returnedtuple
. If no dimension is given or none of the given dimensions exists onvalue
, returns a list containing onlyvalue
.See Also:
phiml.math.slice
.Args
value
phiml.math.magic.Shapable
, such asphiml.math.Tensor
dim
- Dimensions as
Shape
or comma-separatedstr
or dimension type, i.e.channel
,spatial
,instance
,batch
.
Returns
tuple
of objects matching the type ofvalue
.Examples
>>> unstack(expand(0, spatial(x=5)), 'x') (0.0, 0.0, 0.0, 0.0, 0.0)
def upsample2x(grid: phi.field._field.Field) ‑> phi.field._field.Field
-
Expand source code
def upsample2x(grid: Field) -> Field: """ Increases the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation. See Also: `downsample2x()`. Args: grid: `CenteredGrid` or `StaggeredGrid`. Returns: `Grid` of same type as `grid`. """ assert grid.is_grid, f"upsample2x only supported for grids but got {grid}" if grid.is_centered: values = math.upsample2x(grid.values, grid.extrapolation) return CenteredGrid(values, bounds=grid.bounds, extrapolation=grid.extrapolation) elif grid.is_staggered: raise NotImplementedError() else: raise ValueError(type(grid))
Increases the number of sample points by a factor of 2 in each spatial dimension. The new values are determined via linear interpolation.
See Also:
downsample2x()
.Args
grid
CenteredGrid()
orStaggeredGrid()
.
Returns
Field
of same type asgrid
. def vec_abs(field: phi.field._field.Field)
-
Expand source code
def vec_length(field: Field): """ See `phi.math.vec_abs()` """ assert isinstance(field, Field), f"Field required but got {type(field).__name__}" if field.is_grid and field.is_staggered: field = field.at_centers() return field.with_values(math.vec_abs(field.values))
See
phi.math.vec_abs()
def vec_length(field: phi.field._field.Field)
-
Expand source code
def vec_length(field: Field): """ See `phi.math.vec_abs()` """ assert isinstance(field, Field), f"Field required but got {type(field).__name__}" if field.is_grid and field.is_staggered: field = field.at_centers() return field.with_values(math.vec_abs(field.values))
See
phi.math.vec_abs()
def vec_squared(field: phi.field._field.Field)
-
Expand source code
def vec_squared(field: Field): """ See `phi.math.vec_squared()` """ if field.is_grid and field.is_staggered: field = field.at_centers() return field.with_values(math.vec_squared(field.values))
See
phi.math.vec_squared()
def where(mask: phi.field._field.Field,
field_true: phi.field._field.Field,
field_false: phi.field._field.Field) ‑> phi.field._field.Field-
Expand source code
def where(mask: Field or Geometry or float, field_true: Field or float, field_false: Field or float) -> Field: """ Element-wise where operation. Picks the value of `field_true` where `mask=1 / True` and the value of `field_false` where `mask=0 / False`. The fields are automatically resampled if necessary, preferring the sample points of `mask`. At least one of the arguments must be a `Field`. Args: mask: `Field` or `Geometry` object. field_true: `Field` field_false: `Field` Returns: `Field` """ field_true, field_false, mask = _auto_resample(field_true, field_false, mask) values = math.where(mask.values, field_true.values, field_false.values) return field_true.with_values(values)
def write(field: phi.field._field.Field, file: str | phiml.math._tensors.Tensor)
-
Expand source code
def write(field: Field, file: Union[str, Tensor]): """ Writes a field to disc using a NumPy file format. Depending on `file`, the data may be split up into multiple files. All characteristics of the field are serialized so that it can be fully restored using `read()`. See Also: `read()` Args: field: Field to be saved. file: Single file as `str` or `Tensor` of string type. If `file` is a tensor, the dimensions of `field` are matched to the dimensions of `file`. Dimensions of `file` that are missing in `field` result in data duplication. Dimensions of `field` that are missing in `file` result in larger files. """ if isinstance(file, str): write_single_field(field, file) elif isinstance(file, Tensor): if file.rank == 0: write_single_field(field, file.native()) else: dim = file.shape.names[0] files = math.unstack(file, dim) fields = field.dimension(dim).unstack(file.shape.get_size(dim)) for field_, file_ in zip(fields, files): write(field_, file_) else: raise ValueError(file)
Writes a field to disc using a NumPy file format. Depending on
file
, the data may be split up into multiple files.All characteristics of the field are serialized so that it can be fully restored using
read()
.See Also:
read()
Args
field
- Field to be saved.
file
- Single file as
str
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.
Classes
class AngularVelocity (location: phiml.math._tensors.Tensor | tuple | list | numbers.Number,
strength: phiml.math._tensors.Tensor | numbers.Number = 1.0,
falloff: Callable = None)-
Expand source code
class AngularVelocity(FieldInitializer): """ Model of a single vortex or set of vortices. The falloff of the velocity magnitude can be controlled. Without a specified falloff, the velocity increases linearly with the distance from the vortex center. This is the case with rotating rigid bodies, for example. """ def __init__(self, location: Union[Tensor, tuple, list, Number], strength: Union[Tensor, Number] = 1.0, falloff: Callable = None): location = wrap(location) strength = wrap(strength) assert location.shape.channel.names == ('vector',), "location must have a single channel dimension called 'vector'" assert location.shape.spatial.is_empty, "location tensor cannot have any spatial dimensions" assert not instance(location), "AngularVelocity does not support instance dimensions" self.location = location self.strength = strength self.falloff = falloff spatial_names = location.vector.item_names assert spatial_names is not None, "location.vector must list spatial dimensions as item names" self._shape = location.shape & spatial(**{dim: 1 for dim in spatial_names}) def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor: points = get_sample_points(geometry, at, boundaries) distances = points - self.location strength = self.strength if self.falloff is None else self.strength * self.falloff(distances) velocity = cross(strength, distances) velocity = math.sum(velocity, self.location.shape.batch.without(points.shape)) return velocity
Model of a single vortex or set of vortices. The falloff of the velocity magnitude can be controlled.
Without a specified falloff, the velocity increases linearly with the distance from the vortex center. This is the case with rotating rigid bodies, for example.
Ancestors
- phi.field._field.FieldInitializer
class Field (geometry: phi.geom._geom.Geometry,
values: phiml.math._tensors.Tensor,
boundary: phiml.math.extrapolation.Extrapolation = 0.0,
variable_attrs: Tuple[str, ...] = ('values',),
value_attrs: Tuple[str, ...] = ('values',))-
Expand source code
@dataclass(frozen=True) class Field(metaclass=_FieldType): """ A `Field` represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`. The boundary conditions and values outside the geometry are determined by `boundary`. Examples: Create a periodic 2D grid, initialized via noise fluctuations. >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC) Create a field on an unstructured mesh loaded from a .gmsh file >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0}) Create two cubes and compute a scalar values for each. >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x) See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ geometry: Geometry """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """ values: Tensor """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`.""" boundary: Extrapolation = 0. """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """ variable_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`""" value_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`""" def __post_init__(self): math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel) # shape check @property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry @property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def faces(self): return get_faces(self.geometry, self.boundary) @property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary) @property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary) @property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary) @property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry @property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry @property def is_centered(self): return not self.is_staggered @property def is_staggered(self): return is_staggered(self.values, self.geometry) @property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation) @property def points(self): return self.center @property def data(self) -> Tensor: return self.values def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)] def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor() @property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary @property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values @property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch @property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank @property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper) box = bounds @property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid) @property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh) @property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph) @property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True @property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution @property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary) def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary) def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary) def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary) def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary) @property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1] def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs) def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs) def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector']) def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary) def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary) with_extrapolation = with_boundary def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary) def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary) with_elements = with_geometry def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta)) def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position)) def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths) def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation) def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind) def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at) def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew) def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.") def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result @staticmethod def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Field': from ._field_math import stack return stack(values, dim, kwargs.get('bounds', None)) @staticmethod def __concat__(values: tuple, dim: str, **kwargs) -> 'Field': from ._field_math import concat return concat(values, dim) def __and__(self, other): assert isinstance(other, Field) assert instance(self).rank == instance(other).rank == 1, f"Can only use & on PointClouds that have a single instance dimension but got shapes {self.shape} & {other.shape}" from ._field_math import concat return concat([self, other], instance(self)) def __matmul__(self, other: 'Field'): # value @ representation # Deprecated. Use `resample(value, field)` instead. warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(self, to=other, keep_boundary=False) def __rmatmul__(self, other): # values @ representation if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)): warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __rshift__(self, other): if isinstance(other, (Field, Geometry)): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) return self.at(other, keep_boundary=False) else: return NotImplemented def __rrshift__(self, other): return self.with_values(other) def __lshift__(self, other): return self.with_values(other) def __rrshift__(self, other): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) if not isinstance(self, Field): return NotImplemented if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)): from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __getitem__(self, item) -> 'Field': """ Access a slice of the Field. The returned `Field` may be of a different type than `self`. Args: item: `dict` mapping dimensions (`str`) to selections (`int` or `slice`) or other supported type, such as `int` or `str`. Returns: Sliced `Field`. """ item = slicing_dict(self, item) if not item: return self boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements)) item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'} geometry = self.geometry[item_without_vec] if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape: assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids" dims = item['vector'] dims_ = self.geometry.shape['vector'].after_gather({'vector': dims}) dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]] proj_dims = set(self.resolution.names) - set(dims) if any(dim not in item for dim in proj_dims): # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}") item['~vector'] = item['vector'] del item['vector'] geometry = self.sampled_elements[item] else: item['~vector'] = dims del item['vector'] values = self.values[item] return Field(geometry, values, boundary) def __getattr__(self, name: str) -> BoundDim: return BoundDim(self, name) def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name) def __expand__(self, dims: Shape, **kwargs) -> 'Field': return self.with_values(expand(self.values, dims, **kwargs)) def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'Field': elements = math.rename_dims(self.geometry, dims, new_dims) values = math.rename_dims(self.values, dims, new_dims) extrapolation = math.rename_dims(self.boundary, dims, new_dims, **kwargs) return Field(elements, values, extrapolation) def __eq__(self, other): if not isinstance(other, Field): return False if self.geometry != other.geometry: return False if self.boundary != other.boundary: return False return math.always_close(self.values, other.values) def __hash__(self): return hash((self.geometry, self.boundary)) def __mul__(self, other): return self._op2(other, lambda d1, d2: d1 * d2) __rmul__ = __mul__ def __truediv__(self, other): return self._op2(other, lambda d1, d2: d1 / d2) def __rtruediv__(self, other): return self._op2(other, lambda d1, d2: d2 / d1) def __sub__(self, other): return self._op2(other, lambda d1, d2: d1 - d2) def __rsub__(self, other): return self._op2(other, lambda d1, d2: d2 - d1) def __add__(self, other): return self._op2(other, lambda d1, d2: d1 + d2) __radd__ = __add__ def __pow__(self, power, modulo=None): return self._op2(power, lambda f, p: f ** p) def __neg__(self): return self._op1(lambda x: -x) def __gt__(self, other): return self._op2(other, lambda x, y: x > y) def __ge__(self, other): return self._op2(other, lambda x, y: x >= y) def __lt__(self, other): return self._op2(other, lambda x, y: x < y) def __le__(self, other): return self._op2(other, lambda x, y: x <= y) def __abs__(self): return self._op1(lambda x: abs(x)) def _op1(self: 'Field', operator: Callable) -> 'Field': """ Perform an operation on the data of this field. Args: operator: function that accepts tensors and extrapolations and returns objects of the same type and dimensions Returns: Field of same type """ values = operator(self.values) extrapolation_ = operator(self.boundary) return self.with_values(values).with_extrapolation(extrapolation_) def _op2(self, other, operator) -> 'Field': if isinstance(other, Geometry): raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}") if isinstance(other, Field): if self.geometry == other.geometry: values = operator(self.values, other.values) extrapolation_ = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, extrapolation_) from ._resample import sample other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry) values = operator(self.values, other_values) boundary = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, boundary) else: if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank: other = math.wrap(other, self.geometry.shape['vector']) else: other = math.wrap(other) # try: # boundary = operator(self.boundary, as_boundary(other, self.geometry)) # except TypeError: # e.g. ZERO_GRADIENT + constant boundary = self.boundary # constants don't affect the boundary conditions (legacy reasons) if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape: other = other.vector.as_dual() values = operator(self.values, other) return Field(self.geometry, values, boundary) def __repr__(self): if self.is_grid: type_name = "Grid" if self.is_centered else "Grid faces" elif self.is_mesh: type_name = "Mesh" if self.is_centered else "Mesh faces" elif self.is_point_cloud: type_name = "Point cloud" if self.is_centered else "Point cloud edges" elif self.is_graph: type_name = "Graph" if self.is_centered else "Graph edges" else: type_name = self.__class__.__name__ if self.values is not None: return f"{type_name}[{self.values}, ext={self.boundary}]" else: return f"{type_name}[{self.resolution}, ext={self.boundary}]" def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs) def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
A
Field
represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in thegeometry
property and the corresponding values are stored as oneTensor
invalues
. The boundary conditions and values outside the geometry are determined byboundary
.Examples
Create a periodic 2D grid, initialized via noise fluctuations.
>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)
Create a field on an unstructured mesh loaded from a .gmsh file
>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})
Create two cubes and compute a scalar values for each.
>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)
See the
phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlClass variables
var boundary : phiml.math.extrapolation.Extrapolation
-
Boundary conditions describe the values outside of
geometry
and are used by numerical solvers to compute edge values. var geometry : phi.geom._geom.Geometry
-
Discretization
Geometry
. This determines where in space thevalues
are sampled as well as their relationship and interpretation. var value_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) are considered values. See
phiml.math.magic.PhiTreeNode.__value_attrs__
var values : phiml.math._tensors.Tensor
-
The sampled values, matching some point set of
geometry
, e.g. center points, seeGeometry.sets
. var variable_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) should be traced / optimized. See
phiml.math.magic.PhiTreeNode.__variable_attrs__
Instance variables
prop bounds : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop box : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop cells
-
Expand source code
@property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry
prop center : phiml.math._tensors.Tensor
-
Expand source code
@property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation)
Returns the center points of the
elements
of thisField
. prop data : phiml.math._tensors.Tensor
-
Expand source code
@property def data(self) -> Tensor: return self.values
prop dx : phiml.math._tensors.Tensor
-
Expand source code
@property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution
prop elements
-
Expand source code
@property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
-
Expand source code
@property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary
Returns the
Extrapolation
of thisField
. prop face_areas
-
Expand source code
@property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
-
Expand source code
@property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
-
Expand source code
@property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
-
Expand source code
@property def faces(self): return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
-
Expand source code
@property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aGraph
. prop grid : phi.geom._grid.UniformGrid
-
Expand source code
@property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aUniformGrid
. prop is_centered
-
Expand source code
@property def is_centered(self): return not self.is_staggered
prop is_graph
-
Expand source code
@property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph)
A Field represents graph data if its
geometry
is aGraph
instance. prop is_grid
-
Expand source code
@property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid)
A Field represents grid data if its
geometry
is aUniformGrid
instance. prop is_mesh
-
Expand source code
@property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh)
A Field represents mesh data if its
geometry
is aMesh
instance. prop is_point_cloud
-
Expand source code
@property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True
A Field represents graph data if its
geometry
is not a set of connected elements, but rather individual geometric objects. prop is_staggered
-
Expand source code
@property def is_staggered(self): return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
-
Expand source code
@property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aMesh
. prop points
-
Expand source code
@property def points(self): return self.center
prop resolution
-
Expand source code
@property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
-
Expand source code
@property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
-
Expand source code
@property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry
If the values represent are sampled at the element centers or represent the whole element, returns
self.geometry
. If the values are sampled at the faces, returnsself.faces
. prop shape : phiml.math._shape.Shape
-
Expand source code
@property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
prop spatial_rank : int
-
Expand source code
@property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank
Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the
data
.
Methods
def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
-
Expand source code
def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
Returns an
Extrapolation
representing this 'Field''s values as a Dirichlet (constant) boundary. If thisField
encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of thisField
's sampled domain are required, thisField
's boundary conditions will be applied to determine the boundary values.Returns
Extrapolation
def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary)
Returns this field as a PointCloud. This replaces the
Field.geometry
with aPoint
instance while leaving the sample points unchanged.See Also:
Field.as_spheres()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butPoint
geometry. def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary)
Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the
Field.geometry
with aSphere
instance while leaving the sample points unchanged.See Also:
Field.as_points()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butSphere
geometry. def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field-
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs)
Short for
resample()(self, representation)
See Also
resample()
.Returns
Field object of same type as
representation
def at_centers(self, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary)
Interpolates the values to the cell centers.
See Also:
Field.at_faces()
,Field.at()
,resample()
.Args
**kwargs
- Sampling arguments.
Returns
CenteredGrid()
sampled at cell centers. def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
-
Expand source code
def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector'])
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension 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. def curl(self, at='corner')
-
Expand source code
def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at)
Alias for
curl()
def dimension(self, name: str)
-
Expand source code
def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name)
Returns a reference to one of the dimensions of this field.
The dimension reference can be used the same way as a
Tensor
dimension reference. Notable properties and methods of a dimension reference are: indexing using[index]
,unstack()
,size
,exists
,is_batch
,is_spatial
,is_channel
.A shortcut to calling this function is the syntax
field.<dim_name>
which callsfield.dimension(<dim_name>)
.Args
name
- dimension name
Returns
dimension reference
def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)-
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind)
Alias for
divergence()
def downsample(self, factor: int)
-
Expand source code
def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)-
Expand source code
def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
Alias for
spatial_gradient()
def grid_scatter(self, *args, **kwargs)
-
Expand source code
def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs)
Deprecated. Use
sample()
withscatter=True
instead. def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)-
Expand source code
def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)
Alias for
laplace()
def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)-
Expand source code
def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]
Return the field values as
NumPy
array(s).Args
order
- Dimension order as
str
orShape
.
Returns
A single NumPy array for uniform values, else a list of NumPy arrays.
def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
-
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths)
Alias for
pad()
.Pads this
Field
using its extrapolation.Unlike padding the values, this function also affects the
geometry
of the field, changing its size and origin depending onwidths
.Args
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
Padded
Field
def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor-
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs)
Sample the values of this
Field
at the given location or geometry.Args
where
- Location
Tensor
orGeometry
or at
'center'
or'face'
.**kwargs
- Sampling arguments.
Returns
Tensor
def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta))
Move the positions of this field's
geometry
bydelta
.See Also:
Field.shifted_to()
.Args
delta
- Shift amount for each center position of
geometry
.
Returns
New
Field
sampled atgeometry.center + delta
. def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position))
Move the positions of this field's
geometry
topositions
.See Also:
Field.shifted()
.Args
position
- New center positions of
geometry
.
Returns
New
Field
sampled at given positions. def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
-
Expand source code
def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result
Stacks all component grids into a single uniform
phi.math.Tensor
. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the gridresolution
in every spatial dimension.Returns
Uniform
phi.math.Tensor
. def to_grid(self, resolution=(), bounds=None, **resolution_)
-
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
-
Expand source code
def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor()
Returns a uniform tensor containing
values
.For periodic grids, which always have a uniform value tensor, `values' is returned directly. If
values
is not uniform, it is padded as inStaggeredGrid.staggered_tensor()
. def with_boundary(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_bounds(self, bounds: phi.geom._box.Box)
-
Expand source code
def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary)
Returns a copy of this field with
bounds
replaced. def with_elements(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_extrapolation(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_geometry(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_values(self, values, **sampling_kwargs)
-
Expand source code
def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary)
Returns a copy of this field with
values
replaced.
class SampledField (geometry: phi.geom._geom.Geometry,
values: phiml.math._tensors.Tensor,
boundary: phiml.math.extrapolation.Extrapolation = 0.0,
variable_attrs: Tuple[str, ...] = ('values',),
value_attrs: Tuple[str, ...] = ('values',))-
Expand source code
@dataclass(frozen=True) class Field(metaclass=_FieldType): """ A `Field` represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`. The boundary conditions and values outside the geometry are determined by `boundary`. Examples: Create a periodic 2D grid, initialized via noise fluctuations. >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC) Create a field on an unstructured mesh loaded from a .gmsh file >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0}) Create two cubes and compute a scalar values for each. >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x) See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ geometry: Geometry """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """ values: Tensor """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`.""" boundary: Extrapolation = 0. """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """ variable_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`""" value_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`""" def __post_init__(self): math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel) # shape check @property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry @property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def faces(self): return get_faces(self.geometry, self.boundary) @property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary) @property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary) @property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary) @property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry @property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry @property def is_centered(self): return not self.is_staggered @property def is_staggered(self): return is_staggered(self.values, self.geometry) @property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation) @property def points(self): return self.center @property def data(self) -> Tensor: return self.values def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)] def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor() @property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary @property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values @property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch @property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank @property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper) box = bounds @property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid) @property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh) @property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph) @property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True @property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution @property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary) def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary) def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary) def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary) def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary) @property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1] def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs) def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs) def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector']) def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary) def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary) with_extrapolation = with_boundary def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary) def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary) with_elements = with_geometry def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta)) def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position)) def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths) def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation) def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind) def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at) def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew) def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.") def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result @staticmethod def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Field': from ._field_math import stack return stack(values, dim, kwargs.get('bounds', None)) @staticmethod def __concat__(values: tuple, dim: str, **kwargs) -> 'Field': from ._field_math import concat return concat(values, dim) def __and__(self, other): assert isinstance(other, Field) assert instance(self).rank == instance(other).rank == 1, f"Can only use & on PointClouds that have a single instance dimension but got shapes {self.shape} & {other.shape}" from ._field_math import concat return concat([self, other], instance(self)) def __matmul__(self, other: 'Field'): # value @ representation # Deprecated. Use `resample(value, field)` instead. warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(self, to=other, keep_boundary=False) def __rmatmul__(self, other): # values @ representation if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)): warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __rshift__(self, other): if isinstance(other, (Field, Geometry)): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) return self.at(other, keep_boundary=False) else: return NotImplemented def __rrshift__(self, other): return self.with_values(other) def __lshift__(self, other): return self.with_values(other) def __rrshift__(self, other): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) if not isinstance(self, Field): return NotImplemented if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)): from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __getitem__(self, item) -> 'Field': """ Access a slice of the Field. The returned `Field` may be of a different type than `self`. Args: item: `dict` mapping dimensions (`str`) to selections (`int` or `slice`) or other supported type, such as `int` or `str`. Returns: Sliced `Field`. """ item = slicing_dict(self, item) if not item: return self boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements)) item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'} geometry = self.geometry[item_without_vec] if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape: assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids" dims = item['vector'] dims_ = self.geometry.shape['vector'].after_gather({'vector': dims}) dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]] proj_dims = set(self.resolution.names) - set(dims) if any(dim not in item for dim in proj_dims): # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}") item['~vector'] = item['vector'] del item['vector'] geometry = self.sampled_elements[item] else: item['~vector'] = dims del item['vector'] values = self.values[item] return Field(geometry, values, boundary) def __getattr__(self, name: str) -> BoundDim: return BoundDim(self, name) def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name) def __expand__(self, dims: Shape, **kwargs) -> 'Field': return self.with_values(expand(self.values, dims, **kwargs)) def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'Field': elements = math.rename_dims(self.geometry, dims, new_dims) values = math.rename_dims(self.values, dims, new_dims) extrapolation = math.rename_dims(self.boundary, dims, new_dims, **kwargs) return Field(elements, values, extrapolation) def __eq__(self, other): if not isinstance(other, Field): return False if self.geometry != other.geometry: return False if self.boundary != other.boundary: return False return math.always_close(self.values, other.values) def __hash__(self): return hash((self.geometry, self.boundary)) def __mul__(self, other): return self._op2(other, lambda d1, d2: d1 * d2) __rmul__ = __mul__ def __truediv__(self, other): return self._op2(other, lambda d1, d2: d1 / d2) def __rtruediv__(self, other): return self._op2(other, lambda d1, d2: d2 / d1) def __sub__(self, other): return self._op2(other, lambda d1, d2: d1 - d2) def __rsub__(self, other): return self._op2(other, lambda d1, d2: d2 - d1) def __add__(self, other): return self._op2(other, lambda d1, d2: d1 + d2) __radd__ = __add__ def __pow__(self, power, modulo=None): return self._op2(power, lambda f, p: f ** p) def __neg__(self): return self._op1(lambda x: -x) def __gt__(self, other): return self._op2(other, lambda x, y: x > y) def __ge__(self, other): return self._op2(other, lambda x, y: x >= y) def __lt__(self, other): return self._op2(other, lambda x, y: x < y) def __le__(self, other): return self._op2(other, lambda x, y: x <= y) def __abs__(self): return self._op1(lambda x: abs(x)) def _op1(self: 'Field', operator: Callable) -> 'Field': """ Perform an operation on the data of this field. Args: operator: function that accepts tensors and extrapolations and returns objects of the same type and dimensions Returns: Field of same type """ values = operator(self.values) extrapolation_ = operator(self.boundary) return self.with_values(values).with_extrapolation(extrapolation_) def _op2(self, other, operator) -> 'Field': if isinstance(other, Geometry): raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}") if isinstance(other, Field): if self.geometry == other.geometry: values = operator(self.values, other.values) extrapolation_ = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, extrapolation_) from ._resample import sample other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry) values = operator(self.values, other_values) boundary = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, boundary) else: if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank: other = math.wrap(other, self.geometry.shape['vector']) else: other = math.wrap(other) # try: # boundary = operator(self.boundary, as_boundary(other, self.geometry)) # except TypeError: # e.g. ZERO_GRADIENT + constant boundary = self.boundary # constants don't affect the boundary conditions (legacy reasons) if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape: other = other.vector.as_dual() values = operator(self.values, other) return Field(self.geometry, values, boundary) def __repr__(self): if self.is_grid: type_name = "Grid" if self.is_centered else "Grid faces" elif self.is_mesh: type_name = "Mesh" if self.is_centered else "Mesh faces" elif self.is_point_cloud: type_name = "Point cloud" if self.is_centered else "Point cloud edges" elif self.is_graph: type_name = "Graph" if self.is_centered else "Graph edges" else: type_name = self.__class__.__name__ if self.values is not None: return f"{type_name}[{self.values}, ext={self.boundary}]" else: return f"{type_name}[{self.resolution}, ext={self.boundary}]" def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs) def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
A
Field
represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in thegeometry
property and the corresponding values are stored as oneTensor
invalues
. The boundary conditions and values outside the geometry are determined byboundary
.Examples
Create a periodic 2D grid, initialized via noise fluctuations.
>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)
Create a field on an unstructured mesh loaded from a .gmsh file
>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})
Create two cubes and compute a scalar values for each.
>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)
See the
phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlClass variables
var boundary : phiml.math.extrapolation.Extrapolation
-
Boundary conditions describe the values outside of
geometry
and are used by numerical solvers to compute edge values. var geometry : phi.geom._geom.Geometry
-
Discretization
Geometry
. This determines where in space thevalues
are sampled as well as their relationship and interpretation. var value_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) are considered values. See
phiml.math.magic.PhiTreeNode.__value_attrs__
var values : phiml.math._tensors.Tensor
-
The sampled values, matching some point set of
geometry
, e.g. center points, seeGeometry.sets
. var variable_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) should be traced / optimized. See
phiml.math.magic.PhiTreeNode.__variable_attrs__
Instance variables
prop bounds : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop box : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop cells
-
Expand source code
@property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry
prop center : phiml.math._tensors.Tensor
-
Expand source code
@property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation)
Returns the center points of the
elements
of thisField
. prop data : phiml.math._tensors.Tensor
-
Expand source code
@property def data(self) -> Tensor: return self.values
prop dx : phiml.math._tensors.Tensor
-
Expand source code
@property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution
prop elements
-
Expand source code
@property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
-
Expand source code
@property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary
Returns the
Extrapolation
of thisField
. prop face_areas
-
Expand source code
@property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
-
Expand source code
@property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
-
Expand source code
@property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
-
Expand source code
@property def faces(self): return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
-
Expand source code
@property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aGraph
. prop grid : phi.geom._grid.UniformGrid
-
Expand source code
@property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aUniformGrid
. prop is_centered
-
Expand source code
@property def is_centered(self): return not self.is_staggered
prop is_graph
-
Expand source code
@property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph)
A Field represents graph data if its
geometry
is aGraph
instance. prop is_grid
-
Expand source code
@property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid)
A Field represents grid data if its
geometry
is aUniformGrid
instance. prop is_mesh
-
Expand source code
@property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh)
A Field represents mesh data if its
geometry
is aMesh
instance. prop is_point_cloud
-
Expand source code
@property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True
A Field represents graph data if its
geometry
is not a set of connected elements, but rather individual geometric objects. prop is_staggered
-
Expand source code
@property def is_staggered(self): return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
-
Expand source code
@property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aMesh
. prop points
-
Expand source code
@property def points(self): return self.center
prop resolution
-
Expand source code
@property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
-
Expand source code
@property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
-
Expand source code
@property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry
If the values represent are sampled at the element centers or represent the whole element, returns
self.geometry
. If the values are sampled at the faces, returnsself.faces
. prop shape : phiml.math._shape.Shape
-
Expand source code
@property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
prop spatial_rank : int
-
Expand source code
@property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank
Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the
data
.
Methods
def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
-
Expand source code
def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
Returns an
Extrapolation
representing this 'Field''s values as a Dirichlet (constant) boundary. If thisField
encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of thisField
's sampled domain are required, thisField
's boundary conditions will be applied to determine the boundary values.Returns
Extrapolation
def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary)
Returns this field as a PointCloud. This replaces the
Field.geometry
with aPoint
instance while leaving the sample points unchanged.See Also:
Field.as_spheres()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butPoint
geometry. def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary)
Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the
Field.geometry
with aSphere
instance while leaving the sample points unchanged.See Also:
Field.as_points()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butSphere
geometry. def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field-
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs)
Short for
resample()(self, representation)
See Also
resample()
.Returns
Field object of same type as
representation
def at_centers(self, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary)
Interpolates the values to the cell centers.
See Also:
Field.at_faces()
,Field.at()
,resample()
.Args
**kwargs
- Sampling arguments.
Returns
CenteredGrid()
sampled at cell centers. def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
-
Expand source code
def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector'])
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension 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. def curl(self, at='corner')
-
Expand source code
def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at)
Alias for
curl()
def dimension(self, name: str)
-
Expand source code
def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name)
Returns a reference to one of the dimensions of this field.
The dimension reference can be used the same way as a
Tensor
dimension reference. Notable properties and methods of a dimension reference are: indexing using[index]
,unstack()
,size
,exists
,is_batch
,is_spatial
,is_channel
.A shortcut to calling this function is the syntax
field.<dim_name>
which callsfield.dimension(<dim_name>)
.Args
name
- dimension name
Returns
dimension reference
def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)-
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind)
Alias for
divergence()
def downsample(self, factor: int)
-
Expand source code
def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)-
Expand source code
def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
Alias for
spatial_gradient()
def grid_scatter(self, *args, **kwargs)
-
Expand source code
def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs)
Deprecated. Use
sample()
withscatter=True
instead. def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)-
Expand source code
def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)
Alias for
laplace()
def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)-
Expand source code
def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]
Return the field values as
NumPy
array(s).Args
order
- Dimension order as
str
orShape
.
Returns
A single NumPy array for uniform values, else a list of NumPy arrays.
def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
-
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths)
Alias for
pad()
.Pads this
Field
using its extrapolation.Unlike padding the values, this function also affects the
geometry
of the field, changing its size and origin depending onwidths
.Args
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
Padded
Field
def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor-
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs)
Sample the values of this
Field
at the given location or geometry.Args
where
- Location
Tensor
orGeometry
or at
'center'
or'face'
.**kwargs
- Sampling arguments.
Returns
Tensor
def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta))
Move the positions of this field's
geometry
bydelta
.See Also:
Field.shifted_to()
.Args
delta
- Shift amount for each center position of
geometry
.
Returns
New
Field
sampled atgeometry.center + delta
. def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position))
Move the positions of this field's
geometry
topositions
.See Also:
Field.shifted()
.Args
position
- New center positions of
geometry
.
Returns
New
Field
sampled at given positions. def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
-
Expand source code
def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result
Stacks all component grids into a single uniform
phi.math.Tensor
. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the gridresolution
in every spatial dimension.Returns
Uniform
phi.math.Tensor
. def to_grid(self, resolution=(), bounds=None, **resolution_)
-
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
-
Expand source code
def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor()
Returns a uniform tensor containing
values
.For periodic grids, which always have a uniform value tensor, `values' is returned directly. If
values
is not uniform, it is padded as inStaggeredGrid.staggered_tensor()
. def with_boundary(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_bounds(self, bounds: phi.geom._box.Box)
-
Expand source code
def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary)
Returns a copy of this field with
bounds
replaced. def with_elements(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_extrapolation(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_geometry(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_values(self, values, **sampling_kwargs)
-
Expand source code
def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary)
Returns a copy of this field with
values
replaced.
class Grid
-
Expand source code
@dataclass(frozen=True) class Field(metaclass=_FieldType): """ A `Field` represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in the `geometry` property and the corresponding values are stored as one `Tensor` in `values`. The boundary conditions and values outside the geometry are determined by `boundary`. Examples: Create a periodic 2D grid, initialized via noise fluctuations. >>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC) Create a field on an unstructured mesh loaded from a .gmsh file >>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0}) Create two cubes and compute a scalar values for each. >>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x) See the `phi.field` module documentation at https://tum-pbs.github.io/PhiFlow/Fields.html """ geometry: Geometry """ Discretization `Geometry`. This determines where in space the `values` are sampled as well as their relationship and interpretation. """ values: Tensor """ The sampled values, matching some point set of `geometry`, e.g. center points, see `Geometry.sets`.""" boundary: Extrapolation = 0. """ Boundary conditions describe the values outside of `geometry` and are used by numerical solvers to compute edge values. """ variable_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) should be traced / optimized. See `phiml.math.magic.PhiTreeNode.__variable_attrs__`""" value_attrs: Tuple[str, ...] = ('values',) """ Which of the three attributes (geometry,values,boundary) are considered values. See `phiml.math.magic.PhiTreeNode.__value_attrs__`""" def __post_init__(self): math.merge_shapes(self.values, non_batch(self.sampled_elements).non_channel) # shape check @property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry @property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry @property def faces(self): return get_faces(self.geometry, self.boundary) @property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary) @property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary) @property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary) @property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry @property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry @property def is_centered(self): return not self.is_staggered @property def is_staggered(self): return is_staggered(self.values, self.geometry) @property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation) @property def points(self): return self.center @property def data(self) -> Tensor: return self.values def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)] def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor() @property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary @property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values @property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch @property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank @property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper) box = bounds @property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid) @property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh) @property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph) @property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True @property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution @property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary) def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary) def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary) def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary) def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary) @property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1] def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs) def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs) def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector']) def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary) def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary) with_extrapolation = with_boundary def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary) def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary) with_elements = with_geometry def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta)) def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position)) def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths) def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation) def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind) def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at) def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew) def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.") def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result @staticmethod def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Field': from ._field_math import stack return stack(values, dim, kwargs.get('bounds', None)) @staticmethod def __concat__(values: tuple, dim: str, **kwargs) -> 'Field': from ._field_math import concat return concat(values, dim) def __and__(self, other): assert isinstance(other, Field) assert instance(self).rank == instance(other).rank == 1, f"Can only use & on PointClouds that have a single instance dimension but got shapes {self.shape} & {other.shape}" from ._field_math import concat return concat([self, other], instance(self)) def __matmul__(self, other: 'Field'): # value @ representation # Deprecated. Use `resample(value, field)` instead. warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(self, to=other, keep_boundary=False) def __rmatmul__(self, other): # values @ representation if isinstance(other, (Geometry, Number, tuple, list, FieldInitializer)): warnings.warn("value @ field is deprecated. Use resample(value, field) instead.", DeprecationWarning) from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __rshift__(self, other): if isinstance(other, (Field, Geometry)): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) return self.at(other, keep_boundary=False) else: return NotImplemented def __rrshift__(self, other): return self.with_values(other) def __lshift__(self, other): return self.with_values(other) def __rrshift__(self, other): warnings.warn(">> operator for Fields is deprecated. Use field.at(), the constructor or obj @ field instead.", SyntaxWarning, stacklevel=2) if not isinstance(self, Field): return NotImplemented if isinstance(other, (Geometry, float, int, complex, tuple, list, FieldInitializer)): from ._resample import resample return resample(other, to=self, keep_boundary=False) return NotImplemented def __getitem__(self, item) -> 'Field': """ Access a slice of the Field. The returned `Field` may be of a different type than `self`. Args: item: `dict` mapping dimensions (`str`) to selections (`int` or `slice`) or other supported type, such as `int` or `str`. Returns: Sliced `Field`. """ item = slicing_dict(self, item) if not item: return self boundary = domain_slice(self.boundary, item, tuple(self.geometry.boundary_faces if self.sampled_at == 'face' else self.geometry.boundary_elements)) item_without_vec = {dim: selection for dim, selection in item.items() if dim != 'vector'} geometry = self.geometry[item_without_vec] if self.is_staggered and 'vector' in item and '~vector' in self.geometry.face_shape: assert isinstance(self.geometry, UniformGrid), f"Vector slicing is only supported for grids" dims = item['vector'] dims_ = self.geometry.shape['vector'].after_gather({'vector': dims}) dims = dims_.item_names[0] if dims_ else [dims] if isinstance(dims, str) else [self.geometry.shape['vector'].item_names[0][dims]] proj_dims = set(self.resolution.names) - set(dims) if any(dim not in item for dim in proj_dims): # warnings.warn(f"Projecting a staggered grid (by slicing 'vector' without the corresponding spatial dims) will return a non-staggered grid. The projected dims {proj_dims} were not sliced off.\nFull slice: {item}") item['~vector'] = item['vector'] del item['vector'] geometry = self.sampled_elements[item] else: item['~vector'] = dims del item['vector'] values = self.values[item] return Field(geometry, values, boundary) def __getattr__(self, name: str) -> BoundDim: return BoundDim(self, name) def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name) def __expand__(self, dims: Shape, **kwargs) -> 'Field': return self.with_values(expand(self.values, dims, **kwargs)) def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'Field': elements = math.rename_dims(self.geometry, dims, new_dims) values = math.rename_dims(self.values, dims, new_dims) extrapolation = math.rename_dims(self.boundary, dims, new_dims, **kwargs) return Field(elements, values, extrapolation) def __eq__(self, other): if not isinstance(other, Field): return False if self.geometry != other.geometry: return False if self.boundary != other.boundary: return False return math.always_close(self.values, other.values) def __hash__(self): return hash((self.geometry, self.boundary)) def __mul__(self, other): return self._op2(other, lambda d1, d2: d1 * d2) __rmul__ = __mul__ def __truediv__(self, other): return self._op2(other, lambda d1, d2: d1 / d2) def __rtruediv__(self, other): return self._op2(other, lambda d1, d2: d2 / d1) def __sub__(self, other): return self._op2(other, lambda d1, d2: d1 - d2) def __rsub__(self, other): return self._op2(other, lambda d1, d2: d2 - d1) def __add__(self, other): return self._op2(other, lambda d1, d2: d1 + d2) __radd__ = __add__ def __pow__(self, power, modulo=None): return self._op2(power, lambda f, p: f ** p) def __neg__(self): return self._op1(lambda x: -x) def __gt__(self, other): return self._op2(other, lambda x, y: x > y) def __ge__(self, other): return self._op2(other, lambda x, y: x >= y) def __lt__(self, other): return self._op2(other, lambda x, y: x < y) def __le__(self, other): return self._op2(other, lambda x, y: x <= y) def __abs__(self): return self._op1(lambda x: abs(x)) def _op1(self: 'Field', operator: Callable) -> 'Field': """ Perform an operation on the data of this field. Args: operator: function that accepts tensors and extrapolations and returns objects of the same type and dimensions Returns: Field of same type """ values = operator(self.values) extrapolation_ = operator(self.boundary) return self.with_values(values).with_extrapolation(extrapolation_) def _op2(self, other, operator) -> 'Field': if isinstance(other, Geometry): raise ValueError(f"Cannot combine {self.__class__.__name__} with a Geometry, got {type(other)}") if isinstance(other, Field): if self.geometry == other.geometry: values = operator(self.values, other.values) extrapolation_ = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, extrapolation_) from ._resample import sample other_values = sample(other, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry) values = operator(self.values, other_values) boundary = operator(self.boundary, other.extrapolation) return Field(self.geometry, values, boundary) else: if isinstance(other, (tuple, list)) and len(other) == self.spatial_rank: other = math.wrap(other, self.geometry.shape['vector']) else: other = math.wrap(other) # try: # boundary = operator(self.boundary, as_boundary(other, self.geometry)) # except TypeError: # e.g. ZERO_GRADIENT + constant boundary = self.boundary # constants don't affect the boundary conditions (legacy reasons) if 'vector' in self.shape and 'vector' not in self.values.shape and '~vector' in self.values.shape: other = other.vector.as_dual() values = operator(self.values, other) return Field(self.geometry, values, boundary) def __repr__(self): if self.is_grid: type_name = "Grid" if self.is_centered else "Grid faces" elif self.is_mesh: type_name = "Mesh" if self.is_centered else "Mesh faces" elif self.is_point_cloud: type_name = "Point cloud" if self.is_centered else "Point cloud edges" elif self.is_graph: type_name = "Graph" if self.is_centered else "Graph edges" else: type_name = self.__class__.__name__ if self.values is not None: return f"{type_name}[{self.values}, ext={self.boundary}]" else: return f"{type_name}[{self.resolution}, ext={self.boundary}]" def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs) def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
A
Field
represents a discretized physical quantity (like temperature field or velocity field). The sample points and their relation are encoded in thegeometry
property and the corresponding values are stored as oneTensor
invalues
. The boundary conditions and values outside the geometry are determined byboundary
.Examples
Create a periodic 2D grid, initialized via noise fluctuations.
>>> Field(UniformGrid(x=32, y=32), values=Noise(), boundary=PERIODIC)
Create a field on an unstructured mesh loaded from a .gmsh file
>>> mesh = phi.geom.load_gmsh('cylinder.msh', ('y-', 'x+', 'y+', 'x-', 'cyl+', 'cyl-')) >>> Field(mesh, values=vec(x=1, y=0), boundary={'x': ZERO_GRADIENT, 'y': 0, 'cyl': 0})
Create two cubes and compute a scalar values for each.
>>> Field(Cuboid(vec(x=[0, 2], y=0), x=1, y=1), values=lambda x,y: x)
See the
phi.field
module documentation at https://tum-pbs.github.io/PhiFlow/Fields.htmlClass variables
var boundary : phiml.math.extrapolation.Extrapolation
-
Boundary conditions describe the values outside of
geometry
and are used by numerical solvers to compute edge values. var geometry : phi.geom._geom.Geometry
-
Discretization
Geometry
. This determines where in space thevalues
are sampled as well as their relationship and interpretation. var value_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) are considered values. See
phiml.math.magic.PhiTreeNode.__value_attrs__
var values : phiml.math._tensors.Tensor
-
The sampled values, matching some point set of
geometry
, e.g. center points, seeGeometry.sets
. var variable_attrs : Tuple[str, ...]
-
Which of the three attributes (geometry,values,boundary) should be traced / optimized. See
phiml.math.magic.PhiTreeNode.__variable_attrs__
Instance variables
prop bounds : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop box : phi.geom._box.BaseBox
-
Expand source code
@property def bounds(self) -> BaseBox: """ The bounds represent the area inside which the values of this `Field` are valid. The bounds will also be used as axis limits for plots. The bounds can be set manually in the constructor, otherwise default bounds will be generated. For fields that are valid without bounds, the lower and upper limit of `bounds` is set to `-inf` and `inf`, respectively. Fields whose spatial rank is determined only during sampling return an empty `Box`. """ if isinstance(self.geometry.bounds, BaseBox): return self.geometry.bounds extent = self.geometry.bounding_half_extent().vector.as_dual('_extent') points = self.geometry.center + extent lower = math.min(points, dim=points.shape.non_batch.non_channel) upper = math.max(points, dim=points.shape.non_batch.non_channel) return Box(lower, upper)
The bounds represent the area inside which the values of this
Field
are valid. The bounds will also be used as axis limits for plots.The bounds can be set manually in the constructor, otherwise default bounds will be generated.
For fields that are valid without bounds, the lower and upper limit of
bounds
is set to-inf
andinf
, respectively.Fields whose spatial rank is determined only during sampling return an empty
Box
. prop cells
-
Expand source code
@property def cells(self): assert isinstance(self.geometry, (UniformGrid, Mesh)) return self.geometry
prop center : phiml.math._tensors.Tensor
-
Expand source code
@property def center(self) -> Tensor: """ Returns the center points of the `elements` of this `Field`. """ all_points = self.geometry.get_points(self.sampled_at) boundary = self.geometry.get_boundary(self.sampled_at) return slice_off_constant_faces(all_points, boundary, self.extrapolation)
Returns the center points of the
elements
of thisField
. prop data : phiml.math._tensors.Tensor
-
Expand source code
@property def data(self) -> Tensor: return self.values
prop dx : phiml.math._tensors.Tensor
-
Expand source code
@property def dx(self) -> Tensor: assert spatial(self.geometry), f"dx is only defined for elements with spatial dims but Field has elements {self.geometry.shape}" return self.bounds.size / self.resolution
prop elements
-
Expand source code
@property def elements(self): # raise SyntaxError("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead.") warnings.warn("Field.elements is deprecated. Use Field.geometry or Field.sampled_elements instead. Field.elements now defaults to Field.geometry.", DeprecationWarning, stacklevel=2) return self.geometry
prop extrapolation : phiml.math.extrapolation.Extrapolation
-
Expand source code
@property def extrapolation(self) -> Extrapolation: """ Returns the `Extrapolation` of this `Field`. """ return self.boundary
Returns the
Extrapolation
of thisField
. prop face_areas
-
Expand source code
@property def face_areas(self): return self.geometry.face_areas # return slice_off_constant_faces(self.geometry.face_areas, self.geometry.boundary_faces, self.boundary)
prop face_centers
-
Expand source code
@property def face_centers(self): return self.geometry.face_centers # return slice_off_constant_faces(self.geometry.face_centers, self.geometry.boundary_faces, self.boundary)
prop face_normals
-
Expand source code
@property def face_normals(self): return self.geometry.face_normals # return slice_off_constant_faces(self.geometry.face_normals, self.geometry.boundary_faces, self.boundary)
prop faces
-
Expand source code
@property def faces(self): return get_faces(self.geometry, self.boundary)
prop graph : phi.geom._graph.Graph
-
Expand source code
@property def graph(self) -> Graph: """Cast `self.geometry` to a `phi.geom.Graph`.""" assert isinstance(self.geometry, Graph), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aGraph
. prop grid : phi.geom._grid.UniformGrid
-
Expand source code
@property def grid(self) -> UniformGrid: """Cast `self.geometry` to a `phi.geom.UniformGrid`.""" assert isinstance(self.geometry, UniformGrid), f"Geometry is not a UniformGrid but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aUniformGrid
. prop is_centered
-
Expand source code
@property def is_centered(self): return not self.is_staggered
prop is_graph
-
Expand source code
@property def is_graph(self): """A Field represents graph data if its `geometry` is a `phi.geom.Graph` instance.""" return isinstance(self.geometry, Graph)
A Field represents graph data if its
geometry
is aGraph
instance. prop is_grid
-
Expand source code
@property def is_grid(self): """A Field represents grid data if its `geometry` is a `phi.geom.UniformGrid` instance.""" return isinstance(self.geometry, UniformGrid)
A Field represents grid data if its
geometry
is aUniformGrid
instance. prop is_mesh
-
Expand source code
@property def is_mesh(self): """A Field represents mesh data if its `geometry` is a `phi.geom.Mesh` instance.""" return isinstance(self.geometry, Mesh)
A Field represents mesh data if its
geometry
is aMesh
instance. prop is_point_cloud
-
Expand source code
@property def is_point_cloud(self): """A Field represents graph data if its `geometry` is not a set of connected elements, but rather individual geometric objects.""" if isinstance(self.geometry, (UniformGrid, Mesh, Graph)): return False if isinstance(self.geometry, (BaseBox, Sphere, Point)): return True return True
A Field represents graph data if its
geometry
is not a set of connected elements, but rather individual geometric objects. prop is_staggered
-
Expand source code
@property def is_staggered(self): return is_staggered(self.values, self.geometry)
prop mesh : phi.geom._mesh.Mesh
-
Expand source code
@property def mesh(self) -> Mesh: """Cast `self.geometry` to a `phi.geom.Mesh`.""" assert isinstance(self.geometry, Mesh), f"Geometry is not a mesh but {type(self.geometry)}" return self.geometry
Cast
self.geometry
to aMesh
. prop points
-
Expand source code
@property def points(self): return self.center
prop resolution
-
Expand source code
@property def resolution(self): return self.geometry.shape.non_channel.non_dual.non_batch
prop sampled_at
-
Expand source code
@property def sampled_at(self): matching_sets = [s for s, s_shape in self.geometry.sets.items() if s_shape.non_batch in self.values.shape] return matching_sets[-1]
prop sampled_elements : phi.geom._geom.Geometry
-
Expand source code
@property def sampled_elements(self) -> Geometry: """ If the values represent are sampled at the element centers or represent the whole element, returns `self.geometry`. If the values are sampled at the faces, returns `self.faces`. """ return get_faces(self.geometry, self.boundary) if is_staggered(self.values, self.geometry) else self.geometry
If the values represent are sampled at the element centers or represent the whole element, returns
self.geometry
. If the values are sampled at the faces, returnsself.faces
. prop shape : phiml.math._shape.Shape
-
Expand source code
@property def shape(self) -> Shape: """ Returns a shape with the following properties * The spatial dimension names match the dimensions of this Field * The batch dimensions match the batch dimensions of this Field * The channel dimensions match the channels of this Field """ if self.is_grid and '~vector' in self.values.shape: return batch(self.geometry) & self.resolution & non_dual(self.values).without(self.resolution) & self.geometry.shape['vector'] set_shape = self.geometry.sets[self.sampled_at] return batch(self.geometry) & (channel(self.geometry) - 'vector') & set_shape & self.values
Returns a shape with the following properties
- The spatial dimension names match the dimensions of this Field
- The batch dimensions match the batch dimensions of this Field
- The channel dimensions match the channels of this Field
prop spatial_rank : int
-
Expand source code
@property def spatial_rank(self) -> int: """ Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the `data`. """ return self.geometry.spatial_rank
Spatial rank of the field (1 for 1D, 2 for 2D, 3 for 3D). This is equal to the spatial rank of the
data
.
Methods
def as_boundary(self) ‑> phiml.math.extrapolation.Extrapolation
-
Expand source code
def as_boundary(self) -> Extrapolation: """ Returns an `Extrapolation` representing this 'Field''s values as a Dirichlet (constant) boundary. If this `Field` encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of this `Field`'s sampled domain are required, this `Field`'s boundary conditions will be applied to determine the boundary values. Returns: `Extrapolation` """ from ._embed import FieldEmbedding return FieldEmbedding(self)
Returns an
Extrapolation
representing this 'Field''s values as a Dirichlet (constant) boundary. If thisField
encloses the required boundaries, its values will be interpolated to the required boundaries. If boundaries outside of thisField
's sampled domain are required, thisField
's boundary conditions will be applied to determine the boundary values.Returns
Extrapolation
def as_points(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_points(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud. This replaces the `Field.geometry` with a `phi.geom.Point` instance while leaving the sample points unchanged. See Also: `Field.as_spheres()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Point` geometry. """ points = self.sampled_elements.center values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) return Field(Point(points), values, self.boundary)
Returns this field as a PointCloud. This replaces the
Field.geometry
with aPoint
instance while leaving the sample points unchanged.See Also:
Field.as_spheres()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butPoint
geometry. def as_spheres(self, list_dim: phiml.math._shape.Shape | None = (elementsⁱ=None)) ‑> phi.field._field.Field
-
Expand source code
def as_spheres(self, list_dim: Optional[Shape] = instance('elements')) -> 'Field': """ Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the `Field.geometry` with a `phi.geom.Sphere` instance while leaving the sample points unchanged. See Also: `Field.as_points()`. Args: list_dim: If not `None`, packs spatial, instance and dual dims. Defaults to `instance('elements')`. Returns: `Field` with same values and boundaries but `Sphere` geometry. """ points = self.sampled_elements.center volumes = self.sampled_elements.volume values = self.values if list_dim: dims = non_batch(points).non_channel & non_batch(points).non_channel points = pack_dims(points, dims, list_dim) values = pack_dims(values, dims, list_dim) volumes = pack_dims(volumes, dims, list_dim) return Field(Sphere(points, volume=volumes), values, self.boundary)
Returns this field as a PointCloud with spherical / circular elements, preserving element volumes. This replaces the
Field.geometry
with aSphere
instance while leaving the sample points unchanged.See Also:
Field.as_points()
.Args
list_dim
- If not
None
, packs spatial, instance and dual dims. Defaults toinstance('elements')
.
Returns
Field
with same values and boundaries butSphere
geometry. def at(self,
representation: ForwardRef('Field') | phi.geom._geom.Geometry,
keep_boundary=False,
**kwargs) ‑> phi.field._field.Field-
Expand source code
def at(self, representation: Union['Field', Geometry], keep_boundary=False, **kwargs) -> 'Field': """ Short for `resample(self, representation)` See Also `resample()`. Returns: Field object of same type as `representation` """ from ._resample import resample return resample(self, representation, keep_boundary, **kwargs)
Short for
resample()(self, representation)
See Also
resample()
.Returns
Field object of same type as
representation
def at_centers(self, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_centers(self, **kwargs) -> 'Field': """ Interpolates the values to the cell centers. See Also: `Field.at_faces()`, `Field.at()`, `resample`. Args: **kwargs: Sampling arguments. Returns: `CenteredGrid` sampled at cell centers. """ if self.is_centered: return self from ._resample import sample values = sample(self, self.geometry, at='center', boundary=self.boundary, **kwargs) return Field(self.geometry, values, self.boundary)
Interpolates the values to the cell centers.
See Also:
Field.at_faces()
,Field.at()
,resample()
.Args
**kwargs
- Sampling arguments.
Returns
CenteredGrid()
sampled at cell centers. def at_faces(self, boundary=None, **kwargs) ‑> phi.field._field.Field
-
Expand source code
def at_faces(self, boundary=None, **kwargs) -> 'Field': if self.is_staggered and not boundary: return self boundary = as_boundary(boundary, self.geometry) if boundary else self.boundary from ._resample import sample values = sample(self, self.geometry, at='face', boundary=boundary, **kwargs) return Field(self.geometry, values, boundary)
def closest_values(self, points: phiml.math._tensors.Tensor)
-
Expand source code
def closest_values(self, points: Tensor): """ Sample the closest grid point values of this field at the world-space locations (in physical units) given by `points`. Points must have a single channel dimension named `vector`. It may additionally contain any number of batch and spatial dimensions, all treated as batch dimensions. Args: points: world-space locations Returns: Closest grid point values as a `Tensor`. For each dimension, the grid points immediately left and right of the sample points are evaluated. For each point in `points`, a *2^d* cube of points is determined where *d* is the number of spatial dimensions of this field. These values are stacked along the new dimensions `'closest_<dim>'` where `<dim>` refers to the name of a spatial dimension. """ warnings.warn("Field.closest_values() is deprecated.", DeprecationWarning, stacklevel=2) if isinstance(points, Geometry): points = points.center # --- CenteredGrid --- local_points = self.box.global_to_local(points) * self.resolution - 0.5 return math.closest_grid_values(self.values, local_points, self.extrapolation) # --- StaggeredGrid --- if 'staggered_direction' in points.shape: points_ = math.unstack(points, '~vector') channels = [component.closest_values(p) for p, component in zip(points_, self.vector.unstack())] else: channels = [component.closest_values(points) for component in self.vector.unstack()] return math.stack(channels, points.shape['~vector'])
Sample the closest grid point values of this field at the world-space locations (in physical units) given by
points
. Points must have a single channel dimension 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. def curl(self, at='corner')
-
Expand source code
def curl(self, at='corner'): """Alias for `phi.field.curl`""" from ._field_math import curl return curl(self, at=at)
Alias for
curl()
def dimension(self, name: str)
-
Expand source code
def dimension(self, name: str): """ Returns a reference to one of the dimensions of this field. The dimension reference can be used the same way as a `Tensor` dimension reference. Notable properties and methods of a dimension reference are: indexing using `[index]`, `unstack()`, `size`, `exists`, `is_batch`, `is_spatial`, `is_channel`. A shortcut to calling this function is the syntax `field.<dim_name>` which calls `field.dimension(<dim_name>)`. Args: name: dimension name Returns: dimension reference """ return BoundDim(self, name)
Returns a reference to one of the dimensions of this field.
The dimension reference can be used the same way as a
Tensor
dimension reference. Notable properties and methods of a dimension reference are: indexing using[index]
,unstack()
,size
,exists
,is_batch
,is_spatial
,is_channel
.A shortcut to calling this function is the syntax
field.<dim_name>
which callsfield.dimension(<dim_name>)
.Args
name
- dimension name
Returns
dimension reference
def divergence(self,
order=2,
implicit: phiml.math._optimize.Solve = None,
upwind: Field = None)-
Expand source code
def divergence(self, order=2, implicit: Solve = None, upwind: 'Field' = None): """Alias for `phi.field.divergence`""" from ._field_math import divergence return divergence(self, order=order, implicit=implicit, upwind=upwind)
Alias for
divergence()
def downsample(self, factor: int)
-
Expand source code
def downsample(self, factor: int): from ._field_math import downsample2x result = self while factor >= 2: result = downsample2x(result) factor /= 2 if math.close(factor, 1.): return result from ._resample import resample raise NotImplementedError(f"downsample does not support fractional re-sampling. Only 2^n currently supported.")
def gradient(self,
boundary: phiml.math.extrapolation.Extrapolation = None,
at: str = 'center',
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
stack_dim: phiml.math._shape.Shape | str = (vectorᶜ=None),
order=2,
implicit: phiml.math._optimize.Solve = None,
scheme=None,
upwind: Field = None,
gradient_extrapolation: phiml.math.extrapolation.Extrapolation = None)-
Expand source code
def gradient(self, boundary: Extrapolation = None, at: str = 'center', dims: math.DimFilter = spatial, stack_dim: Union[Shape, str] = channel('vector'), order=2, implicit: Solve = None, scheme=None, upwind: 'Field' = None, gradient_extrapolation: Extrapolation = None): """Alias for `phi.field.spatial_gradient`""" from ._field_math import spatial_gradient return spatial_gradient(self, boundary=boundary, at=at, dims=dims, stack_dim=stack_dim, order=order, implicit=implicit, scheme=scheme, upwind=upwind, gradient_extrapolation=gradient_extrapolation)
Alias for
spatial_gradient()
def grid_scatter(self, *args, **kwargs)
-
Expand source code
def grid_scatter(self, *args, **kwargs): """Deprecated. Use `sample` with `scatter=True` instead.""" warnings.warn("Field.grid_scatter() is deprecated. Use field.sample() with scatter=True instead.", DeprecationWarning, stacklevel=2) from ._resample import grid_scatter return grid_scatter(self, *args, **kwargs)
Deprecated. Use
sample()
withscatter=True
instead. def laplace(self,
axes: str | Sequence | set | ForwardRef('Shape') | Callable | None = <function spatial>,
gradient: Field = None,
order=2,
implicit: phiml.math._optimize.Solve = None,
weights: phiml.math._tensors.Tensor | ForwardRef('Field') = None,
upwind: Field = None,
correct_skew=True)-
Expand source code
def laplace(self, axes: DimFilter = spatial, gradient: 'Field' = None, order=2, implicit: math.Solve = None, weights: Union[Tensor, 'Field'] = None, upwind: 'Field' = None, correct_skew=True): """Alias for `phi.field.laplace`""" from ._field_math import laplace return laplace(self, axes=axes, gradient=gradient, order=order, implicit=implicit, weights=weights, upwind=upwind, correct_skew=correct_skew)
Alias for
laplace()
def numpy(self,
order: str | Sequence | set | ForwardRef('Shape') | Callable | None = None)-
Expand source code
def numpy(self, order: DimFilter = None): """ Return the field values as `NumPy` array(s). Args: order: Dimension order as `str` or `Shape`. Returns: A single NumPy array for uniform values, else a list of NumPy arrays. """ if order is None and self.is_grid: axes = self.values.shape.only(self.geometry.vector.item_names, reorder=True) order = concat_shapes(self.values.shape.dual, self.values.shape.batch, axes, self.values.shape.channel) if self.values.shape.is_uniform: return self.values.numpy(order) else: assert order is not None, f"order must be specified for non-uniform Field values" order = self.values.shape.only(order, reorder=True) stack_dims = order.non_uniform_shape inner_order = order.without(stack_dims) return [v.numpy(inner_order) for v in unstack(self.values, stack_dims)]
Return the field values as
NumPy
array(s).Args
order
- Dimension order as
str
orShape
.
Returns
A single NumPy array for uniform values, else a list of NumPy arrays.
def pad(self, widths: int | tuple | list | dict) ‑> phi.field._field.Field
-
Expand source code
def pad(self, widths: Union[int, tuple, list, dict]) -> 'Field': """ Alias for `phi.field.pad()`. Pads this `Field` using its extrapolation. Unlike padding the values, this function also affects the `geometry` of the field, changing its size and origin depending on `widths`. Args: widths: Either `int` or `(lower, upper)` to pad the same number of cells in all spatial dimensions or `dict` mapping dimension names to `(lower, upper)`. Returns: Padded `Field` """ from ._field_math import pad return pad(self, widths)
Alias for
pad()
.Pads this
Field
using its extrapolation.Unlike padding the values, this function also affects the
geometry
of the field, changing its size and origin depending onwidths
.Args
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
Padded
Field
def sample(self,
where: phi.geom._geom.Geometry | ForwardRef('Field') | phiml.math._tensors.Tensor,
at: str = 'center',
**kwargs) ‑> phiml.math._tensors.Tensor-
Expand source code
def sample(self, where: Union[Geometry, 'Field', Tensor], at: str = 'center', **kwargs) -> 'Tensor': """ Sample the values of this `Field` at the given location or geometry. Args: where: Location `Tensor` or `Geometry` or at: `'center'` or `'face'`. **kwargs: Sampling arguments. Returns: `Tensor` """ from ._resample import sample return sample(self, where, at, **kwargs)
Sample the values of this
Field
at the given location or geometry.Args
where
- Location
Tensor
orGeometry
or at
'center'
or'face'
.**kwargs
- Sampling arguments.
Returns
Tensor
def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted(self, delta: Tensor) -> 'Field': """ Move the positions of this field's `geometry` by `delta`. See Also: `Field.shifted_to`. Args: delta: Shift amount for each center position of `geometry`. Returns: New `Field` sampled at `geometry.center + delta`. """ return self.with_geometry(self.geometry.shifted(delta))
Move the positions of this field's
geometry
bydelta
.See Also:
Field.shifted_to()
.Args
delta
- Shift amount for each center position of
geometry
.
Returns
New
Field
sampled atgeometry.center + delta
. def shifted_to(self, position: phiml.math._tensors.Tensor) ‑> phi.field._field.Field
-
Expand source code
def shifted_to(self, position: Tensor) -> 'Field': """ Move the positions of this field's `geometry` to `positions`. See Also: `Field.shifted`. Args: position: New center positions of `geometry`. Returns: New `Field` sampled at given positions. """ return self.with_geometry(self.geometry.at(position))
Move the positions of this field's
geometry
topositions
.See Also:
Field.shifted()
.Args
position
- New center positions of
geometry
.
Returns
New
Field
sampled at given positions. def staggered_tensor(self) ‑> phiml.math._tensors.Tensor
-
Expand source code
def staggered_tensor(self) -> Tensor: """ Stacks all component grids into a single uniform `phi.math.Tensor`. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the grid `resolution` in every spatial dimension. Returns: Uniform `phi.math.Tensor`. """ assert self.resolution.names == self.shape.get_item_names('vector'), "Field.staggered_tensor() only defined for Fields whose vector components match the resolution" padded = [] for dim, component in zip(self.resolution.names, self.vector): widths = {d: (0, 1) for d in self.resolution.names} lo_valid, up_valid = self.extrapolation.valid_outer_faces(dim) widths[dim] = (int(not lo_valid), int(not up_valid)) padded.append(math.pad(component.values, widths, self.extrapolation[{'vector': dim}], bounds=self.bounds)) result = math.stack(padded, channel(vector=self.resolution)) assert result.shape.is_uniform return result
Stacks all component grids into a single uniform
phi.math.Tensor
. The individual components are padded to a common (larger) shape before being stacked. The shape of the returned tensor is exactly one cell larger than the gridresolution
in every spatial dimension.Returns
Uniform
phi.math.Tensor
. def to_grid(self, resolution=(), bounds=None, **resolution_)
-
Expand source code
def to_grid(self, resolution=math.EMPTY_SHAPE, bounds=None, **resolution_): resolution = resolution.spatial & spatial(**resolution_) if self.is_grid and (not resolution or resolution == self.resolution) and (bounds is None or bounds == self.bounds): return self bounds = self.bounds if bounds is None else bounds if not resolution: half_sizes = self.geometry.bounding_half_extent() if (half_sizes > 0).all: size = math.min(2 * half_sizes, non_batch(half_sizes).non_channel) else: cell_count = non_batch(self.geometry).non_channel.non_dual.volume size = (bounds.volume / cell_count) ** (1 / self.spatial_rank) res = math.maximum(1, math.round(bounds.size / size)) resolution = spatial(**res.vector) return Field(UniformGrid(resolution, bounds), self, self.boundary)
def uniform_values(self)
-
Expand source code
def uniform_values(self): """ Returns a uniform tensor containing `values`. For periodic grids, which always have a uniform value tensor, `values' is returned directly. If `values` is not uniform, it is padded as in `StaggeredGrid.staggered_tensor()`. """ if self.values.shape.is_uniform: return self.values else: return self.staggered_tensor()
Returns a uniform tensor containing
values
.For periodic grids, which always have a uniform value tensor, `values' is returned directly. If
values
is not uniform, it is padded as inStaggeredGrid.staggered_tensor()
. def with_boundary(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_bounds(self, bounds: phi.geom._box.Box)
-
Expand source code
def with_bounds(self, bounds: Box): """ Returns a copy of this field with `bounds` replaced. """ order = list(bounds.vector.item_names) geometry = self.geometry.vector[order] new_shape = self.values.shape.without(order) & self.values.shape.only(order, reorder=True) values = math.swap_axes(self.values, new_shape) return Field(geometry, values, self.boundary)
Returns a copy of this field with
bounds
replaced. def with_elements(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_extrapolation(self, boundary)
-
Expand source code
def with_boundary(self, boundary): """ Returns a copy of this field with the `boundary` replaced. """ boundary = as_boundary(boundary, self.geometry) boundary_elements = 'boundary_faces' if self.is_staggered else 'boundary_elements' old_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if self.boundary.determines_boundary_values(k)} new_determined_slices = {k: s for k, s in getattr(self.geometry, boundary_elements).items() if boundary.determines_boundary_values(k)} if old_determined_slices.values() == new_determined_slices.values(): return Field(self.geometry, self.values, boundary) # ToDo unnecessary once the rest is implemented to_add = {k: sl for k, sl in old_determined_slices.items() if sl not in new_determined_slices.values()} to_remove = [sl for sl in new_determined_slices.values() if sl not in old_determined_slices.values()] values = math.slice_off(self.values, *to_remove) if to_add: if self.is_mesh: values = self.mesh.pad_boundary(values, to_add, self.boundary) elif self.is_grid and self.is_staggered: values = self.values.vector.dual.as_channel() to_add = {k: {'vector' if dim == '~vector' else dim: v for dim, v in sl.items()} for k, sl in to_add.items()} values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) values = values.vector.as_dual() else: values = math.pad(values, list(to_add.values()), self.boundary, bounds=self.bounds) return Field(self.geometry, values, boundary)
Returns a copy of this field with the
boundary
replaced. def with_geometry(self, elements: phi.geom._geom.Geometry)
-
Expand source code
def with_geometry(self, elements: Geometry): """ Returns a copy of this field with `elements` replaced. """ assert non_batch(elements) == non_batch(self.geometry), f"Field.with_elements() only accepts elements with equal non-batch dimensions but got {elements.shape} for Field with shape {self.geometry.shape}" return Field(elements, self.values, self.boundary)
Returns a copy of this field with
elements
replaced. def with_values(self, values, **sampling_kwargs)
-
Expand source code
def with_values(self, values, **sampling_kwargs): """ Returns a copy of this field with `values` replaced. """ if not isinstance(values, (Tensor, Number)): from ._resample import sample values = sample(values, self.geometry, self.sampled_at, self.boundary, dot_face_normal=self.geometry if 'vector' not in self.values.shape else None, **sampling_kwargs) else: if not spatial(values): geo_shape = self.sampled_elements.shape if self.is_staggered else self.geometry.shape if '~vector' in geo_shape and 'vector' in shape(values) and '~vector' not in shape(values): values = values.vector.as_dual() values = expand(wrap(values), geo_shape.non_batch.non_channel) return Field(self.geometry, values, self.boundary)
Returns a copy of this field with
values
replaced.
class HardGeometryMask (geometry: phi.geom._geom.Geometry)
-
Expand source code
class HardGeometryMask(FieldInitializer): """ Deprecated since version 2.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry): self.geometry = geometry warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2) @property def shape(self): return self.geometry.shape.non_channel def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor: return math.to_float(self.geometry.lies_inside(geometry.center)) def __getitem__(self, item: dict): return HardGeometryMask(self.geometry[item])
Deprecated since version 2.3. Use
mask()
orresample()
instead.Ancestors
- phi.field._field.FieldInitializer
Subclasses
- phi.field._mask.SoftGeometryMask
Instance variables
prop shape
-
Expand source code
@property def shape(self): return self.geometry.shape.non_channel
class Noise (*shape: phiml.math._shape.Shape, scale=10.0, smoothness=1.0, **channel_dims)
-
Expand source code
class Noise(FieldInitializer): """ Generates random noise fluctuations which can be configured in physical size and smoothness. Each time values are sampled from a Noise field, a new noise field is generated. Noise is typically used as an initializer for CenteredGrids or StaggeredGrids. """ def __init__(self, *shape: math.Shape, scale=10., smoothness=1.0, **channel_dims): """ Args: shape: Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid. scale: Size of noise fluctuations in physical units. smoothness: Determines how quickly high frequencies die out. **dims: Additional dimensions, added to `shape`. """ self.scale = scale self.smoothness = smoothness self._shape = math.concat_shapes(*shape, channel(**channel_dims)) def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> Tensor: if isinstance(geometry, UniformGrid): if at == 'center': return self.grid_sample(geometry.resolution, geometry.grid_size) elif at == 'face': result = {dim: self.grid_sample(grid.resolution, grid.grid_size) for dim, grid in geometry.staggered_cells(boundaries).items()} return vec(geometry.face_shape.dual, **result) raise NotImplementedError(f"{type(geometry)} not supported. Only UniformGrid allowed.") def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None): shape = (self._shape if shape is None else shape) & resolution for dim in channel(self._shape): if dim.name == 'vector' and dim.item_names[0] is None: warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning) shape &= channel(**{dim.name: resolution.names}) rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape)) # Note: there is no complex32 # --- Compute 1 / k^2 --- k_vec = math.fftfreq(resolution, size) * resolution * math.tensor(self.scale) # in physical units k2 = math.vec_squared(k_vec) lowest_frequency = 0.1 weight_mask = math.to_float(k2 > lowest_frequency) inv_k2 = math.divide_no_nan(1, k2) # --- Compute result --- fft = rndj * inv_k2 ** self.smoothness * weight_mask array = math.real(math.ifft(fft)) array /= math.std(array, dim=array.shape.non_batch) array -= math.mean(array, dim=array.shape.non_batch) array = math.to_float(array) return array def __repr__(self): return f"{self._shape}, scale={self.scale}, smoothness={self.smoothness}"
Generates random noise fluctuations which can be configured in physical size and smoothness. Each time values are sampled from a Noise field, a new noise field is generated.
Noise is typically used as an initializer for CenteredGrids or StaggeredGrids.
Args
shape
- Batch and channel dimensions. Spatial dimensions will be added automatically once sampled on a grid.
scale
- Size of noise fluctuations in physical units.
smoothness
- Determines how quickly high frequencies die out.
**dims
- Additional dimensions, added to
shape
.
Ancestors
- phi.field._field.FieldInitializer
Methods
def grid_sample(self,
resolution: phiml.math._shape.Shape,
size,
shape: phiml.math._shape.Shape = None)-
Expand source code
def grid_sample(self, resolution: math.Shape, size, shape: math.Shape = None): shape = (self._shape if shape is None else shape) & resolution for dim in channel(self._shape): if dim.name == 'vector' and dim.item_names[0] is None: warnings.warn(f"Please provide item names for Noise dim {dim} using {dim}='x,y,z'", FutureWarning) shape &= channel(**{dim.name: resolution.names}) rndj = math.to_complex(random_normal(shape)) + 1j * math.to_complex(random_normal(shape)) # Note: there is no complex32 # --- Compute 1 / k^2 --- k_vec = math.fftfreq(resolution, size) * resolution * math.tensor(self.scale) # in physical units k2 = math.vec_squared(k_vec) lowest_frequency = 0.1 weight_mask = math.to_float(k2 > lowest_frequency) inv_k2 = math.divide_no_nan(1, k2) # --- Compute result --- fft = rndj * inv_k2 ** self.smoothness * weight_mask array = math.real(math.ifft(fft)) array /= math.std(array, dim=array.shape.non_batch) array -= math.mean(array, dim=array.shape.non_batch) array = math.to_float(array) return array
class Scene
-
Expand source code
class Scene: """ Provides methods for reading and writing simulation data. See the format documentation at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html . All data of a `Scene` is located inside a single directory with name `sim_xxxxxx` where `xxxxxx` is the `id`. The data of the scene is organized into NumPy files by *name* and *frame*. To create a new scene, use `Scene.create()`. To reference an existing scene, use `Scene.at()`. To list all scenes within a directory, use `Scene.list()`. """ def __init__(self, paths: Union[str, math.Tensor]): self._paths = math.wrap(paths) self._properties: Union[dict, None] = None def __getitem__(self, item): return Scene(self._paths[item]) def __getattr__(self, name: str) -> BoundDim: return BoundDim(self, name) def __variable_attrs__(self) -> Tuple[str, ...]: return 'paths', def __with_attrs__(self, **attrs): if 'paths' in attrs: return Scene(attrs['paths']) else: return Scene(self._paths) @property def shape(self): return self._paths.shape @property def is_batch(self): return self._paths.rank > 0 @property def path(self) -> str: """ Relative path of the scene directory. This property only exists for single scenes, not scene batches. """ assert not self.is_batch, "Scene.path is not defined for scene batches." return self._paths.native() @property def paths(self) -> math.Tensor: return self._paths @staticmethod def stack(*scenes: 'Scene', dim: Shape = batch('batch')) -> 'Scene': return Scene(math.stack([s._paths for s in scenes], dim)) @staticmethod def create(parent_directory: str, shape: math.Shape = math.EMPTY_SHAPE, name='sim', copy_calling_script=True, **dimensions) -> 'Scene': """ Creates a new `Scene` or a batch of new scenes inside `parent_directory`. See Also: `Scene.at()`, `Scene.list()`. Args: parent_directory: Directory to hold the new `Scene`. If it doesn't exist, it will be created. shape: Determines number of scenes to create. Multiple scenes will be represented by a `Scene` with `is_batch=True`. name: Name of the directory (excluding index). Default is `'sim'`. copy_calling_script: Whether to copy the Python file that invoked this method into the `src` folder of all created scenes. See `Scene.copy_calling_script()`. dimensions: Additional batch dimensions Returns: Single `Scene` object representing the new scene(s). """ shape = shape & math.batch(**dimensions) parent_directory = expanduser(parent_directory) abs_dir = abspath(parent_directory) if not isdir(abs_dir): os.makedirs(abs_dir) next_id = 0 else: indices = [int(f[len(name)+1:]) for f in os.listdir(abs_dir) if f.startswith(f"{name}_")] next_id = max([-1] + indices) + 1 ids = unpack_dim(wrap(tuple(range(next_id, next_id + shape.volume))), 'vector', shape) paths = math.map(lambda id_: join(parent_directory, f"{name}_{id_:06d}"), ids) scene = Scene(paths) scene.mkdir() if copy_calling_script: try: scene.copy_calling_script() except IOError as err: warnings.warn(f"Failed to copy calling script to scene during Scene.create(): {err}", RuntimeWarning) return scene @staticmethod def list(parent_directory: str, name='sim', include_other: bool = False, dim: Union[Shape, None] = None) -> Union['Scene', tuple]: """ Lists all scenes inside the given directory. See Also: `Scene.at()`, `Scene.create()`. Args: parent_directory: Directory that contains scene folders. name: Name of the directory (excluding index). Default is `'sim'`. include_other: Whether folders that do not match the scene format should also be treated as scenes. dim: Stack dimension. If None, returns tuple of `Scene` objects. Otherwise, returns a scene batch with this dimension. Returns: `tuple` of scenes. """ parent_directory = expanduser(parent_directory) abs_dir = abspath(parent_directory) if not isdir(abs_dir): return () names = [sim for sim in os.listdir(abs_dir) if sim.startswith(f"{name}_") or (include_other and isdir(join(abs_dir, sim)))] names = list(sorted(names)) if dim is None: return tuple(Scene(join(parent_directory, n)) for n in names) else: paths = math.wrap([join(parent_directory, n) for n in names], dim) return Scene(paths) @staticmethod def at(directory: Union[str, tuple, typing_list, math.Tensor, 'Scene'], id: Union[int, math.Tensor, None] = None) -> 'Scene': """ Creates a `Scene` for an existing directory. See Also: `Scene.create()`, `Scene.list()`. Args: directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`. id: (Optional) Scene `id`, will be determined from `directory` if not specified. Returns: `Scene` object for existing scene. """ if isinstance(directory, Scene): assert id is None, f"Got id={id} but directory is already a Scene." return directory if isinstance(directory, (tuple, list)): directory = math.wrap(directory, batch('scenes')) directory = math.wrap(math.map(lambda d: expanduser(d), directory)) if isinstance(id, int) and id < 0: assert directory.shape.volume == 1 scenes = Scene.list(directory.native()) assert len(scenes) >= -id, f"Failed to get scene {id} at {directory}. {len(scenes)} scenes available in that directory." return scenes[id] if id is None: paths = wrap(directory) else: id = math.wrap(id) paths = wrap(math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id)) # test all exist for path in math.flatten(wrap(paths), flatten_batch=True): if not isdir(path): raise IOError(f"There is no scene at '{path}'") return Scene(paths) def subpath(self, name: str, create=False, create_parent=False) -> Union[str, tuple]: """ Resolves the relative path `name` with this `Scene` as the root folder. Args: name: Relative path with this `Scene` as the root folder. create: Whether to create a directory of that name. create_parent: Whether to create the parent directory. Returns: Relative path including the path to this `Scene`. In batch mode, returns a `tuple`, else a `str`. """ def single_subpath(path): path = join(path, name) if create_parent and not isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) if create and not isdir(path): os.mkdir(path) return path result = math.map(single_subpath, self._paths) return result def _init_properties(self): if self._properties is not None: return def read_json(path: str) -> dict: json_file = join(path, "description.json") if isfile(json_file): with open(json_file) as stream: props = json.load(stream) if '__tensors__' in props: for key in props['__tensors__']: props[key] = math.from_dict(props[key]) return props else: return {} if self._paths.shape.volume == 1: self._properties = read_json(self._paths.native()) else: self._properties = {} dicts = [read_json(p) for p in self._paths] keys = set(sum([tuple(d.keys()) for d in dicts], ())) for key in keys: assert all(key in d for d in dicts), f"Failed to create batched Scene because property '{key}' is present in some scenes but not all." if all([math.all(d[key] == dicts[0][key]) for d in dicts]): self._properties[key] = dicts[0][key] else: self._properties[key] = stack([d[key] for d in dicts], self._paths.shape) if '__tensors__' in self._properties: del self._properties['__tensors__'] def exist_properties(self): """ Checks whether the file `description.json` exists or has existed. """ if self._properties is not None: return True # must have been written or read else: json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json") return isfile(json_file) def exists_config(self): """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """ if isinstance(self.path, str): return isfile(join(self.path, "description.json")) else: return any(isfile(join(p, "description.json")) for p in self.path) @property def properties(self): self._init_properties() return self._properties @properties.setter def properties(self, dict): self._properties = dict with open(join(self.path, "description.json"), "w") as out: json.dump(self._properties, out, indent=2) def put_property(self, key, value): """ See `Scene.put_properties()`. """ self._init_properties() self._properties[key] = value self._write_properties() def put_properties(self, update: dict = None, **kw_updates): """ Updates the properties dictionary and stores it in `description.json` of all scene folders. Args: update: new values, must be JSON serializable. kw_updates: additional update as keyword arguments. This overrides `update`. """ self._init_properties() if update: self._properties.update(update) self._properties.update(kw_updates) for key, value in self._properties.items(): if isinstance(value, (np.int64, np.int32)): value = int(value) elif isinstance(value, (np.float16, np.float32, np.float64, np.float16)) or (hasattr(np, 'float128') and isinstance(value, np.float128)): value = float(value) self._properties[key] = value self._write_properties() def _get_properties(self, index: dict): result = dict(self._properties) tensor_names = [] for key, value in self._properties.items(): if isinstance(value, math.Tensor): value = value[index] if value.rank == 0: value = value.dtype.kind(value) else: value = math.to_dict(value) tensor_names.append(key) result[key] = value if tensor_names: result['__tensors__'] = tuple(tensor_names) return result def _write_properties(self): for instance in self.paths.shape.meshgrid(): path = self.paths[instance].native() instance_properties = self._get_properties(instance) with open(join(path, "description.json"), "w") as out: json.dump(instance_properties, out, indent=2) def write(self, data: dict = None, frame=0, **kw_data): """ Writes fields to this scene. One NumPy file will be created for each `phi.field.Field` See Also: `Scene.read()`. Args: data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`. kw_data: Additional data, overrides elements in `data`. frame: Frame number. """ data = dict(data) if data else {} data.update(kw_data) for name, field in data.items(): self.write_field(field, name, frame) def write_field(self, field: Field, name: str, frame: int): """ Write a `Field` to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html . Args: field: single field or structure of Fields to save. name: Base file name. frame: Frame number as `int`, typically time step index. """ if not isinstance(field, Field): raise ValueError(f"Only Field instances can be saved but got {field}") name = _slugify_filename(name) files = wrap(math.map(lambda dir_: _filename(dir_, name, frame), self._paths)) write(field, files) def read_field(self, name: str, frame: int, convert_to_backend=True) -> Field: """ Reads a single `Field` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `Field` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend) read_array = read_field def read(self, *names: str, frame=0, convert_to_backend=True): """ Reads one or multiple fields from disc. See Also: `Scene.write()`. Args: names: Single field name or sequence of field names. frame: Frame number. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Single `phi.field.Field` or sequence of fields, depending on the type of `names`. """ if len(names) == 1 and isinstance(names[0], (tuple, list)): names = names[0] result = [self.read_array(name, frame, convert_to_backend) for name in names] return result[0] if len(names) == 1 else result @property def fieldnames(self) -> tuple: """ Determines all field names present in this `Scene`, independent of frame. """ return get_fieldnames(self.path) @property def frames(self): """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """ return get_frames(self.path, mode=set.union) @property def complete_frames(self): """ Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields. See Also: `Scene.frames` """ return get_frames(self.path, mode=set.intersection) def __repr__(self): return f"{self.paths:no-dtype}" def __eq__(self, other): return isinstance(other, Scene) and (other._paths == self._paths).all def copy_calling_script(self, full_trace=False, include_context_information=True): """ Copies the Python file that called this method into the `src` folder of this `Scene`. In batch mode, the script is copied to all scenes. Args: full_trace: Whether to include scripts that indirectly called this method. include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`. """ script_paths = [frame.filename for frame in inspect.stack()] script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths)) script_paths = set(script_paths) if full_trace else [script_paths[0]] self.subpath('src', create=True) for script_path in script_paths: if script_path.endswith('.py'): self.copy_src(script_path, only_external=False) elif 'ipython' in script_path: from IPython import get_ipython cells = get_ipython().user_ns['In'] blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)] text = "\n\n".join(blocks) self.copy_src_text('ipython.py', text) if include_context_information: for path in math.flatten(self._paths, flatten_batch=True): with open(join(path, 'src', 'context.json'), 'w') as context_file: json.dump({ 'phi_version': phi_version, 'argv': sys.argv }, context_file) def copy_src(self, script_path, only_external=True): for path in math.flatten(self._paths, flatten_batch=True): if not only_external or not _is_phi_file(script_path): shutil.copy(script_path, join(path, 'src', basename(script_path))) def copy_src_text(self, filename, text): for path in math.flatten(self._paths, flatten_batch=True): target = join(path, 'src', filename) with open(target, "w") as file: file.writelines(text) def mkdir(self): for path in math.flatten(self._paths, flatten_batch=True): isdir(path) or os.mkdir(path) def remove(self): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): shutil.rmtree(p) def rename(self, name: str): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): new_path = os.path.join(os.path.dirname(p), name) print(f"Renaming {p} to {new_path}") shutil.move(p, new_path)
Provides methods for reading and writing simulation data.
See the format documentation at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .
All data of a
Scene
is located inside a single directory with 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()
.Static methods
def at(directory: str | tuple | list | phiml.math._tensors.Tensor | ForwardRef('Scene'),
id: int | phiml.math._tensors.Tensor | None = None) ‑> phi.field._scene.Scene-
Expand source code
@staticmethod def at(directory: Union[str, tuple, typing_list, math.Tensor, 'Scene'], id: Union[int, math.Tensor, None] = None) -> 'Scene': """ Creates a `Scene` for an existing directory. See Also: `Scene.create()`, `Scene.list()`. Args: directory: Either directory containing scene folder if `id` is given, or scene path if `id=None`. id: (Optional) Scene `id`, will be determined from `directory` if not specified. Returns: `Scene` object for existing scene. """ if isinstance(directory, Scene): assert id is None, f"Got id={id} but directory is already a Scene." return directory if isinstance(directory, (tuple, list)): directory = math.wrap(directory, batch('scenes')) directory = math.wrap(math.map(lambda d: expanduser(d), directory)) if isinstance(id, int) and id < 0: assert directory.shape.volume == 1 scenes = Scene.list(directory.native()) assert len(scenes) >= -id, f"Failed to get scene {id} at {directory}. {len(scenes)} scenes available in that directory." return scenes[id] if id is None: paths = wrap(directory) else: id = math.wrap(id) paths = wrap(math.map(lambda d, i: join(d, f"sim_{i:06d}"), directory, id)) # test all exist for path in math.flatten(wrap(paths), flatten_batch=True): if not isdir(path): raise IOError(f"There is no scene at '{path}'") return Scene(paths)
Creates a
Scene
for an existing directory.See Also:
Scene.create()
,Scene.list()
.Args
directory
- Either directory containing scene folder if
id
is given, or scene path ifid=None
. id
- (Optional) Scene
id
, will be determined fromdirectory
if not specified.
Returns
Scene
object for existing scene. def create(parent_directory: str,
shape: phiml.math._shape.Shape = (),
name='sim',
copy_calling_script=True,
**dimensions) ‑> phi.field._scene.Scene-
Expand source code
@staticmethod def create(parent_directory: str, shape: math.Shape = math.EMPTY_SHAPE, name='sim', copy_calling_script=True, **dimensions) -> 'Scene': """ Creates a new `Scene` or a batch of new scenes inside `parent_directory`. See Also: `Scene.at()`, `Scene.list()`. Args: parent_directory: Directory to hold the new `Scene`. If it doesn't exist, it will be created. shape: Determines number of scenes to create. Multiple scenes will be represented by a `Scene` with `is_batch=True`. name: Name of the directory (excluding index). Default is `'sim'`. copy_calling_script: Whether to copy the Python file that invoked this method into the `src` folder of all created scenes. See `Scene.copy_calling_script()`. dimensions: Additional batch dimensions Returns: Single `Scene` object representing the new scene(s). """ shape = shape & math.batch(**dimensions) parent_directory = expanduser(parent_directory) abs_dir = abspath(parent_directory) if not isdir(abs_dir): os.makedirs(abs_dir) next_id = 0 else: indices = [int(f[len(name)+1:]) for f in os.listdir(abs_dir) if f.startswith(f"{name}_")] next_id = max([-1] + indices) + 1 ids = unpack_dim(wrap(tuple(range(next_id, next_id + shape.volume))), 'vector', shape) paths = math.map(lambda id_: join(parent_directory, f"{name}_{id_:06d}"), ids) scene = Scene(paths) scene.mkdir() if copy_calling_script: try: scene.copy_calling_script() except IOError as err: warnings.warn(f"Failed to copy calling script to scene during Scene.create(): {err}", RuntimeWarning) return scene
Creates a new
Scene
or a batch of new scenes 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). def list(parent_directory: str,
name='sim',
include_other: bool = False,
dim: phiml.math._shape.Shape | None = None) ‑> phi.field._scene.Scene | tuple-
Expand source code
@staticmethod def list(parent_directory: str, name='sim', include_other: bool = False, dim: Union[Shape, None] = None) -> Union['Scene', tuple]: """ Lists all scenes inside the given directory. See Also: `Scene.at()`, `Scene.create()`. Args: parent_directory: Directory that contains scene folders. name: Name of the directory (excluding index). Default is `'sim'`. include_other: Whether folders that do not match the scene format should also be treated as scenes. dim: Stack dimension. If None, returns tuple of `Scene` objects. Otherwise, returns a scene batch with this dimension. Returns: `tuple` of scenes. """ parent_directory = expanduser(parent_directory) abs_dir = abspath(parent_directory) if not isdir(abs_dir): return () names = [sim for sim in os.listdir(abs_dir) if sim.startswith(f"{name}_") or (include_other and isdir(join(abs_dir, sim)))] names = list(sorted(names)) if dim is None: return tuple(Scene(join(parent_directory, n)) for n in names) else: paths = math.wrap([join(parent_directory, n) for n in names], dim) return Scene(paths)
Lists all scenes inside the given directory.
See Also:
Scene.at()
,Scene.create()
.Args
parent_directory
- Directory that contains scene folders.
name
- Name of the directory (excluding index). Default is
'sim'
. include_other
- Whether folders that do not match the scene format should also be treated as scenes.
dim
- Stack dimension. If None, returns tuple of
Scene
objects. Otherwise, returns a scene batch with this dimension.
Returns
tuple
of scenes. def stack(*scenes: Scene,
dim: phiml.math._shape.Shape = (batchᵇ=None)) ‑> phi.field._scene.Scene-
Expand source code
@staticmethod def stack(*scenes: 'Scene', dim: Shape = batch('batch')) -> 'Scene': return Scene(math.stack([s._paths for s in scenes], dim))
Instance variables
prop complete_frames
-
Expand source code
@property def complete_frames(self): """ Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields. See Also: `Scene.frames` """ return get_frames(self.path, mode=set.intersection)
Determines all frame number for which all existing fields are available. If there are multiple fields stored within this scene, a frame is considered complete only if an entry exists for all fields.
See Also:
Scene.frames
prop fieldnames : tuple
-
Expand source code
@property def fieldnames(self) -> tuple: """ Determines all field names present in this `Scene`, independent of frame. """ return get_fieldnames(self.path)
Determines all field names present in this
Scene
, independent of frame. prop frames
-
Expand source code
@property def frames(self): """ Determines all frame numbers present in this `Scene`, independent of field names. See `Scene.complete_frames`. """ return get_frames(self.path, mode=set.union)
Determines all frame numbers present in this
Scene
, independent of field names. SeeScene.complete_frames
. prop is_batch
-
Expand source code
@property def is_batch(self): return self._paths.rank > 0
prop path : str
-
Expand source code
@property def path(self) -> str: """ Relative path of the scene directory. This property only exists for single scenes, not scene batches. """ assert not self.is_batch, "Scene.path is not defined for scene batches." return self._paths.native()
Relative path of the scene directory. This property only exists for single scenes, not scene batches.
prop paths : phiml.math._tensors.Tensor
-
Expand source code
@property def paths(self) -> math.Tensor: return self._paths
prop properties
-
Expand source code
@property def properties(self): self._init_properties() return self._properties
prop shape
-
Expand source code
@property def shape(self): return self._paths.shape
Methods
def copy_calling_script(self, full_trace=False, include_context_information=True)
-
Expand source code
def copy_calling_script(self, full_trace=False, include_context_information=True): """ Copies the Python file that called this method into the `src` folder of this `Scene`. In batch mode, the script is copied to all scenes. Args: full_trace: Whether to include scripts that indirectly called this method. include_context_information: If True, writes the phiflow version and `sys.argv` into `context.json`. """ script_paths = [frame.filename for frame in inspect.stack()] script_paths = list(filter(lambda path: not _is_phi_file(path), script_paths)) script_paths = set(script_paths) if full_trace else [script_paths[0]] self.subpath('src', create=True) for script_path in script_paths: if script_path.endswith('.py'): self.copy_src(script_path, only_external=False) elif 'ipython' in script_path: from IPython import get_ipython cells = get_ipython().user_ns['In'] blocks = [f"#%% In[{i}]\n{cell}" for i, cell in enumerate(cells)] text = "\n\n".join(blocks) self.copy_src_text('ipython.py', text) if include_context_information: for path in math.flatten(self._paths, flatten_batch=True): with open(join(path, 'src', 'context.json'), 'w') as context_file: json.dump({ 'phi_version': phi_version, 'argv': sys.argv }, context_file)
Copies the Python file that called this method into the
src
folder of 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
.
def copy_src(self, script_path, only_external=True)
-
Expand source code
def copy_src(self, script_path, only_external=True): for path in math.flatten(self._paths, flatten_batch=True): if not only_external or not _is_phi_file(script_path): shutil.copy(script_path, join(path, 'src', basename(script_path)))
def copy_src_text(self, filename, text)
-
Expand source code
def copy_src_text(self, filename, text): for path in math.flatten(self._paths, flatten_batch=True): target = join(path, 'src', filename) with open(target, "w") as file: file.writelines(text)
def exist_properties(self)
-
Expand source code
def exist_properties(self): """ Checks whether the file `description.json` exists or has existed. """ if self._properties is not None: return True # must have been written or read else: json_file = join(next(iter(math.flatten(self._paths, flatten_batch=True))), "description.json") return isfile(json_file)
Checks whether the file
description.json
exists or has existed. def exists_config(self)
-
Expand source code
def exists_config(self): """ Tests if the configuration file *description.json* exists. In batch mode, tests if any configuration exists. """ if isinstance(self.path, str): return isfile(join(self.path, "description.json")) else: return any(isfile(join(p, "description.json")) for p in self.path)
Tests if the configuration file description.json exists. In batch mode, tests if any configuration exists.
def mkdir(self)
-
Expand source code
def mkdir(self): for path in math.flatten(self._paths, flatten_batch=True): isdir(path) or os.mkdir(path)
def put_properties(self, update: dict = None, **kw_updates)
-
Expand source code
def put_properties(self, update: dict = None, **kw_updates): """ Updates the properties dictionary and stores it in `description.json` of all scene folders. Args: update: new values, must be JSON serializable. kw_updates: additional update as keyword arguments. This overrides `update`. """ self._init_properties() if update: self._properties.update(update) self._properties.update(kw_updates) for key, value in self._properties.items(): if isinstance(value, (np.int64, np.int32)): value = int(value) elif isinstance(value, (np.float16, np.float32, np.float64, np.float16)) or (hasattr(np, 'float128') and isinstance(value, np.float128)): value = float(value) self._properties[key] = value self._write_properties()
Updates the properties dictionary and stores it in
description.json
of all scene folders.Args
update
- new values, must be JSON serializable.
kw_updates
- additional update as keyword arguments. This overrides
update
.
def put_property(self, key, value)
-
Expand source code
def put_property(self, key, value): """ See `Scene.put_properties()`. """ self._init_properties() self._properties[key] = value self._write_properties()
def read(self, *names: str, frame=0, convert_to_backend=True)
-
Expand source code
def read(self, *names: str, frame=0, convert_to_backend=True): """ Reads one or multiple fields from disc. See Also: `Scene.write()`. Args: names: Single field name or sequence of field names. frame: Frame number. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: Single `phi.field.Field` or sequence of fields, depending on the type of `names`. """ if len(names) == 1 and isinstance(names[0], (tuple, list)): names = names[0] result = [self.read_array(name, frame, convert_to_backend) for name in names] return result[0] if len(names) == 1 else result
Reads one or multiple fields from disc.
See Also:
Scene.write()
.Args
names
- Single field name or sequence of field names.
frame
- Frame number.
convert_to_backend
- Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors.
Returns
Single
Field
or sequence of fields, depending on the type ofnames
. def read_array(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.Field
-
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> Field: """ Reads a single `Field` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `Field` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend)
def read_field(self, name: str, frame: int, convert_to_backend=True) ‑> phi.field._field.Field
-
Expand source code
def read_field(self, name: str, frame: int, convert_to_backend=True) -> Field: """ Reads a single `Field` from files contained in this `Scene` (batch). Args: name: Base file name. frame: Frame number as `int`, typically time step index. convert_to_backend: Whether to convert the read data to the data format of the default backend, e.g. TensorFlow tensors. Returns: `Field` """ name = _slugify_filename(name) files = math.map(lambda dir_: _filename(dir_, name, frame), self._paths) return read(files, convert_to_backend=convert_to_backend)
def remove(self)
-
Expand source code
def remove(self): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): shutil.rmtree(p)
Deletes the scene directory and all contained files.
def rename(self, name: str)
-
Expand source code
def rename(self, name: str): """ Deletes the scene directory and all contained files. """ for p in math.flatten(self._paths, flatten_batch=True): p = abspath(p) if isdir(p): new_path = os.path.join(os.path.dirname(p), name) print(f"Renaming {p} to {new_path}") shutil.move(p, new_path)
Deletes the scene directory and all contained files.
def subpath(self, name: str, create=False, create_parent=False) ‑> str | tuple
-
Expand source code
def subpath(self, name: str, create=False, create_parent=False) -> Union[str, tuple]: """ Resolves the relative path `name` with this `Scene` as the root folder. Args: name: Relative path with this `Scene` as the root folder. create: Whether to create a directory of that name. create_parent: Whether to create the parent directory. Returns: Relative path including the path to this `Scene`. In batch mode, returns a `tuple`, else a `str`. """ def single_subpath(path): path = join(path, name) if create_parent and not isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) if create and not isdir(path): os.mkdir(path) return path result = math.map(single_subpath, self._paths) return result
Resolves the relative path
name
with 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
. def write(self, data: dict = None, frame=0, **kw_data)
-
Expand source code
def write(self, data: dict = None, frame=0, **kw_data): """ Writes fields to this scene. One NumPy file will be created for each `phi.field.Field` See Also: `Scene.read()`. Args: data: `dict` mapping field names to `Field` objects that can be written using `phi.field.write()`. kw_data: Additional data, overrides elements in `data`. frame: Frame number. """ data = dict(data) if data else {} data.update(kw_data) for name, field in data.items(): self.write_field(field, name, frame)
Writes fields to this scene. One NumPy file will be created for each
Field
See Also:
Scene.read()
.Args
def write_field(self, field: phi.field._field.Field, name: str, frame: int)
-
Expand source code
def write_field(self, field: Field, name: str, frame: int): """ Write a `Field` to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html . Args: field: single field or structure of Fields to save. name: Base file name. frame: Frame number as `int`, typically time step index. """ if not isinstance(field, Field): raise ValueError(f"Only Field instances can be saved but got {field}") name = _slugify_filename(name) files = wrap(math.map(lambda dir_: _filename(dir_, name, frame), self._paths)) write(field, files)
Write a
Field
to a file. The filenames are created from the provided names and the frame index in accordance with the scene format specification at https://tum-pbs.github.io/PhiFlow/Scene_Format_Specification.html .Args
field
- single field or structure of Fields to save.
name
- Base file name.
frame
- Frame number as
int
, typically time step index.
class GeometryMask (geometry: phi.geom._geom.Geometry,
balance: phiml.math._tensors.Tensor | float = 0.5)-
Expand source code
class SoftGeometryMask(HardGeometryMask): """ Deprecated since version 2.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5): warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2) super().__init__(geometry) self.balance = balance def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor: return self.geometry.approximate_fraction_inside(geometry, self.balance) def __getitem__(self, item: dict): return SoftGeometryMask(self.geometry[item], self.balance)
Deprecated since version 2.3. Use
mask()
orresample()
instead.Ancestors
- phi.field._mask.HardGeometryMask
- phi.field._field.FieldInitializer
class SoftGeometryMask (geometry: phi.geom._geom.Geometry,
balance: phiml.math._tensors.Tensor | float = 0.5)-
Expand source code
class SoftGeometryMask(HardGeometryMask): """ Deprecated since version 2.3. Use `phi.field.mask()` or `phi.field.resample()` instead. """ def __init__(self, geometry: Geometry, balance: Union[Tensor, float] = 0.5): warnings.warn("HardGeometryMask and SoftGeometryMask are deprecated. Use field.mask or field.resample instead.", DeprecationWarning, stacklevel=2) super().__init__(geometry) self.balance = balance def _sample(self, geometry: Geometry, at: str, boundaries: Extrapolation, **kwargs) -> math.Tensor: return self.geometry.approximate_fraction_inside(geometry, self.balance) def __getitem__(self, item: dict): return SoftGeometryMask(self.geometry[item], self.balance)
Deprecated since version 2.3. Use
mask()
orresample()
instead.Ancestors
- phi.field._mask.HardGeometryMask
- phi.field._field.FieldInitializer