Module phi.geom

Differentiable geometry package.

Classes:

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

Functions

def as_sdf(geo: phi.geom._geom.Geometry,
bounds=None,
rel_margin=None,
abs_margin=0.0,
separate: str | Sequence | set | ForwardRef('Shape') | Callable | None = None,
method='auto') ‑> phi.geom._sdf.SDF
Expand source code
def as_sdf(geo: Geometry, bounds=None, rel_margin=None, abs_margin=0., separate: DimFilter = None, method='auto') -> SDF:
    """
    Represent existing geometry as a signed distance function.

    Args:
        geo: `Geometry` to represent as a signed distance function.
            Must implement `Geometry.approximate_signed_distance()`.
        bounds: Bounds of the SDF. If `None` will be determined from bounds of `geo` and `rel_margin`/`abs_margin`.
        rel_margin: Relative size to pad the domain on all sides around the bounds of `geo`.
            For example, 0.1 will pad 10% of `geo`'s size in each axis on both sides.
        abs_margin: World-space size to pad the domain on all sides around the bounds of `geo`.
        separate: Dimensions along which to unstack `geo` and return individual SDFs.
            Once created, SDFs cannot be unstacked.

    Returns:

    """
    separate = geo.shape.only(separate)
    if separate:
        return math.map(as_sdf, geo, bounds, rel_margin, abs_margin, separate=None, dims=separate, unwrap_scalars=True)
    if bounds is None:
        bounds: BaseBox = geo.bounding_box()
        rel_margin = .1 if rel_margin is None else rel_margin
    rel_margin = 0 if rel_margin is None else rel_margin
    bounds = Cuboid(bounds.center, half_size=bounds.half_size * (1 + 2 * rel_margin) + 2 * abs_margin)
    if isinstance(geo, SDF):
        return SDF(geo._sdf, geo._out_shape, bounds, geo._center, geo._volume, geo._bounding_radius)
    elif isinstance(geo, Mesh) and geo.spatial_rank == 3 and geo.element_rank == 2:  # 3D surface mesh
        method = 'closest-face' if method == 'auto' else method
        if method == 'pysdf':
            from pysdf import SDF as PySDF  # https://github.com/sxyu/sdf    https://github.com/sxyu/sdf/blob/master/src/sdf.cpp
            np_verts = geo.vertices.center.numpy('vertices,vector')
            np_tris = geo.elements._indices.numpy('cells,~vertices')
            np_sdf = PySDF(np_verts, np_tris)  # (num_vertices, 3) and (num_faces, 3)
            np_sdf_c = lambda x: np.clip(np_sdf(x), -float(bounds.size.min) / 2, float(bounds.size.max))
            return numpy_sdf(np_sdf_c, bounds, geo.bounding_box().center)
        elif method == 'closest-face':
            def sdf_closest_face(location):
                closest_elem = math.find_closest(geo.center, location)
                center = geo.center[closest_elem]
                normal = geo.normals[closest_elem]
                return plane_sgn_dist(center, normal, location)
            def sdf_and_grad(location):  # for close distances < face_size use normal vector, for far distances use distance from center
                closest_elem = math.find_closest(geo.center, location)
                center = geo.center[closest_elem]
                normal = geo.normals[closest_elem]
                face_size = math.sqrt(geo.volume) * 4
                size = face_size[closest_elem]
                sgn_dist = plane_sgn_dist(center, normal, location)
                outward = math.where(abs(sgn_dist) < size, normal, math.vec_normalize(location - center))
                return sgn_dist, outward
            return SDF(sdf_closest_face, math.EMPTY_SHAPE, bounds, geo.bounding_box().center, sdf_and_grad=sdf_and_grad)
        elif method == 'mesh-to-sdf':
            from mesh_to_sdf import mesh_to_sdf
            from trimesh import Trimesh
            np_verts = geo.vertices.center.numpy('vertices,vector')
            np_tris = geo.elements._indices.numpy('cells,~vertices')
            trimesh = Trimesh(np_verts, np_tris)
            def np_sdf(points):
                return mesh_to_sdf(trimesh, points, surface_point_method='scan', sign_method='normal')
            return numpy_sdf(np_sdf, bounds, geo.bounding_box().center)
        else:
            raise ValueError(f"Method '{method}' not implemented for Mesh SDF")
    def sdf_and_grad(x: Tensor):
        sgn_dist, delta, *_ = geo.approximate_closest_surface(x)
        return sgn_dist, math.vec_normalize(-delta)
    return SDF(geo.approximate_signed_distance, geo.shape.non_instance.without('vector'), bounds, geo.center, geo.volume, geo.bounding_radius(), sdf_and_grad)

Represent existing geometry as a signed distance function.

Args

geo
Geometry to represent as a signed distance function. Must implement Geometry.approximate_signed_distance().
bounds
Bounds of the SDF. If None will be determined from bounds of geo and rel_margin/abs_margin.
rel_margin
Relative size to pad the domain on all sides around the bounds of geo. For example, 0.1 will pad 10% of geo's size in each axis on both sides.
abs_margin
World-space size to pad the domain on all sides around the bounds of geo.
separate
Dimensions along which to unstack geo and return individual SDFs. Once created, SDFs cannot be unstacked.

Returns:

def assert_same_rank(rank1, rank2, error_message)
Expand source code
def assert_same_rank(rank1, rank2, error_message):
    """ Tests that two objects have the same spatial rank. Objects can be of types: `int`, `None` (no check), `Geometry`, `Shape`, `Tensor` """
    rank1_, rank2_ = _rank(rank1), _rank(rank2)
    if rank1_ is not None and rank2_ is not None:
        assert rank1_ == rank2_, 'Ranks do not match: %s and %s. %s' % (rank1_, rank2_, error_message)

Tests that two objects have the same spatial rank. Objects can be of types: int, None (no check), Geometry, Shape, Tensor

def bounding_box(geometry: phi.geom._geom.Geometry | phiml.math._tensors.Tensor,
reduce=<function non_batch>) ‑> phi.geom._box.Box
Expand source code
def bounding_box(geometry: Geometry | Tensor, reduce=non_batch) -> Box:
    """
    Builds a bounding box around `geometry` or a collection of points.

    Args:
        geometry: `Geometry` object or `Tensor` of points.
        reduce: Which objects to includes in each bounding box. Non-reduced dims will be part of the returned box.

    Returns:
        Bounding `Box` containing only batch dims and `vector`.
    """
    if isinstance(geometry, Tensor):
        assert 'vector' in geometry.shape, f"When passing a Tensor to bounding_box, it needs to have a vector dimension but got {geometry.shape}"
        reduce = geometry.shape.only(reduce) - 'vector'
        return Box(math.min(geometry, reduce), math.max(geometry, reduce))
    center = geometry.center
    extent = geometry.bounding_half_extent()
    boxes = Box(lower=center - extent, upper=center + extent)
    return boxes.largest(boxes.shape.only(reduce)-'vector')

Builds a bounding box around geometry or a collection of points.

Args

geometry
Geometry object or Tensor of points.
reduce
Which objects to includes in each bounding box. Non-reduced dims will be part of the returned box.

Returns

Bounding Box containing only batch dims and vector.

def build_mesh(bounds: phi.geom._box.Box = None,
resolution=(),
obstacles: phi.geom._geom.Geometry | Dict[str, phi.geom._geom.Geometry] = None,
method='quad',
cell_dim: phiml.math._shape.Shape = (cellsⁱ=None),
face_format: str = 'csc',
max_squish: float | None = 0.5,
**resolution_: int | phiml.math._tensors.Tensor | tuple | list | Any) ‑> phi.geom._mesh.Mesh
Expand source code
def build_mesh(bounds: Box = None,
               resolution=EMPTY_SHAPE,
               obstacles: Union[Geometry, Dict[str, Geometry]] = None,
               method='quad',
               cell_dim: Shape = instance('cells'),
               face_format: str = 'csc',
               max_squish: Optional[float] = .5,
               **resolution_: Union[int, Tensor, tuple, list, Any]) -> Mesh:
    """
    Build a mesh for a given domain, respecting obstacles.

    Args:
        bounds: Bounds for uniform cells.
        resolution: Base resolution
        obstacles: Single `Geometry` or `dict` mapping boundary name to corresponding `Geometry`.
        method: Meshing algorithm. Only `quad` is currently supported.
        cell_dim: Dimension along which to list the cells. This should be an instance dimension.
        face_format: Sparse storage format for cell connectivity.
        max_squish: Smallest allowed cell size compared to the smallest regular cell.
        **resolution_: For uniform grid, pass resolution as `int` and specify `bounds`.
            Or pass a sequence of floats for each dimension, specifying the vertex positions along each axis.
            This allows for variable cell stretching.

    Returns:
        `Mesh`
    """
    if obstacles is None:
        obstacles = {}
    elif isinstance(obstacles, Geometry):
        obstacles = {'obstacle': obstacles}
    assert isinstance(obstacles, dict), f"obstacles needs to be a Geometry or dict"
    if method == 'quad':
        if bounds is None:  # **resolution_ specifies points
            assert not resolution, f"When specifying vertex positions, bounds and resolution will be inferred and must not be specified."
            resolution = spatial(**{dim: non_batch(x).volume for dim, x in resolution_.items()}) - 1
            vert_pos = meshgrid(**resolution_)
            bounds = Box(**{dim: (x[0], x[-1]) for dim, x in resolution_.items()})
            # centroid_x = {dim: .5 * (wrap(x[:-1]) + wrap(x[1:])) for dim, x in resolution_.items()}
            # centroids = meshgrid(**centroid_x)
        else:  # uniform grid from bounds, resolution
            resolution = resolution & spatial(**resolution_)
            vert_pos = meshgrid(resolution + 1) / resolution * bounds.size + bounds.lower
            # centroids = UniformGrid(resolution, bounds).center
        dx = bounds.size / resolution
        regular_size = math.min(dx, channel)
        vert_pos, polygons, boundaries = build_quadrilaterals(vert_pos, resolution, obstacles, bounds, regular_size * max_squish)
        if max_squish is not None:
            lin_vert_pos = pack_dims(vert_pos, spatial, instance('polygon'))
            corner_pos = lin_vert_pos[polygons]
            min_pos = math.min(corner_pos, '~polygon')
            max_pos = math.max(corner_pos, '~polygon')
            cell_sizes = math.min(max_pos - min_pos, 'vector')
            too_small = cell_sizes < regular_size * max_squish
            # --- remove too small cells ---
            removed = polygons[too_small]
            removed_centers = mean(lin_vert_pos[removed], '~polygon')
            kept_vert = removed[{'~polygon': 0}]
            vert_pos = scatter(lin_vert_pos, kept_vert, removed_centers)
            vertex_map = math.range(non_channel(lin_vert_pos))
            vertex_map = scatter(vertex_map, rename_dims(removed, '~polygon', instance('poly_list')), expand(kept_vert, instance(poly_list=4)))
            polygons = polygons[~too_small]
            polygons = vertex_map[polygons]
            boundaries = {boundary: vertex_map[edge_list] for boundary, edge_list in boundaries.items()}
            boundaries = {boundary: edge_list[edge_list[{'~vert': 'start'}] != edge_list[{'~vert': 'end'}]] for boundary, edge_list in boundaries.items()}
            # ToDo remove edges which now point to the same vertex
        def build_single_mesh(vert_pos, polygons, boundaries):
            points_np = reshaped_numpy(vert_pos, [..., channel])
            polygon_list = reshaped_numpy(polygons, [..., dual])
            boundaries = {b: edges.numpy('edges,~vert') for b, edges in boundaries.items()}
            return mesh_from_numpy(points_np, polygon_list, boundaries, cell_dim=cell_dim, face_format=face_format)
        return math.map(build_single_mesh, vert_pos, polygons, boundaries, dims=batch)

Build a mesh for a given domain, respecting obstacles.

Args

bounds
Bounds for uniform cells.
resolution
Base resolution
obstacles
Single Geometry or dict mapping boundary name to corresponding Geometry.
method
Meshing algorithm. Only quad is currently supported.
cell_dim
Dimension along which to list the cells. This should be an instance dimension.
face_format
Sparse storage format for cell connectivity.
max_squish
Smallest allowed cell size compared to the smallest regular cell.
**resolution_
For uniform grid, pass resolution as int and specify bounds. Or pass a sequence of floats for each dimension, specifying the vertex positions along each axis. This allows for variable cell stretching.

Returns

Mesh

def clip_length(vec: phiml.math._tensors.Tensor,
min_len=0,
max_len=1,
vec_dim: str | Sequence | set | ForwardRef('Shape') | Callable | None = 'vector',
eps: phiml.math._tensors.Tensor | float = 1e-05)
Expand source code
def clip_length(vec: Tensor, min_len=0, max_len=1, vec_dim: DimFilter = 'vector', eps: Union[float, Tensor] = 1e-5):
    """
    Clips the length of a vector to the interval `[min_len, max_len]` while keeping the direction.
    Zero-vectors remain zero-vectors.

    Args:
        vec: `Tensor`
        min_len: Lower clipping threshold.
        max_len: Upper clipping threshold.
        vec_dim: Dimensions to compute the length over. By default, all channel dimensions are used to compute the vector length.
        eps: Minimum vector length. Use to avoid `inf` gradients for zero-length vectors.

    Returns:
        `Tensor` with same shape as `vec`.
    """
    le = math.length(vec, vec_dim, eps)
    new_length = clip(le, min_len, max_len)
    return vec * safe_div(new_length, le)

Clips the length of a vector to the interval [min_len, max_len] while keeping the direction. Zero-vectors remain zero-vectors.

Args

vec
Tensor
min_len
Lower clipping threshold.
max_len
Upper clipping threshold.
vec_dim
Dimensions to compute the length over. By default, all channel dimensions are used to compute the vector length.
eps
Minimum vector length. Use to avoid inf gradients for zero-length vectors.

Returns

Tensor with same shape as vec.

def concat(values: Sequence[~PhiTreeNodeType],
dim: phiml.math._shape.Shape | str,
/,
expand_values=False,
**kwargs) ‑> ~PhiTreeNodeType
Expand source code
def concat(values: Sequence[PhiTreeNodeType], dim: Union[str, Shape], /, expand_values=False, **kwargs) -> PhiTreeNodeType:
    """
    Concatenates a sequence of `phiml.math.magic.Shapable` objects, e.g. `Tensor`, along one dimension.
    All values must have the same spatial, instance and channel dimensions and their sizes must be equal, except for `dim`.
    Batch dimensions will be added as needed.

    Args:
        values: Tuple or list of `phiml.math.magic.Shapable`, such as `phiml.math.Tensor`
        dim: Concatenation dimension, must be present in all `values`.
            The size along `dim` is determined from `values` and can be set to undefined (`None`).
            Alternatively, a `str` of the form `'t->name:t'` can be specified, where `t` is on of `b d i s c` denoting the dimension type.
            This first packs all dimensions of the input into a new dim with given name and type, then concatenates the values along this dim.
        expand_values: If `True`, will first add missing dimensions to all values, not just batch dimensions.
            This allows tensors with different dimensions to be concatenated.
            The resulting tensor will have all dimensions that are present in `values`.
        **kwargs: Additional keyword arguments required by specific implementations.
            Adding spatial dimensions to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions.
            Adding batch dimensions must always work without keyword arguments.

    Returns:
        Concatenated `Tensor`

    Examples:
        >>> concat([math.zeros(batch(b=10)), math.ones(batch(b=10))], 'b')
        (bᵇ=20) 0.500 ± 0.500 (0e+00...1e+00)

        >>> concat([vec(x=1, y=0), vec(z=2.)], 'vector')
        (x=1.000, y=0.000, z=2.000) float64
    """
    assert len(values) > 0, f"concat() got empty sequence {values}"
    if isinstance(dim, Shape):
        dim = dim.name
    assert isinstance(dim, str), f"dim must be a str or Shape but got '{dim}' of type {type(dim)}"
    if '->' in dim:
        dim_type, dim = [s.strip() for s in dim.split('->', 1)]
        dim_type = DIM_FUNCTIONS[INV_CHAR[dim_type]]
        dim = auto(dim, dim_type)
        values = [pack_dims(v, dim_type, dim) for v in values]
        dim = dim.name
    else:
        dim = auto(dim, channel).name
    # Add missing dimensions
    if expand_values:
        all_dims = merge_shapes(*values, allow_varying_sizes=True)
        all_dims = all_dims.with_dim_size(dim, 1, keep_item_names=False)
        values = [expand(v, all_dims.without(shape(v))) for v in values]
    else:
        for v in values:
            assert dim in shape(v), f"concat dim '{dim}' must be present in the shapes of all values bot got value {type(v).__name__} with shape {shape(v)}"
        for v in values[1:]:
            assert set(non_batch(v).names) == set(non_batch(values[0]).names), f"Concatenated values must have the same non-batch dimensions but got {non_batch(values[0])} and {non_batch(v)}"
        all_batch_dims = merge_shapes(*[shape(v).batch.without(dim) for v in values])
        values = [expand(v, all_batch_dims) for v in values]
    # --- First try __concat__ ---
    for v in values:
        if isinstance(v, Shapable):
            if hasattr(v, '__concat__'):
                result = v.__concat__(values, dim, **kwargs)
                if result is not NotImplemented:
                    assert isinstance(result, Shapable), f"__concat__ must return a Shapable object but got {type(result).__name__} from {type(v).__name__} {v}"
                    return result
    # --- Next: try concat attributes for tree nodes ---
    if all(isinstance(v, PhiTreeNode) for v in values):
        attributes = all_attributes(values[0])
        if attributes and all(all_attributes(v) == attributes for v in values):
            new_attrs = {}
            for a in attributes:
                common_shape = merge_shapes(*[shape(getattr(v, a)).without(dim) for v in values])
                a_values = [expand(getattr(v, a), common_shape & shape(v).only(dim)) for v in values]  # expand by dim if missing, and dims of others
                new_attrs[a] = concat(a_values, dim, **kwargs)
            return copy_with(values[0], **new_attrs)
        else:
            warnings.warn(f"Failed to concat values using value attributes because attributes differ among values {values}")
    # --- Fallback: slice and stack ---
    try:
        unstacked = sum([unstack(v, dim) for v in values], ())
    except MagicNotImplemented:
        raise MagicNotImplemented(f"concat: No value implemented __concat__ and not all values were Sliceable along {dim}. values = {[type(v) for v in values]}")
    if len(unstacked) > 8:
        warnings.warn(f"concat() default implementation is slow on large dimensions ({dim}={len(unstacked)}). Please implement __concat__()", RuntimeWarning, stacklevel=2)
    dim = shape(values[0])[dim].with_size(None)
    try:
        return stack(unstacked, dim, **kwargs)
    except MagicNotImplemented:
        raise MagicNotImplemented(f"concat: No value implemented __concat__ and slices could not be stacked. values = {[type(v) for v in values]}")

Concatenates a sequence of phiml.math.magic.Shapable objects, e.g. Tensor, along one dimension. All values must have the same spatial, instance and channel dimensions and their sizes must be equal, except for dim. Batch dimensions will be added as needed.

Args

values
Tuple or list of phiml.math.magic.Shapable, such as phiml.math.Tensor
dim
Concatenation dimension, must be present in all values. The size along dim is determined from values and can be set to undefined (None). Alternatively, a str of the form 't->name:t' can be specified, where t is on of b d i s c denoting the dimension type. This first packs all dimensions of the input into a new dim with given name and type, then concatenates the values along this dim.
expand_values
If True, will first add missing dimensions to all values, not just batch dimensions. This allows tensors with different dimensions to be concatenated. The resulting tensor will have all dimensions that are present in values.
**kwargs
Additional keyword arguments required by specific implementations. Adding spatial dimensions to fields requires the bounds: Box argument specifying the physical extent of the new dimensions. Adding batch dimensions must always work without keyword arguments.

Returns

Concatenated Tensor

Examples

>>> concat([math.zeros(batch(b=10)), math.ones(batch(b=10))], 'b')
(bᵇ=20) 0.500 ± 0.500 (0e+00...1e+00)
>>> concat([vec(x=1, y=0), vec(z=2.)], 'vector')
(x=1.000, y=0.000, z=2.000) float64
def cross(vec1: phiml.math._tensors.Tensor, vec2: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def cross(vec1: Tensor, vec2: Tensor) -> Tensor:
    """
    Computes the cross product of two vectors in 2D.

    Args:
        vec1: `Tensor` with a single channel dimension called `'vector'`
        vec2: `Tensor` with a single channel dimension called `'vector'`

    Returns:
        `Tensor`
    """
    vec1 = math.tensor(vec1)
    vec2 = math.tensor(vec2)
    spatial_rank = vec1.vector.size if 'vector' in vec1.shape else vec2.vector.size
    if spatial_rank == 2:  # Curl in 2D
        assert vec2.vector.exists
        if vec1.vector.exists:
            v1_x, v1_y = vec1.vector
            v2_x, v2_y = vec2.vector
            return v1_x * v2_y - v1_y * v2_x
        else:
            v2_x, v2_y = vec2.vector
            return vec1 * stack([-v2_y, v2_x], channel(vec2))
    elif spatial_rank == 3:  # Curl in 3D
        assert vec1.vector.exists and vec2.vector.exists, f"Both vectors must have a 'vector' dimension but got shapes {vec1.shape}, {vec2.shape}"
        v1_x, v1_y, v1_z = vec1.vector
        v2_x, v2_y, v2_z = vec2.vector
        return math.stack([
            v1_y * v2_z - v1_z * v2_y,
            v1_z * v2_x - v1_x * v2_z,
            v1_x * v2_y - v1_y * v2_x,
        ], vec1.shape['vector'])
    else:
        raise AssertionError(f'dims = {spatial_rank}. Vector product not available in > 3 dimensions')

Computes the cross product of two vectors in 2D.

Args

vec1
Tensor with a single channel dimension called 'vector'
vec2
Tensor with a single channel dimension called 'vector'

Returns

Tensor

def cylinder(center: phiml.math._tensors.Tensor | float = None,
radius: phiml.math._tensors.Tensor | float = None,
depth: phiml.math._tensors.Tensor | float = None,
rotation: phiml.math._tensors.Tensor | None = None,
axis: int | str | phiml.math._tensors.Tensor = -1,
variables=('center', 'radius', 'depth', 'rotation'),
**center_: phiml.math._tensors.Tensor | float) ‑> phi.geom._cylinder.Cylinder
Expand source code
def cylinder(center: Union[Tensor, float] = None,
             radius: Union[float, Tensor] = None,
             depth: Union[float, Tensor] = None,
             rotation: Optional[Tensor] = None,
             axis: int | str | Tensor = -1,
             variables=('center', 'radius', 'depth', 'rotation'),
             **center_: Union[float, Tensor]) -> Cylinder:
    """
    Args:
        center: Cylinder center as `Tensor` with `vector` dimension.
            The spatial dimension order should be specified in the `vector` dimension via item names.
            Can be left empty to specify dimensions via kwargs.
        radius: Cylinder radius as `float` or `Tensor`.
        depth: Cylinder length as `float` or `Tensor`.
        rotation: Rotation angle(s) or rotation matrix.
        axis: The cylinder is aligned along this axis, perturbed by `rotation`.
            Specified either as the dim along which the cylinder is aligned or as a vector.
        variables: Which properties of the cylinder are variable, i.e. traced and optimizable. All by default.
        **center_: Specifies center when the `center` argument is not given. Center position by dimension, e.g. `x=0.5, y=0.2`.
    """
    if center is not None:
        if not isinstance(center, Tensor):
            assert center == 0 and isinstance(axis, Tensor)
            center = expand(0, axis.shape['vector'])
        assert isinstance(center, Tensor), f"center must be a Tensor but got {type(center).__name__}"
        assert 'vector' in center.shape, f"Sphere center must have a 'vector' dimension."
        assert center.shape.get_item_names('vector') is not None, f"Vector dimension must list spatial dimensions as item names. Use the syntax Sphere(x=x, y=y) to assign names."
        center = center
    else:
        center = wrap(tuple(center_.values()), channel(vector=tuple(center_.keys())))
    assert radius is not None, "radius must be specified"
    radius = wrap(radius)
    if depth is None:
        assert isinstance(axis, Tensor)
        depth = 2 * length(axis, 'vector')
    else:
        depth = wrap(depth)
    axis = center.vector.item_names[axis] if isinstance(axis, int) else axis
    if isinstance(axis, Tensor):  # specify cylinder axis as vector
        assert 'vector' in axis.shape, f"When specifying axis a Tensor, it must have a 'vector' dimension."
        assert rotation is None, f"When specifying axis as a "
        axis_ = center.vector.item_names[-1]
        unit_vec = vec(**{d: 1 if d == axis_ else 0 for d in center.vector.item_names})
        rotation = rotation_matrix_from_directions(unit_vec, axis, epsilon=1e-5)
        axis = axis_
    else:
        rotation = rotation_matrix(rotation)
    variables = [{'center': '_center'}.get(v, v) for v in variables]
    assert 'vector' not in radius.shape, f"Cylinder radius must not vary along vector but got {radius}"
    assert set(variables).issubset(set(all_attributes(Cylinder))), f"Invalid variables: {variables}"
    assert axis in center.vector.item_names, f"Cylinder axis {axis} not part of vector dim {center.vector}"
    return Cylinder(center, radius, depth, rotation, axis, tuple(variables), ())

Args

center
Cylinder center as Tensor with vector dimension. The spatial dimension order should be specified in the vector dimension via item names. Can be left empty to specify dimensions via kwargs.
radius
Cylinder radius as float or Tensor.
depth
Cylinder length as float or Tensor.
rotation
Rotation angle(s) or rotation matrix.
axis
The cylinder is aligned along this axis, perturbed by rotation. Specified either as the dim along which the cylinder is aligned or as a vector.
variables
Which properties of the cylinder are variable, i.e. traced and optimizable. All by default.
**center_
Specifies center when the center argument is not given. Center position by dimension, e.g. x=0.5, y=0.2.
def embed(geometry: phi.geom._geom.Geometry,
projected_dims: phiml.math._shape.Shape | str | tuple | list | None) ‑> phi.geom._geom.Geometry
Expand source code
def embed(geometry: Geometry, projected_dims: Union[math.Shape, str, tuple, list, None]) -> Geometry:
    """
    Adds fake spatial dimensions to a geometry.
    The geometry value will be constant along the added dimensions, as if it had infinite length in these directions.

    Dimensions that are already present with `geometry` are ignored.

    Args:
        geometry: `Geometry`
        projected_dims: Additional dimensions

    Returns:
        `Geometry` with spatial rank `geometry.spatial_rank + projected_dims.rank`.
    """
    if projected_dims is None:
        return geometry
    axes = parse_dim_order(projected_dims)
    embedded_axes = [a for a in axes if a not in geometry.shape.get_item_names('vector')]
    if not embedded_axes:
        return geometry[axes]
    # --- add dims from geometry to axes ---
    for name in reversed(geometry.shape.get_item_names('vector')):
        if name not in projected_dims:
            axes = (name,) + axes
    if isinstance(geometry, BaseBox):
        box = geometry.corner_representation()
        embedded = box * Box(**{dim: None for dim in embedded_axes})
        return embedded[axes]
    return _EmbeddedGeometry(geometry, axes)

Adds fake spatial dimensions to a geometry. The geometry value will be constant along the added dimensions, as if it had infinite length in these directions.

Dimensions that are already present with geometry are ignored.

Args

geometry
Geometry
projected_dims
Additional dimensions

Returns

Geometry with spatial rank geometry.spatial_rank + projected_dims.rank.

def enclosing_grid(*geometries: phi.geom._geom.Geometry,
voxel_count: int,
rel_margin=0.0,
abs_margin=0.0) ‑> phi.geom._grid.UniformGrid
Expand source code
def enclosing_grid(*geometries: Geometry, voxel_count: int, rel_margin=0., abs_margin=0.) -> UniformGrid:
    """
    Constructs a `UniformGrid` which fully encloses the `geometries`.
    The grid voxels are chosen to have approximately the same size along each axis.

    Args:
        *geometries: `Geometry` objects which should lie within the grid.
        voxel_count: Approximate number of total voxels.
        rel_margin: Relative margin, i.e. empty space on each side as a fraction of the bounding box size of `geometries`.
        abs_margin: Absolute margin, i.e. empty space on each side.

    Returns:
        `UniformGrid`
    """
    bounds = stack([g.bounding_box() for g in geometries], batch('_geometries'))
    bounds = bounds.largest(shape).scaled(1+rel_margin)
    bounds = Box(bounds.lower - abs_margin, bounds.upper + abs_margin)
    voxel_vol = bounds.volume / voxel_count
    voxel_size = voxel_vol ** (1/bounds.spatial_rank)
    resolution = math.to_int32(math.round(bounds.size / voxel_size))
    resolution = spatial(**resolution.vector)
    return UniformGrid(resolution, bounds)

Constructs a UniformGrid which fully encloses the geometries. The grid voxels are chosen to have approximately the same size along each axis.

Args

*geometries
Geometry objects which should lie within the grid.
voxel_count
Approximate number of total voxels.
rel_margin
Relative margin, i.e. empty space on each side as a fraction of the bounding box size of geometries.
abs_margin
Absolute margin, i.e. empty space on each side.

Returns

UniformGrid

def graph(nodes: phi.geom._geom.Geometry | phiml.math._tensors.Tensor,
edges: phiml.math._tensors.Tensor,
boundary: Dict[str, Dict[str, slice]] = None,
build_distances=True,
build_bounding_distance=False) ‑> phi.geom._graph.Graph
Expand source code
def graph(nodes: Union[Geometry, Tensor],
          edges: Tensor,
          boundary: Dict[str, Dict[str, slice]] = None,
          build_distances=True,
          build_bounding_distance=False) -> Graph:
    """
    Construct a `Graph`.

    Args:
        nodes: Location `Tensor` or `Geometry` objects representing the nodes.
        edges: Connectivity and edge value `Tensor`.
        boundary: Named boundary sets.
        build_distances: Whether to compute all edge lengths.
            This enables the properties `Graph.deltas`, `Graph.unit_deltas`, `Graph.distances`.
        build_bounding_distance: Whether to compute the maximum edge length.
            This enables the property `Graph.bounding_distance`.

    Returns:
        `Graph`
    """
    if isinstance(nodes, Tensor):
        assert 'vector' in channel(nodes) and channel(nodes).get_item_names('vector') is not None, f"nodes must have a 'vector' dim listing the physical dimensions but got {shape(nodes)}"
        nodes = Point(nodes)
    boundary = {} if boundary is None else boundary
    deltas = math.pairwise_distances(nodes.center, format=edges) if build_distances else None
    distances = math.vec_length(deltas) if build_distances else None
    bound = math.max(distances) if build_bounding_distance else None
    return Graph(nodes, edges, boundary, deltas, distances, bound)

Construct a Graph.

Args

nodes
Location Tensor or Geometry objects representing the nodes.
edges
Connectivity and edge value Tensor.
boundary
Named boundary sets.
build_distances
Whether to compute all edge lengths. This enables the properties Graph.deltas, Graph.unit_deltas, Graph.distances.
build_bounding_distance
Whether to compute the maximum edge length. This enables the property Graph.bounding_distance.

Returns

Graph

def infinite_cylinder(center=None,
radius=None,
inf_dim: str | phiml.math._shape.Shape | tuple | list = None,
**center_) ‑> phi.geom._geom.Geometry
Expand source code
def infinite_cylinder(center=None, radius=None, inf_dim: Union[str, Shape, tuple, list] = None, **center_) -> Geometry:
    """
    Creates an infinite cylinder.
    This is equal to embedding an `n`-dimensional `Sphere` in `n+1` dimensions.

    See Also:
        `Sphere`, `embed`

    Args:
        center: Center coordinates without `inf_dim`. Alternatively use keyword arguments.
        radius: Cylinder radius.
        inf_dim: Dimension along which the cylinder is infinite.
            Use `Geometry.rotated()` if the direction does not align with an axis.
        **center_: Alternatively specify center coordinates without `inf_dim` as keyword arguments.

    Returns:
        `Geometry`
    """
    sphere = Sphere(center, radius, **center_)
    return embed(sphere, inf_dim)

Creates an infinite cylinder. This is equal to embedding an n-dimensional Sphere in n+1 dimensions.

See Also: Sphere, embed()

Args

center
Center coordinates without inf_dim. Alternatively use keyword arguments.
radius
Cylinder radius.
inf_dim
Dimension along which the cylinder is infinite. Use Geometry.rotated() if the direction does not align with an axis.
**center_
Alternatively specify center coordinates without inf_dim as keyword arguments.

Returns

Geometry

def intersection(*geometries: phi.geom._geom.Geometry, dim=(intersectionⁱ=None)) ‑> phi.geom._geom.Geometry
Expand source code
def intersection(*geometries: Geometry, dim=instance('intersection')) -> Geometry:
    """
    Intersection of the given geometries.
    A point lies inside the union if it lies within all of the geometries.

    Args:
        *geometries: arbitrary geometries with same spatial dims. Arbitrary batch dims are allowed.
        dim: Intersection dimension. This must be an instance dimension.

    Returns:
        intersection `Geometry`
    """
    return _stack_geometries(geometries, 'intersection', dim)

Intersection of the given geometries. A point lies inside the union if it lies within all of the geometries.

Args

*geometries
arbitrary geometries with same spatial dims. Arbitrary batch dims are allowed.
dim
Intersection dimension. This must be an instance dimension.

Returns

intersection Geometry

def invert(geometry: phi.geom._geom.Geometry)
Expand source code
def invert(geometry: Geometry):
    """
    Swaps inside and outside.

    Args:
        geometry: `phi.geom.Geometry` to swap

    Returns:
        New `phi.geom.Geometry` object with same surface but swapped normals
    """
    return ~geometry

Swaps inside and outside.

Args

geometry
Geometry to swap

Returns

New Geometry object with same surface but swapped normals

def length(obj: phi.geom._geom.Geometry | phiml.math._tensors.Tensor, epsilon=1e-05) ‑> phiml.math._tensors.Tensor
Expand source code
def length(obj: Geometry | Tensor, epsilon=1e-5) -> Tensor:
    """
    Returns the length of a vector `Tensor` or geometric object with a length-like property.

    Args:
        obj:
        epsilon: Minimum valid vector length. Use to avoid `inf` gradients for zero-length vectors.
            Lengths shorter than `eps` are set to 0.

    Returns:
        Length as `Tensor`
    """
    if isinstance(obj, Tensor):
        assert 'vector' in obj.shape, f"length() requires 'vector' dim but got {type(obj)} with shape {shape(obj)}."
        return math.length(obj, 'vector', epsilon)
    elif isinstance(obj, Cylinder):
        return obj.depth
    raise ValueError(obj)

Returns the length of a vector Tensor or geometric object with a length-like property.

Args

obj:
epsilon
Minimum valid vector length. Use to avoid inf gradients for zero-length vectors. Lengths shorter than eps are set to 0.

Returns

Length as Tensor

def line_trace(geo: phi.geom._geom.Geometry,
origin: phiml.math._tensors.Tensor,
direction: phiml.math._tensors.Tensor,
side='both',
tolerance=None,
max_iter=64,
step_size=0.9,
max_line_length=None) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor | None]
Expand source code
def line_trace(geo: Geometry, origin: Tensor, direction: Tensor, side='both', tolerance=None, max_iter=64, step_size=.9, max_line_length=None) -> Tuple[Tensor, Tensor, Tensor, Tensor, Optional[Tensor]]:
    """
    Trace a line until it hits the surface of `geo`.
    The surface can be hit either from the outside or the inside.

    Args:
        geo: `Geometry` that implements `approximate_closest_surface`.
        origin: Line start location.
        direction: Unit vector pointing in the line direction.
        side: 'outside' or 'inside' or 'both'.
        tolerance: Surface distance tolerance.
        max_iter: Maximum number of steps per line.
        step_size: Step size factor. This can be set to `1` if the signed distance values of `geo` are exact.
            For inexact SDFs, smaller step sizes prevent skipping over surfaces.

    Returns:
        hit: Whether a surface intersection was found for the line.
        distance: Distance between the line and the surface.
        position: Hit location or point until which the line was traced.
        normal: Surface normal at hit location
        hit_index: Geometry face index at hit location
    """
    assert side in ['outside', 'inside', 'both'], f"{side} is not a valid side"
    if tolerance is None:
        tolerance = 1e-4 * geo.bounding_box().size.min
    walked = 0
    has_hit = False
    initial_sdf = None
    last_sdf = None
    has_crossed = wrap(False)
    for i in range(max_iter):
        sgn_dist, delta, normal, _, face_index = geo.approximate_closest_surface(origin + walked * direction)
        initial_sdf = sgn_dist if initial_sdf is None else initial_sdf
        normal_dot_direction = normal.vector @ direction.vector
        intersection = (normal.vector @ delta.vector) / normal_dot_direction
        intersection = math.where(math.is_nan(intersection), math.INF, intersection)
        if side == 'both':
            can_hit_surface = True
        elif side == 'outside':
            can_hit_surface = normal_dot_direction <= 0
        else:
            can_hit_surface = normal_dot_direction >= 0
        intersection = math.where(intersection < math.where(can_hit_surface, -tolerance, 0), math.INF, intersection)  # surface behind us
        if last_sdf is not None:
            if side == 'both':
                has_crossed = (sgn_dist * last_sdf < 0) | (abs(sgn_dist) <= tolerance)
            elif side == 'outside':
                has_crossed = (last_sdf > tolerance) & (sgn_dist <= tolerance)
                has_crossed |= (last_sdf > 0) & (sgn_dist <= 0)
            else:
                has_crossed = (last_sdf < -tolerance) & (sgn_dist >= -tolerance)
                has_crossed |= (last_sdf < 0) & (sgn_dist >= 0)
        has_hit |= has_crossed
        max_walk = math.minimum(abs(sgn_dist), intersection)
        max_walk = math.where(can_hit_surface, step_size * max_walk, max_walk + tolerance)  # jump over surface if we can't hit it
        max_walk = math.where(has_hit, 0, max_walk)
        walked += max_walk
        is_done = has_hit.all if max_line_length is None else (has_hit | walked > max_line_length).all
        if is_done:
            break
        last_sdf = sgn_dist
        # trj.append(walked)
        # if i == 15:
        #     from phi.vis import show
        #     trj = stack(trj, instance('trj'))
        #     show(geo, trj, overlay='args')
    else:
        warnings.warn(f"thickness reached maximum iterations {max_iter}", RuntimeWarning, stacklevel=2)
    return has_hit, walked, origin + walked * direction, normal, face_index

Trace a line until it hits the surface of geo. The surface can be hit either from the outside or the inside.

Args

geo
Geometry that implements approximate_closest_surface.
origin
Line start location.
direction
Unit vector pointing in the line direction.
side
'outside' or 'inside' or 'both'.
tolerance
Surface distance tolerance.
max_iter
Maximum number of steps per line.
step_size
Step size factor. This can be set to 1 if the signed distance values of geo are exact. For inexact SDFs, smaller step sizes prevent skipping over surfaces.

Returns

hit
Whether a surface intersection was found for the line.
distance
Distance between the line and the surface.
position
Hit location or point until which the line was traced.
normal
Surface normal at hit location
hit_index
Geometry face index at hit location
def load_gmsh(file: str,
boundary_names: Sequence[str] = None,
periodic: str = None,
cell_dim=(cellsⁱ=None),
face_format: str = 'csc')
Expand source code
@broadcast
def load_gmsh(file: str, boundary_names: Sequence[str] = None, periodic: str = None, cell_dim=instance('cells'), face_format: str = 'csc'):
    """
    Load an unstructured mesh from a `.msh` file.

    This requires the package `meshio` to be installed.

    Args:
        file: Path to `.su2` file.
        boundary_names: Boundary identifiers corresponding to the blocks in the file. If not specified, boundaries will be numbered.
        periodic:
        cell_dim: Dimension along which to list the cells. This should be an instance dimension.
        face_format: Sparse storage format for cell connectivity.

    Returns:
        `Mesh`
    """
    import meshio
    from meshio import Mesh
    mesh: Mesh = meshio.read(file)
    dim = max([c.dim for c in mesh.cells])
    if dim == 2 and mesh.points.shape[-1] == 3:
        points = mesh.points[..., :2]
    else:
        assert dim == 3, f"Only 2D and 3D meshes are supported but got {dim} in {file}"
        points = mesh.points
    elements = []
    boundaries = {}
    for cell_block in mesh.cells:
        if cell_block.dim == dim:  # cells
            elements.extend(cell_block.data)
        elif cell_block.dim == dim - 1:
            # derive name from cell_block.tags if present?
            boundary = str(len(boundaries)) if boundary_names is None else boundary_names[len(boundaries)]
            boundaries[boundary] = cell_block.data
        else:
            raise AssertionError(f"Illegal cell block of type {cell_block.type} for {dim}D mesh")
    return mesh_from_numpy(points, elements, boundaries, periodic=periodic, cell_dim=cell_dim, face_format=face_format)

Load an unstructured mesh from a .msh file.

This requires the package meshio to be installed.

Args

file
Path to .su2 file.
boundary_names
Boundary identifiers corresponding to the blocks in the file. If not specified, boundaries will be numbered.
periodic:
cell_dim
Dimension along which to list the cells. This should be an instance dimension.
face_format
Sparse storage format for cell connectivity.

Returns

Mesh

def load_stl(file: str, face_dim=(facesⁱ=None)) ‑> phi.geom._mesh.Mesh
Expand source code
@broadcast
def load_stl(file: str, face_dim=instance('faces')) -> Mesh:
    """
    Load a triangle `Mesh` from an STL file.

    Args:
        file: File path to `.stl` file.
        face_dim: Instance dim along which to list the triangles.

    Returns:
        `Mesh` with `spatial_rank=3` and `element_rank=2`.
    """
    import stl
    model = stl.mesh.Mesh.from_file(file)
    points = np.reshape(model.points, (-1, 3))
    vertices, indices = np.unique(points, axis=0, return_inverse=True)
    indices = np.reshape(indices, (-1, 3))
    mesh = mesh_from_numpy(vertices, indices, element_rank=2, cell_dim=face_dim)
    return mesh

Load a triangle Mesh from an STL file.

Args

file
File path to .stl file.
face_dim
Instance dim along which to list the triangles.

Returns

Mesh with spatial_rank=3 and element_rank=2.

def load_su2(file_or_mesh: str, cell_dim=(cellsⁱ=None), face_format: str = 'csc') ‑> phi.geom._mesh.Mesh
Expand source code
@broadcast
def load_su2(file_or_mesh: str, cell_dim=instance('cells'), face_format: str = 'csc') -> Mesh:
    """
    Load an unstructured mesh from a `.su2` file.

    This requires the package `ezmesh` to be installed.

    Args:
        file_or_mesh: Path to `.su2` file or *ezmesh* `Mesh` instance.
        cell_dim: Dimension along which to list the cells. This should be an instance dimension.
        face_format: Sparse storage format for cell connectivity.

    Returns:
        `Mesh`
    """
    if isinstance(file_or_mesh, str):
        from ezmesh import import_from_file
        mesh = import_from_file(file_or_mesh)
    else:
        mesh = file_or_mesh
    if mesh.dim == 2 and mesh.points.shape[-1] == 3:
        points = mesh.points[..., :2]
    else:
        assert mesh.dim == 3, f"Only 2D and 3D meshes are supported but got {mesh.dim} in {file_or_mesh}"
        points = mesh.points
    boundaries = {name.strip(): markers for name, markers in mesh.markers.items()}
    return mesh_from_numpy(points, mesh.elements, boundaries, cell_dim=cell_dim, face_format=face_format)

Load an unstructured mesh from a .su2 file.

This requires the package ezmesh to be installed.

Args

file_or_mesh
Path to .su2 file or ezmesh Mesh instance.
cell_dim
Dimension along which to list the cells. This should be an instance dimension.
face_format
Sparse storage format for cell connectivity.

Returns

Mesh

def mesh(vertices: phi.geom._geom.Geometry | phiml.math._tensors.Tensor,
elements: phiml.math._tensors.Tensor,
boundaries: str | Dict[str, List[Sequence]] | None = None,
element_rank: int = None,
periodic: str = None,
face_format: str = 'csc',
max_cell_walk: int = None)
Expand source code
@broadcast(dims=batch)
def mesh(vertices: Geometry | Tensor,
         elements: Tensor,
         boundaries: str | Dict[str, List[Sequence]] | None = None,
         element_rank: int = None,
         periodic: str = None,
         face_format: str = 'csc',
         max_cell_walk: int = None):
    """
    Create a mesh from vertex positions and vertex lists.

    Args:
        vertices: `Tensor` with one instance and one channel dimension `vector`.
        elements: Lists of vertex indices as 2D tensor.
            The elements must be listed along an instance dimension, and the vertex indices belonging to the same polygon must be listed along a spatial dimension.
        boundaries: Pass a `str` to assign one name to all boundary faces.
            For multiple boundaries, pass a `dict` mapping group names `str` to lists of faces, defined by their vertices.
            The last entry can be `None` to group all boundary faces not explicitly listed before.
            The `boundaries` `dict` maps boundary names to a list of edges (point pairs) in 2D and faces (3 or more points) in 3D (not yet supported).
        face_format: Storage format for cell connectivity, must be one of `csc`, `coo`, `csr`, `dense`.

    Returns:
        `Mesh`
    """
    assert 'vector' in channel(vertices), f"vertices must have a channel dimension called 'vector' but got {shape(vertices)}"
    assert instance(vertices), f"vertices must have an instance dimension listing all vertices of the mesh but got {shape(vertices)}"
    if not isinstance(vertices, Geometry):
        vertices = Point(vertices)
    if spatial(elements):  # all elements have same number of vertices
        indices: Tensor = rename_dims(elements, spatial, instance(vertices).as_dual())
        values = expand(True, non_batch(indices))
        elements = CompactSparseTensor(indices, values, instance(vertices).as_dual(), True)
    assert instance(vertices).as_dual() in elements.shape, f"elements must have the instance dim of vertices {instance(vertices)} but got {shape(elements)}"
    if element_rank is None:
        if vertices.vector.size == 2:
            element_rank = 2
        elif vertices.vector.size == 3:
            min_vertices = sum_(elements, instance(vertices).as_dual()).min
            element_rank = 2 if min_vertices <= 4 else 3  # assume tri or quad mesh
        else:
            raise ValueError(vertices.vector.size)
    if max_cell_walk is None:
        max_cell_walk = 2 if instance(elements).volume > 1 else 1
    # --- build faces ---
    periodic_dims = []
    if periodic is not None:
        periodic_dims = [s.strip() for s in periodic.split(',') if s.strip()]
        periodic_base = [p[:-len('[::-1]')] if p.endswith('[::-1]') else p for p in periodic_dims]
        assert all(p in vertices.vector.item_names for p in periodic_base), f"Periodic boundaries must be named after axes, e.g. {vertices.vector.item_names} but got {periodic}"
        for base in periodic_base:
            assert base+'+' in boundaries and base+'-' in boundaries, f"Missing boundaries for periodicity '{base}'. Make sure '{base}+' and '{base}-' are keys in boundaries dict, got {tuple(boundaries)}"
    return Mesh(vertices, elements, element_rank, boundaries, periodic_dims, face_format=face_format, max_cell_walk=max_cell_walk)

Create a mesh from vertex positions and vertex lists.

Args

vertices
Tensor with one instance and one channel dimension vector.
elements
Lists of vertex indices as 2D tensor. The elements must be listed along an instance dimension, and the vertex indices belonging to the same polygon must be listed along a spatial dimension.
boundaries
Pass a str to assign one name to all boundary faces. For multiple boundaries, pass a dict mapping group names str to lists of faces, defined by their vertices. The last entry can be None to group all boundary faces not explicitly listed before. The boundaries dict maps boundary names to a list of edges (point pairs) in 2D and faces (3 or more points) in 3D (not yet supported).
face_format
Storage format for cell connectivity, must be one of csc, coo, csr, dense.

Returns

Mesh

def mesh_from_numpy(points: Sequence[Sequence],
polygons: Sequence[Sequence],
boundaries: str | Dict[str, List[Sequence]] | None = None,
element_rank: int = None,
periodic: str = None,
cell_dim: phiml.math._shape.Shape = (cellsⁱ=None),
face_format: str = 'csc',
axes=('x', 'y', 'z')) ‑> phi.geom._mesh.Mesh
Expand source code
def mesh_from_numpy(points: Sequence[Sequence],
                    polygons: Sequence[Sequence],
                    boundaries: str | Dict[str, List[Sequence]] | None = None,
                    element_rank: int = None,
                    periodic: str = None,
                    cell_dim: Shape = instance('cells'),
                    face_format: str = 'csc',
                    axes=('x', 'y', 'z')) -> Mesh:
    """
    Construct an unstructured mesh from vertices.

    Args:
        points: 2D numpy array of shape (num_points, point_coord).
            The last dimension must have length 2 for 2D meshes and 3 for 3D meshes.
        polygons: List of elements. Each polygon is defined as a sequence of point indices mapping into `points'.
            E.g. `[(0, 1, 2)]` denotes a single triangle connecting points 0, 1, and 2.
        boundaries: An unstructured mesh can have multiple boundaries, each defined by a name `str` and a list of faces, defined by their vertices.
            The `boundaries` `dict` maps boundary names to a list of edges (point pairs) in 2D and faces (3 or more points) in 3D (not yet supported).
        cell_dim: Dimension along which to list the cells. This should be an instance dimension.
        face_format: Storage format for cell connectivity, must be one of `csc`, `coo`, `csr`, `dense`.

    Returns:
        `Mesh`
    """
    cell_dim = cell_dim.with_size(len(polygons))
    points = np.asarray(points)
    xyz = tuple(axes[:points.shape[-1]])
    vertices = wrap(points, instance('vertices'), channel(vector=xyz))
    try:  # if all elements have the same vertex count, we stack them
        elements_np = np.stack(polygons).astype(np.int32)
        elements = wrap(elements_np, cell_dim, spatial('vertex_index'))
    except ValueError:
        indices = np.concatenate(polygons)
        vertex_count = np.asarray([len(e) for e in polygons])
        ptr = np.pad(np.cumsum(vertex_count), (1, 0))
        mat = csr_matrix((np.ones(indices.shape, dtype=bool), indices, ptr), shape=(len(polygons), len(points)))
        elements = wrap(mat, cell_dim, instance(vertices).as_dual())
    return mesh(vertices, elements, boundaries, element_rank, periodic, face_format=face_format)

Construct an unstructured mesh from vertices.

Args

points
2D numpy array of shape (num_points, point_coord). The last dimension must have length 2 for 2D meshes and 3 for 3D meshes.
polygons
List of elements. Each polygon is defined as a sequence of point indices mapping into `points'. E.g. [(0, 1, 2)] denotes a single triangle connecting points 0, 1, and 2.
boundaries
An unstructured mesh can have multiple boundaries, each defined by a name str and a list of faces, defined by their vertices. The boundaries dict maps boundary names to a list of edges (point pairs) in 2D and faces (3 or more points) in 3D (not yet supported).
cell_dim
Dimension along which to list the cells. This should be an instance dimension.
face_format
Storage format for cell connectivity, must be one of csc, coo, csr, dense.

Returns

Mesh

def normal_from_slope(slope: phiml.math._tensors.Tensor,
space: str | phiml.math._shape.Shape | Sequence[str])
Expand source code
def normal_from_slope(slope: Tensor, space: Union[str, Shape, Sequence[str]]):
    """
    Computes the normal vector of a line, plane, or hyperplane.

    Args:
        slope: Line Slope (2D), plane slope (3D) or hyperplane slope (4+D).
            Must have one channel dimension listing the vector components.
            The vector must list all but one dimensions of `space`.
        space: Ordered spatial dimensions as comma-separated string, sequence of names or `Shape`

    Returns:
        Normal vector with the channel dimension of `slope` listing all dimensions of `space` in that order.
    """
    assert channel(slope).rank == 1 and all(d in space for d in channel(slope).item_names[0]), f"slope must have a single channel dim listing all but one component of the space {space} but got {slope.shape}"
    space = parse_dim_order(space)
    assert len(space) > 1, f"space must contain at least 2 dimensions"
    up = set(space) - set(channel(slope).item_names[0])
    assert len(up) == 1, f"space must have exactly one more dimension than slope but got slope {channel(slope)} for space {space}"
    up = next(iter(up))
    normal = vec(channel(slope).name, **{d: 1 if d == up else -slope[d] for d in space})
    return vec_normalize(normal, allow_infinite=True)

Computes the normal vector of a line, plane, or hyperplane.

Args

slope
Line Slope (2D), plane slope (3D) or hyperplane slope (4+D). Must have one channel dimension listing the vector components. The vector must list all but one dimensions of space.
space
Ordered spatial dimensions as comma-separated string, sequence of names or Shape

Returns

Normal vector with the channel dimension of slope listing all dimensions of space in that order.

def normalize(obj: phiml.math._tensors.Tensor,
epsilon=1e-05,
allow_infinite=False,
allow_zero=True)
Expand source code
def normalize(obj: Tensor, epsilon=1e-5, allow_infinite=False, allow_zero=True):
    """
    Normalize a vector `Tensor` along the 'vector' dim.

    Args:
        obj: `Tensor` with 'vector' dim.
        epsilon: (Optional) Zero-length threshold. Vectors shorter than this length yield the unit vector (1, 0, 0, ...).
            If not specified, the zero-vector yields `NaN` as it cannot be normalized.
        allow_infinite: Allow infinite components in vectors. These vectors will then only points towards the infinite components.
        allow_zero: Whether to return zero vectors for inputs smaller `epsilon` instead of a unit vector.

    Returns:
        `Tensor` of the same shape as `obj`.
    """
    assert 'vector' in obj.shape, f"normalize() requires 'vector' dim but got {type(obj)} with shape {shape(obj)}."
    return math.normalize(obj, 'vector', epsilon, allow_infinite=allow_infinite, allow_zero=allow_zero)

Normalize a vector Tensor along the 'vector' dim.

Args

obj
Tensor with 'vector' dim.
epsilon
(Optional) Zero-length threshold. Vectors shorter than this length yield the unit vector (1, 0, 0, …). If not specified, the zero-vector yields NaN as it cannot be normalized.
allow_infinite
Allow infinite components in vectors. These vectors will then only points towards the infinite components.
allow_zero
Whether to return zero vectors for inputs smaller epsilon instead of a unit vector.

Returns

Tensor of the same shape as obj.

def numpy_sdf(sdf: Callable,
bounds: phi.geom._box.BaseBox,
center: phiml.math._tensors.Tensor = None) ‑> phi.geom._sdf.SDF
Expand source code
def numpy_sdf(sdf: Callable, bounds: BaseBox, center: Tensor = None) -> SDF:
    """
    Define a `SDF` (signed distance function) from a NumPy function.

    Args:
        sdf: Function mapping a location `numpy.ndarray` of shape `(points, vector)` to the corresponding SDF value `(points,)`.
        bounds: Bounds inside which the function is defined.
        center: Optional center position of the object encoded via `sdf`.

    Returns:
        `SDF`
    """
    def native_sdf_function(pos: Tensor) -> Tensor:
        nat_pos = math.reshaped_native(pos, [..., 'vector'])
        nat_sdf = pos.default_backend.numpy_call(sdf, nat_pos.shape[:1], math.DType(float, 32), nat_pos)
        with pos.default_backend:
            return math.reshaped_tensor(nat_sdf, [pos.shape - 'vector'])
    return SDF(native_sdf_function, math.EMPTY_SHAPE, bounds, center)

Define a SDF (signed distance function) from a NumPy function.

Args

sdf
Function mapping a location numpy.ndarray of shape (points, vector) to the corresponding SDF value (points,).
bounds
Bounds inside which the function is defined.
center
Optional center position of the object encoded via sdf.

Returns

SDF

def pack_dims(value,
dims: str | Sequence | set | ForwardRef('Shape') | Callable | None,
packed_dim: phiml.math._shape.Shape | str,
pos: int | None = None,
**kwargs)
Expand source code
def pack_dims(value, dims: DimFilter, packed_dim: Union[Shape, str], pos: Optional[int] = None, **kwargs):
    """
    Compresses multiple dimensions into a single dimension by concatenating the elements.
    Elements along the new dimensions are laid out according to the order of `dims`.
    If the order of `dims` differs from the current dimension order, the tensor is transposed accordingly.
    This function replaces the traditional `reshape` for these cases.

    The type of the new dimension will be equal to the types of `dims`.
    If `dims` have varying types, the new dimension will be a batch dimension.

    If none of `dims` exist on `value`, `packed_dim` will be added only if it is given with a definite size and `value` is not a primitive type.

    See Also:
        `unpack_dim()`

    Args:
        value: `phiml.math.magic.Shapable`, such as `phiml.math.Tensor`.
        dims: Dimensions to be compressed in the specified order.
        packed_dim: Single-dimension `Shape`.
        pos: Index of new dimension. `None` for automatic, `-1` for last, `0` for first.
        **kwargs: Additional keyword arguments required by specific implementations.
            Adding spatial dimensions to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions.
            Adding batch dimensions must always work without keyword arguments.

    Returns:
        Same type as `value`.

    Examples:
        >>> pack_dims(math.zeros(spatial(x=4, y=3)), spatial, instance('points'))
        (pointsⁱ=12) const 0.0
    """
    if isinstance(value, (Number, bool)):
        return value
    assert isinstance(value, Shapable) and isinstance(value, Sliceable) and isinstance(value, Shaped), f"value must be Shapable but got {type(value)}"
    packed_dim = auto(packed_dim, dims if callable(dims) else None) if isinstance(packed_dim, str) else packed_dim
    dims = shape(value).only(dims, reorder=True)
    if packed_dim in shape(value):
        assert packed_dim in dims, f"Cannot pack dims into new dimension {packed_dim} because it already exists on value {value} and is not packed."
    if len(dims) == 0 or all(dim not in shape(value) for dim in dims):
        return value if packed_dim.size is None else expand(value, packed_dim, **kwargs)  # Inserting size=1 can cause shape errors
    elif len(dims) == 1 and packed_dim.rank == 1:
        return rename_dims(value, dims, packed_dim, **kwargs)
    elif len(dims) == 1 and packed_dim.rank > 1:
        return unpack_dim(value, dims, packed_dim, **kwargs)
    # --- First try __pack_dims__ ---
    if hasattr(value, '__pack_dims__'):
        result = value.__pack_dims__(dims.names, packed_dim, pos, **kwargs)
        if result is not NotImplemented:
            return result
    # --- Next try Tree Node ---
    if isinstance(value, PhiTreeNode):
        return tree_map(pack_dims, value, attr_type=all_attributes, dims=dims, packed_dim=packed_dim, pos=pos, **kwargs)
    # --- Fallback: unstack and stack ---
    if shape(value).only(dims).volume > 8:
        warnings.warn(f"pack_dims() default implementation is slow on large dimensions ({shape(value).only(dims)}). Please implement __pack_dims__() for {type(value).__name__} as defined in phiml.math.magic", RuntimeWarning, stacklevel=2)
    return stack(unstack(value, dims), packed_dim, **kwargs)

Compresses multiple dimensions into a single dimension by concatenating the elements. Elements along the new dimensions are laid out according to the order of dims. If the order of dims differs from the current dimension order, the tensor is transposed accordingly. This function replaces the traditional reshape for these cases.

The type of the new dimension will be equal to the types of dims. If dims have varying types, the new dimension will be a batch dimension.

If none of dims exist on value, packed_dim will be added only if it is given with a definite size and value is not a primitive type.

See Also: unpack_dim()

Args

value
phiml.math.magic.Shapable, such as phiml.math.Tensor.
dims
Dimensions to be compressed in the specified order.
packed_dim
Single-dimension Shape.
pos
Index of new dimension. None for automatic, -1 for last, 0 for first.
**kwargs
Additional keyword arguments required by specific implementations. Adding spatial dimensions to fields requires the bounds: Box argument specifying the physical extent of the new dimensions. Adding batch dimensions must always work without keyword arguments.

Returns

Same type as value.

Examples

>>> pack_dims(math.zeros(spatial(x=4, y=3)), spatial, instance('points'))
(pointsⁱ=12) const 0.0
def rotate(obj: ~GeometricType,
rot: float | phiml.math._tensors.Tensor | None,
invert=False,
pivot: str | phiml.math._tensors.Tensor = 'bounds') ‑> ~GeometricType
Expand source code
def rotate(obj: GeometricType, rot: float | Tensor | None, invert=False, pivot: Tensor | str = 'bounds') -> GeometricType:
    """
    Rotate a vector or `Geometry` about the `pivot`.

    Args:
        obj: n-dimensional vector `Tensor` or `Geometry`.
        rot: Euler angle(s) or rotation matrix.
            `None` is interpreted as no rotation.
        invert: Whether to apply the inverse rotation.
        pivot: Either a point (`Tensor`) lying on the rotation axis or one of the following strings: 'bounds', 'individual'.
            Vector tensors are rotated about the origin if `pivot` is not given as a `Tensor`.

    Returns:
        Rotated vector as `Tensor`
    """
    if rot is None:
        return obj
    if isinstance(obj, Geometry):
        if pivot is None:
            pivot = obj.bounding_box().center
        center = pivot + rotate(obj.center - pivot, rot)
        return obj.rotated(rot).at(center)
    elif isinstance(obj, Tensor):
        if isinstance(pivot, Tensor):
            return pivot + rotate_vector(obj - pivot, rot, invert=invert)
        else:
            return rotate_vector(obj, rot, invert=invert)

Rotate a vector or Geometry about the pivot.

Args

obj
n-dimensional vector Tensor or Geometry.
rot
Euler angle(s) or rotation matrix. None is interpreted as no rotation.
invert
Whether to apply the inverse rotation.
pivot
Either a point (Tensor) lying on the rotation axis or one of the following strings: 'bounds', 'individual'. Vector tensors are rotated about the origin if pivot is not given as a Tensor.

Returns

Rotated vector as Tensor

def rotation_angles(rot: phiml.math._tensors.Tensor)
Expand source code
def rotation_angles(rot: Tensor):
    """
    Compute the scalar x in 2D or the Euler angles in 3D from a given rotation matrix.
    This function returns one valid solution but often, there are multiple solutions.

    Args:
        rot: Rotation matrix as created by `phi.math.rotation_matrix()`.
            Must have exactly one channel and one dual dimension with equally-ordered elements.

    Returns:
        Scalar x in 2D, Euler angles
    """
    assert channel(rot).rank == 1 and dual(rot).rank == 1, f"Rotation matrix must have one channel and one dual dimension but got {rot.shape}"
    if channel(rot).size == 2:
        cos = rot[{channel: 0, dual: 0}]
        sin = rot[{channel: 1, dual: 0}]
        return math.arctan(sin, divide_by=cos)
    elif channel(rot).size == 3:
        a2 = -math.arcsin(rot[{channel: 2, dual: 0}])  # ToDo handle [2, 0] == 1 (i.e. cos_theta == 0)
        cos2 = math.cos(a2)
        a1 = math.arctan(rot[{channel: 2, dual: 1}] / cos2, divide_by=rot[{channel: 2, dual: 2}] / cos2)
        a3 = math.arctan(rot[{channel: 1, dual: 0}] / cos2, divide_by=rot[{channel: 0, dual: 0}] / cos2)
        regular_sol = stack([a1, a2, a3], channel(angle=channel(rot).item_names[0]))
        # --- pole case cos(theta) == 1 ---
        a3_pole = 0  # unconstrained
        bottom_pole = rot[{channel: 2, dual: 0}] < 0
        a2_pole = math.where(bottom_pole, 1.57079632679, -1.57079632679)
        a1_pole = math.where(bottom_pole, math.arctan(rot[{channel: 0, dual: 1}], divide_by=rot[{channel: 0, dual: 2}]), math.arctan(-rot[{channel: 0, dual: 1}], divide_by=-rot[{channel: 0, dual: 2}]))
        pole_sol = stack([a1_pole, a2_pole, a3_pole], channel(regular_sol))
        return math.where(abs(rot[{channel: 2, dual: 0}]) >= 1, pole_sol, regular_sol)
    else:
        raise ValueError(f"")

Compute the scalar x in 2D or the Euler angles in 3D from a given rotation matrix. This function returns one valid solution but often, there are multiple solutions.

Args

rot
Rotation matrix as created by phi.math.rotation_matrix(). Must have exactly one channel and one dual dimension with equally-ordered elements.

Returns

Scalar x in 2D, Euler angles

def rotation_matrix(x: float | phiml.math._tensors.Tensor | None,
matrix_dim=(vectorᶜ=None),
none_to_unit=False) ‑> phiml.math._tensors.Tensor | None
Expand source code
def rotation_matrix(x: float | math.Tensor | None, matrix_dim=channel('vector'), none_to_unit=False) -> Optional[Tensor]:
    """
    Create a 2D or 3D rotation matrix from the corresponding angle(s).

    Args:
        x:
            2D: scalar angle
            3D: Either vector pointing along the rotation axis with rotation angle as length or Euler angles.
            Euler angles need to be laid out along a `angle` channel dimension with dimension names listing the spatial dimensions.
            E.g. a 90° rotation about the z-axis is represented by `vec('angles', x=0, y=0, z=PI/2)`.
            If a rotation matrix is passed for `angle`, it is returned without modification.
        matrix_dim: Matrix dimension for 2D rotations. In 3D, the channel dimension of angle is used.

    Returns:
        Matrix containing `matrix_dim` in primal and dual form as well as all non-channel dimensions of `x`.
    """
    if x is None and not none_to_unit:
        return None
    elif x is None:
        return to_float(arange(matrix_dim) == arange(matrix_dim.as_dual()))
    if isinstance(x, Tensor) and x.dtype == object:  # possibly None in matrices
        return math.map(rotation_matrix, x, dims=object, matrix_dim=matrix_dim, none_to_unit=none_to_unit)
    if isinstance(x, Tensor) and '~vector' in x.shape and 'vector' in x.shape.channel and x.shape.get_size('~vector') == x.shape.get_size('vector'):
        return x  # already a rotation matrix
    elif 'angle' in shape(x) and shape(x).get_size('angle') == 3:  # 3D Euler angles
        assert channel(x).rank == 1 and channel(x).size == 3, f"x for 3D rotations needs to be a 3-vector but got {x}"
        s1, s2, s3 = math.sin(x).angle  # x, y, z
        c1, c2, c3 = math.cos(x).angle
        matrix_dim = matrix_dim.with_size(shape(x).get_item_names('angle'))
        return wrap([[c3 * c2, c3 * s2 * s1 - s3 * c1, c3 * s2 * c1 + s3 * s1],
                     [s3 * c2, s3 * s2 * s1 + c3 * c1, s3 * s2 * c1 - c3 * s1],
                     [-s2, c2 * s1, c2 * c1]], matrix_dim, matrix_dim.as_dual())  # Rz * Ry * Rx  (1. rotate about X by first angle)
    elif 'vector' in shape(x) and shape(x).get_size('vector') == 3:  # 3D axis + x
        angle = vec_length(x)
        s, c = math.sin(angle), math.cos(angle)
        t = 1 - c
        k1, k2, k3 = normalize(x, epsilon=1e-12).vector
        matrix_dim = matrix_dim.with_size(shape(x).get_item_names('vector'))
        return wrap([[c + k1**2 * t, k1 * k2 * t - k3 * s, k1 * k3 * t + k2 * s],
                     [k2 * k1 * t + k3 * s, c + k2**2 * t, k2 * k3 * t - k1 * s],
                     [k3 * k1 * t - k2 * s, k3 * k2 * t + k1 * s, c + k3**2 * t]], matrix_dim, matrix_dim.as_dual())
    else:  # 2D rotation
        sin = wrap(math.sin(x))
        cos = wrap(math.cos(x))
        return wrap([[cos, -sin], [sin, cos]], matrix_dim, matrix_dim.as_dual())

Create a 2D or 3D rotation matrix from the corresponding angle(s).

Args

x:
2D: scalar angle
3D: Either vector pointing along the rotation axis with rotation angle as length or Euler angles.
Euler angles need to be laid out along a angle channel dimension with dimension names listing the spatial dimensions.
E.g. a 90° rotation about the z-axis is represented by vec('angles', x=0, y=0, z=PI/2).
If a rotation matrix is passed for angle, it is returned without modification.
matrix_dim
Matrix dimension for 2D rotations. In 3D, the channel dimension of angle is used.

Returns

Matrix containing matrix_dim in primal and dual form as well as all non-channel dimensions of x.

def rotation_matrix_from_axis_and_angle(axis: phiml.math._tensors.Tensor,
angle: phiml.math._tensors.Tensor | float,
vec_dim='vector',
is_axis_normalized=False,
epsilon=1e-05) ‑> phiml.math._tensors.Tensor
Expand source code
def rotation_matrix_from_axis_and_angle(axis: Tensor, angle: float | Tensor, vec_dim='vector', is_axis_normalized=False, epsilon=1e-5) -> Tensor:
    """
    Computes a rotation matrix that rotates by `angle` around `axis`.

    Args:
        axis: 3D vector. `Tensor` with channel dim called 'vector'.
        angle: Rotation angle.
        is_axis_normalized: Whether `axis` has length 1.
        epsilon: Minimum axis length. For shorter axes, the unit matrix is returned.

    Returns:
        Rotation matrix as `Tensor` with 'vector' dim and its dual counterpart.
    """
    if axis.vector.size == 3:  # Rodrigues' rotation formula
        axis = normalize(axis, vec_dim, epsilon=epsilon, allow_zero=False) if not is_axis_normalized else axis
        kx, ky, kz = axis.vector
        s = math.sin(angle)
        c = 1 - math.cos(angle)
        return wrap([
            (1 - c*(ky*ky+kz*kz),    -kz*s + c*(kx*ky),     ky*s + c*(kx*kz)),
            (   kz*s + c*(kx*ky),  1 - c*(kx*kx+kz*kz),     -kx*s + c*(ky * kz)),
            (  -ky*s + c*(kx*kz),    kx*s + c*(ky * kz),  1 - c*(kx*kx+ky*ky)),
        ], axis.shape['vector'], axis.shape['vector'].as_dual())
    raise NotImplementedError

Computes a rotation matrix that rotates by angle around axis.

Args

axis
3D vector. Tensor with channel dim called 'vector'.
angle
Rotation angle.
is_axis_normalized
Whether axis has length 1.
epsilon
Minimum axis length. For shorter axes, the unit matrix is returned.

Returns

Rotation matrix as Tensor with 'vector' dim and its dual counterpart.

def rotation_matrix_from_directions(source_dir: phiml.math._tensors.Tensor,
target_dir: phiml.math._tensors.Tensor,
vec_dim: str = 'vector',
epsilon=None) ‑> phiml.math._tensors.Tensor
Expand source code
def rotation_matrix_from_directions(source_dir: Tensor, target_dir: Tensor, vec_dim: str = 'vector', epsilon=None) -> Tensor:
    """
    Computes a rotation matrix A, such that `target_dir = A @ source_dir`

    Args:
        source_dir: Two or three-dimensional vector. `Tensor` with channel dim called 'vector'.
        target_dir: Two or three-dimensional vector. `Tensor` with channel dim called 'vector'.

    Returns:
        Rotation matrix as `Tensor` with 'vector' dim and its dual counterpart.
    """
    if source_dir.vector.size == 3:
        axis, angle = axis_angle_from_directions(source_dir, target_dir, vec_dim, epsilon=epsilon)
        return rotation_matrix_from_axis_and_angle(axis, angle, is_axis_normalized=False, epsilon=epsilon)
    raise NotImplementedError

Computes a rotation matrix A, such that target_dir = A @ source_dir

Args

source_dir
Two or three-dimensional vector. Tensor with channel dim called 'vector'.
target_dir
Two or three-dimensional vector. Tensor with channel dim called 'vector'.

Returns

Rotation matrix as Tensor with 'vector' dim and its dual counterpart.

def sample_function(f: Callable,
elements: phi.geom._geom.Geometry,
at: str,
extrapolation: phiml.math.extrapolation.Extrapolation) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_function(f: Callable, elements: Geometry, at: str, extrapolation: Extrapolation) -> Tensor:
    """
    Calls `f`, passing either the `elements` directly or the relevant sample points as a `Tensor`, depending on the signature of `f`.

    Args:
        f: Function taking a `Geometry` or location `Tensor´ and returning a `Tensor`.
            A `Geometry` will be passed if the first argument of `f` is called `geometry` or `geo` or ends with `_geo`.
        elements: `Geometry` on which to sample `f`.
        at: Set of sample points, see `Geometry.sets`.
        extrapolation: Determines which boundary points are relevant.

    Returns:
        Sampled values as `Tensor`.
    """
    from phiml.math._functional import get_function_parameters
    pass_geometry = False
    try:
        params = get_function_parameters(f)
        dims = elements.shape.get_size('vector')
        names_match = tuple(params.keys())[:dims] == elements.shape.get_item_names('vector')
        num_positional = 0
        varargs_only = False
        for i, (n, p) in enumerate(params.items()):
            if p.default is p.empty and p.kind == 1:
                num_positional += 1
            if p.kind == 2 and i == 0:  # _ParameterKind.VAR_POSITIONAL
                varargs_only = True
        assert num_positional <= dims, f"Cannot sample {f.__name__}({', '.join(tuple(params))}) on physical space {elements.shape.get_item_names('vector')}"
        pass_varargs = varargs_only or names_match or num_positional > 1 or num_positional == dims
        if num_positional > 1 and not varargs_only:
            assert names_match, f"Positional arguments of {f.__name__}({', '.join(tuple(params))}) should match physical space {elements.shape.get_item_names('vector')}"
        if not pass_varargs:
            first = next(iter(params))
            if first in ['geo', 'geometry'] or '_geo' in first:
                pass_geometry = True
    except ValueError as err:  # signature not available for all functions
        pass_varargs = False
    if at == 'center':
        pos = slice_off_constant_faces(elements if pass_geometry else elements.center, elements.boundary_elements, extrapolation)
    else:
        pos = elements if pass_geometry else slice_off_constant_faces(elements.face_centers, elements.boundary_faces, extrapolation)
    if pass_varargs:
        values = math.map_s2b(f)(*pos.vector)
    else:
        values = math.map_s2b(f)(pos)
    assert isinstance(values, math.Tensor), f"values function must return a Tensor but returned {type(values)}"
    return values

Calls f, passing either the elements directly or the relevant sample points as a Tensor, depending on the signature of f.

Args

f
Function taking a Geometry or location Tensor´ and returning aTensor`. A Geometry will be passed if the first argument of f is called geometry or geo or ends with _geo.
elements
Geometry on which to sample f.
at
Set of sample points, see Geometry.sets.
extrapolation
Determines which boundary points are relevant.

Returns

Sampled values as Tensor.

def sample_sdf(geometry: phi.geom._geom.Geometry,
bounds: phi.geom._box.BaseBox | phi.geom._grid.UniformGrid = None,
resolution: phiml.math._shape.Shape = (),
approximate_outside=False,
rebuild: str | None = None,
valid_dist=None,
rel_margin=0.1,
abs_margin=0.0,
cache_surface=False,
**resolution_: int) ‑> phi.geom._sdf_grid.SDFGrid
Expand source code
def sample_sdf(geometry: Geometry,
               bounds: BaseBox | UniformGrid = None,
               resolution: Shape = math.EMPTY_SHAPE,
               approximate_outside=False,
               rebuild: Optional[str] = None,
               valid_dist=None,
               rel_margin=.1,
               abs_margin=0.,
               cache_surface=False,
               **resolution_: int) -> SDFGrid:
    """
    Build a grid of signed distance values for a given `Geometry` object.

    Args:
        geometry: `Geometry` to capture.
        bounds: Grid limits in world space.
        resolution: Grid resolution.
        **resolution_: Grid resolution as `kwargs`, e.g. `x=64, y=32`.
        approximate_outside: Whether queries outside the SDF grid should return approximate values. This requires additional computations.
        rebuild: If `'from-surface'`, SDF values are calculated from a narrow strip above the enclosed surface. This is more accurate but requires additional steps.
            If `None` (default), SDF values are queried from `geometry`.
            `'auto'` rebuilds when geometry querying is expected to be in accurate.

    Returns:
        SDF grid as `Geometry`.
    """
    resolution = resolution & spatial(**resolution_)
    if bounds is None:
        bounds: BaseBox = geometry.bounding_box()
        bounds = Cuboid(bounds.center, half_size=bounds.half_size * (1 + 2 * rel_margin) + 2 * abs_margin)
    elif isinstance(bounds, UniformGrid):
        assert not resolution, f"When specifying a UniformGrid, separate resolution values are not allowed."
        resolution = bounds.resolution
        bounds = bounds.bounds
    points = UniformGrid(resolution, bounds).center
    reduce = instance(geometry) & spatial(geometry)
    if reduce:
        center = math.mean(geometry.center, reduce)
        volume = None
        bounding_radius = None
        rebuild = 'from-surface' if rebuild == 'auto' else rebuild
    else:
        center = geometry.center
        volume = geometry.volume
        bounding_radius = geometry.bounding_radius()
        rebuild = None if rebuild == 'auto' else rebuild
    if cache_surface or rebuild is not None:
        sdf, delta, normal, _, idx = geometry.approximate_closest_surface(points)
        approximate = SDFGrid(sdf, bounds, approximate_outside, None, delta, normal, idx, center=center, volume=volume, bounding_radius=bounding_radius)
    else:
        sdf = geometry.approximate_signed_distance(points)
        approximate = SDFGrid(sdf, bounds, approximate_outside, center=center, volume=volume, bounding_radius=bounding_radius)
    if rebuild is None:
        return approximate
    assert rebuild in ['from-surface']
    dx = bounds.size / resolution
    min_dist = math.sum(dx ** 2) ** (1 / geometry.spatial_rank)
    valid_dist = math.maximum(min_dist, valid_dist) if valid_dist is not None else min_dist
    sdf = rebuild_sdf(approximate, 0, valid_dist, refine=[geometry])
    return SDFGrid(sdf, bounds, approximate_outside, center=center, volume=volume, bounding_radius=bounding_radius)

Build a grid of signed distance values for a given Geometry object.

Args

geometry
Geometry to capture.
bounds
Grid limits in world space.
resolution
Grid resolution.
**resolution_
Grid resolution as kwargs, e.g. x=64, y=32.
approximate_outside
Whether queries outside the SDF grid should return approximate values. This requires additional computations.
rebuild
If 'from-surface', SDF values are calculated from a narrow strip above the enclosed surface. This is more accurate but requires additional steps. If None (default), SDF values are queried from geometry. 'auto' rebuilds when geometry querying is expected to be in accurate.

Returns

SDF grid as Geometry.

def scale(obj: ~GeometricType,
scale: phiml.math._tensors.Tensor | float,
pivot: phiml.math._tensors.Tensor = None,
dim='vector') ‑> ~GeometricType
Expand source code
def scale(obj: GeometricType, scale: float | Tensor, pivot: Tensor = None, dim='vector') -> GeometricType:
    """
    Scale a `Geometry` or vector `Tensor` about a pivot point.

    Args:
        obj: `Geometry` to scale.
        scale: Scaling factor.
        pivot: Point that stays fixed under the scaling operation. Defaults to the bounding box center.

    Returns:
        Rotated `Geometry`
    """
    if scale is None:
        return obj
    if isinstance(obj, Geometry):
        if pivot is None:
            pivot = obj.bounding_box().center
        center = pivot + scale * (obj.center - pivot)
        return obj.scaled(scale).at(center)
    elif isinstance(obj, Tensor):
        assert 'vector' in obj.shape, f"vector must have exactly a channel dimension named 'vector'"
        if pivot is None:
            return obj * scale
        raise NotImplementedError
    raise ValueError(obj)

Scale a Geometry or vector Tensor about a pivot point.

Args

obj
Geometry to scale.
scale
Scaling factor.
pivot
Point that stays fixed under the scaling operation. Defaults to the bounding box center.

Returns

Rotated Geometry

def stack(values: Sequence[~PhiTreeNodeType] | Dict[str, ~PhiTreeNodeType],
dim: phiml.math._shape.Shape | str,
expand_values=False,
simplify=False,
layout_non_matching=False,
**kwargs) ‑> ~PhiTreeNodeType
Expand source code
def stack(values: Union[Sequence[PhiTreeNodeType], Dict[str, PhiTreeNodeType]], dim: Union[Shape, str], expand_values=False, simplify=False, layout_non_matching=False, **kwargs) -> PhiTreeNodeType:
    """
    Stacks `values` along the new dimension `dim`.
    All values must have the same spatial, instance and channel dimensions. If the dimension sizes vary, the resulting tensor will be non-uniform.
    Batch dimensions will be added as needed.

    Stacking tensors is performed lazily, i.e. the memory is allocated only when needed.
    This makes repeated stacking and slicing along the same dimension very efficient, i.e. jit-compiled functions will not perform these operations.

    Args:
        values: Collection of `phiml.math.magic.Shapable`, such as `phiml.math.Tensor`
            If a `dict`, keys must be of type `str` and are used as item names along `dim`.
        dim: `Shape` with a least one dimension. None of these dimensions can be present with any of the `values`.
            If `dim` is a single-dimension shape, its size is determined from `len(values)` and can be left undefined (`None`).
            If `dim` is a multi-dimension shape, its volume must be equal to `len(values)`.
        expand_values: If `True`, will first add missing dimensions to all values, not just batch dimensions.
            This allows tensors with different dimensions to be stacked.
            The resulting tensor will have all dimensions that are present in `values`.
            If `False`, this may return a non-numeric object instead.
        simplify: If `True` and all values are equal, returns one value without adding the dimension.
        layout_non_matching: If non-matching values should be stacked using a Layout object, i.e. should be put into a named list instead.
        **kwargs: Additional keyword arguments required by specific implementations.
            Adding spatial dimensions to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions.
            Adding batch dimensions must always work without keyword arguments.

    Returns:
        `Tensor` containing `values` stacked along `dim`.

    Examples:
        >>> stack({'x': 0, 'y': 1}, channel('vector'))
        (x=0, y=1)

        >>> stack([math.zeros(batch(b=2)), math.ones(batch(b=2))], channel(c='x,y'))
        (x=0.000, y=1.000); (x=0.000, y=1.000) (bᵇ=2, cᶜ=x,y)

        >>> stack([vec(x=1, y=0), vec(x=2, y=3.)], batch('b'))
        (x=1.000, y=0.000); (x=2.000, y=3.000) (bᵇ=2, vectorᶜ=x,y)
    """
    assert len(values) > 0, f"stack() got empty sequence {values}"
    if simplify and len(values) == 1:
        return next(iter(values.values())) if isinstance(values, dict) else values[0]
    if not dim:
        assert len(values) == 1, f"Only one element can be passed as `values` if no dim is passed but got {values}"
        return next(iter(values.values())) if isinstance(values, dict) else values[0]
    if not isinstance(dim, Shape):
        dim = auto(dim)
    values_ = tuple(values.values()) if isinstance(values, dict) else values
    if simplify:
        if all(v is None for v in values_):
            return None
        if all(type(v) == type(values_[0]) for v in values_[1:]):
            from ._tensors import equality_by_shape_and_value
            with equality_by_shape_and_value(equal_nan=True):
                if all(v == values_[0] for v in values_[1:]):
                    return values_[0]
    shapes = [shape(v) for v in values_]
    if not expand_values:
        v0_dims = set(shapes[0].non_batch.names)
        for s in shapes[1:]:
            if set(s.non_batch.names) != v0_dims:  # shapes don't match
                if layout_non_matching:
                    from ._tensors import layout
                    return layout(values, dim)
                else:
                    raise ValueError(f"Non-batch dims must match but got: {v0_dims} and {s.non_batch.names}. Manually expand tensors or set expand_values=True")
    # --- Add missing dimensions ---
    if expand_values:
        all_dims = merge_shapes(*shapes, allow_varying_sizes=True)
        if isinstance(values, dict):
            values = {k: expand(v, all_dims - s) for (k, v), s in zip(values.items(), shapes)}
        else:
            values = [expand(v, all_dims - s) for v, s in zip(values, shapes)]
    else:
        all_batch_dims = merge_shapes(*[s.batch for s in shapes], allow_varying_sizes=True)
        if isinstance(values, dict):
            values = {k: expand(v, all_batch_dims - s) for (k, v), s in zip(values.items(), shapes)}
        else:
            values = [expand(v, all_batch_dims - s) for v, s in zip(values, shapes)]
    if dim.rank == 1:
        assert dim.size == len(values) or dim.size is None, f"stack dim size must match len(values) or be undefined but got {dim} for {len(values)} values"
        if dim.size is None:
            dim = dim.with_size(len(values))
        if isinstance(values, dict):
            dim_item_names = tuple([k.name if isinstance(k, Shape) else k for k in values.keys()])
            assert all(isinstance(k, str) for k in dim_item_names), f"dict keys must be of type str but got {dim_item_names}"
            values = tuple(values.values())
            dim = dim.with_size(dim_item_names)
        # --- First try __stack__ ---
        for v in values:
            if hasattr(v, '__stack__'):
                result = v.__stack__(values, dim, **kwargs)
                if result is not NotImplemented:
                    assert isinstance(result, Shape) if isinstance(v, Shape) else isinstance(result, Shapable), "__stack__ must return a Shapable object"
                    return result
        # --- Next: try stacking attributes for tree nodes ---
        if any(dataclasses.is_dataclass(v) for v in values):
            from ..dataclasses._merge import dc_stack
            return dc_stack(values, dim, expand_values=expand_values, simplify=simplify, layout_non_matching=layout_non_matching, **kwargs)
        if all(isinstance(v, dict) for v in values):
            keys = set(values[0])
            if all(set(v) == keys for v in values[1:]):
                new_dict = {}
                for k in keys:
                    k_values = [v[k] for v in values]
                    new_dict[k] = stack(k_values, dim, expand_values=expand_values, simplify=simplify, **kwargs)
                return new_dict
            raise NotImplementedError
        if all(isinstance(v, PhiTreeNode) for v in values):
            attributes = all_attributes(values[0])
            if attributes and all(all_attributes(v) == attributes for v in values):
                new_attrs = {}
                for a in attributes:
                    a_values = [getattr(v, a) for v in values]
                    if all(v is a_values[0] for v in a_values[1:]):
                        new_attrs[a] = expand(a_values[0], dim, **kwargs) if a_values[0] is not None else a_values[0]
                    else:
                        new_attrs[a] = stack(a_values, dim, expand_values=expand_values, simplify=simplify, **kwargs)
                return copy_with(values[0], **new_attrs)
            else:
                warnings.warn(f"Failed to concat values using value attributes because attributes differ among values {values}")
        # --- Fallback: use expand and concat ---
        for v in values:
            if not hasattr(v, '__stack__') and hasattr(v, '__concat__') and hasattr(v, '__expand__'):
                expanded_values = tuple([expand(v, dim.with_size(1 if dim.item_names[0] is None else dim.item_names[0][i]), **kwargs) for i, v in enumerate(values)])
                if len(expanded_values) > 8:
                    warnings.warn(f"stack() default implementation is slow on large dimensions ({dim.name}={len(expanded_values)}). Please implement __stack__()", RuntimeWarning, stacklevel=2)
                result = v.__concat__(expanded_values, dim.name, **kwargs)
                if result is not NotImplemented:
                    assert isinstance(result, Shapable), "__concat__ must return a Shapable object"
                    return result
        # --- else maybe all values are native scalars ---
        from ._tensors import wrap
        try:
            values = tuple([wrap(v) for v in values])
        except ValueError:
            raise MagicNotImplemented(f"At least one item in values must be Shapable but got types {[type(v) for v in values]}")
        return values[0].__stack__(values, dim, **kwargs)
    else:  # multi-dim stack
        assert dim.volume == len(values), f"When passing multiple stack dims, their volume must equal len(values) but got {dim} for {len(values)} values"
        if isinstance(values, dict):
            warnings.warn(f"When stacking a dict along multiple dimensions, the key names are discarded. Got keys {tuple(values.keys())}", RuntimeWarning, stacklevel=2)
            values = tuple(values.values())
        # --- if any value implements Shapable, use stack and unpack_dim ---
        for v in values:
            if hasattr(v, '__stack__') and hasattr(v, '__unpack_dim__'):
                stack_dim = batch('_stack')
                stacked = v.__stack__(values, stack_dim, **kwargs)
                if stacked is not NotImplemented:
                    assert isinstance(stacked, Shapable), "__stack__ must return a Shapable object"
                    assert hasattr(stacked, '__unpack_dim__'), "If a value supports __unpack_dim__, the result of __stack__ must also support it."
                    reshaped = stacked.__unpack_dim__(stack_dim.name, dim, **kwargs)
                    if reshaped is NotImplemented:
                        warnings.warn("__unpack_dim__ is overridden but returned NotImplemented during multi-dimensional stack. This results in unnecessary stack operations.", RuntimeWarning, stacklevel=2)
                    else:
                        return reshaped
        # --- Fallback: multi-level stack ---
        for dim_ in reversed(dim):
            values = [stack(values[i:i + dim_.size], dim_, **kwargs) for i in range(0, len(values), dim_.size)]
        return values[0]

Stacks values along the new dimension dim. All values must have the same spatial, instance and channel dimensions. If the dimension sizes vary, the resulting tensor will be non-uniform. Batch dimensions will be added as needed.

Stacking tensors is performed lazily, i.e. the memory is allocated only when needed. This makes repeated stacking and slicing along the same dimension very efficient, i.e. jit-compiled functions will not perform these operations.

Args

values
Collection of phiml.math.magic.Shapable, such as phiml.math.Tensor If a dict, keys must be of type str and are used as item names along dim.
dim
Shape with a least one dimension. None of these dimensions can be present with any of the values. If dim is a single-dimension shape, its size is determined from len(values) and can be left undefined (None). If dim is a multi-dimension shape, its volume must be equal to len(values).
expand_values
If True, will first add missing dimensions to all values, not just batch dimensions. This allows tensors with different dimensions to be stacked. The resulting tensor will have all dimensions that are present in values. If False, this may return a non-numeric object instead.
simplify
If True and all values are equal, returns one value without adding the dimension.
layout_non_matching
If non-matching values should be stacked using a Layout object, i.e. should be put into a named list instead.
**kwargs
Additional keyword arguments required by specific implementations. Adding spatial dimensions to fields requires the bounds: Box argument specifying the physical extent of the new dimensions. Adding batch dimensions must always work without keyword arguments.

Returns

Tensor containing values stacked along dim.

Examples

>>> stack({'x': 0, 'y': 1}, channel('vector'))
(x=0, y=1)
>>> stack([math.zeros(batch(b=2)), math.ones(batch(b=2))], channel(c='x,y'))
(x=0.000, y=1.000); (x=0.000, y=1.000) (bᵇ=2, cᶜ=x,y)
>>> stack([vec(x=1, y=0), vec(x=2, y=3.)], batch('b'))
(x=1.000, y=0.000); (x=2.000, y=3.000) (bᵇ=2, vectorᶜ=x,y)
def surface_mesh(geo: phi.geom._geom.Geometry,
rel_dx: float = None,
abs_dx: float = None,
method='auto') ‑> phi.geom._mesh.Mesh
Expand source code
def surface_mesh(geo: Geometry,
                 rel_dx: float = None,
                 abs_dx: float = None,
                 method='auto') -> Mesh:
    """
    Create a surface `Mesh` from a Geometry.

    Args:
        geo: `Geometry` to convert. Must implement `approximate_signed_distance`.
        rel_dx: Relative mesh resolution as fraction of bounding box size.
        abs_dx: Absolute mesh resolution. If both `rel_dx` and `abs_dx` are provided, the lower value is used.
        method: 'auto' to select based on the type of `geo`. 'lewiner' or 'lorensen' for marching cubes.

    Returns:
        `Mesh` if there is any geometry
    """
    if geo.spatial_rank != 3:
        raise NotImplementedError("Only 3D SDF currently supported")
    if isinstance(geo, NoGeometry):
        return mesh_from_numpy([], [], element_rank=2)
    # --- Determine resolution ---
    if isinstance(geo, SDFGrid):
        assert rel_dx is None and abs_dx is None, f"When creating a surface mesh from an SDF grid, rel_dx and abs_dx are determined from the grid and must be specified as None"
    if rel_dx is None and abs_dx is None:
        rel_dx = 0.005
    rel_dx = None if rel_dx is None else rel_dx * geo.bounding_box().size.max
    dx = math.minimum(rel_dx, abs_dx, allow_none=True)
    # --- Check special cases ---
    if method == 'auto' and isinstance(geo, BaseBox):
        assert rel_dx is None and abs_dx is None, f"When method='auto', boxes will always use their corners as vertices. Leave rel_dx,abs_dx unspecified or pass 'lewiner' or 'lorensen' as method"
        vertices = pack_dims(geo.corners, dual, instance('vertices'))
        corner_count = vertices.vertices.size
        vertices = pack_dims(vertices, instance(geo) + instance('vertices'), instance('vertices'))
        v1 = [0, 1, 4, 5, 4, 6, 5, 7, 0, 1, 2, 6]
        v2 = [1, 3, 6, 6, 0, 0, 7, 3, 4, 4, 3, 3]
        v3 = [2, 2, 5, 7, 6, 2, 1, 1, 1, 5, 6, 7]
        instance_offset = math.range_tensor(instance(geo)) * corner_count
        faces = wrap([v1, v2, v3], spatial('vertices'), instance('faces')) + instance_offset
        faces = pack_dims(faces, instance, instance('faces'))
        return mesh(vertices, faces, element_rank=2)
    elif method == 'auto' and isinstance(geo, Sphere):
        pass  # ToDo analytic solution
    elif method == 'auto' and isinstance(geo, SplineSolid):
        return geo.surface_mesh()  # ToDo resolution from dx
    # --- Build mesh from SDF ---
    if isinstance(geo, SDFGrid):
        sdf_grid = geo
    else:
        if isinstance(geo, SDF):
            sdf = geo
        else:
            sdf = as_sdf(geo, rel_margin=0, abs_margin=dx)
        resolution = maximum(1, to_int32(math.round(sdf.bounds.size / dx)))
        resolution = spatial(**resolution.vector)
        sdf_grid = sample_sdf(sdf, sdf.bounds, resolution)
    from skimage.measure import marching_cubes
    method = 'lewiner' if method == 'auto' else method
    def generate_mesh(sdf_grid: SDFGrid) -> Mesh:
        dx = sdf_grid.dx.numpy()
        sdf_numpy = sdf_grid.values.numpy(sdf_grid.dx.vector.item_names)
        vertices, faces, v_normals, _ = marching_cubes(sdf_numpy, level=0.0, spacing=dx, allow_degenerate=False, method=method)
        vertices += sdf_grid.bounds.lower.numpy() + .5 * dx
        with math.NUMPY:
            return mesh_from_numpy(vertices, faces, element_rank=2, cell_dim=instance('faces'))
    return math.map(generate_mesh, sdf_grid, dims=batch)

Create a surface Mesh from a Geometry.

Args

geo
Geometry to convert. Must implement approximate_signed_distance.
rel_dx
Relative mesh resolution as fraction of bounding box size.
abs_dx
Absolute mesh resolution. If both rel_dx and abs_dx are provided, the lower value is used.
method
'auto' to select based on the type of geo. 'lewiner' or 'lorensen' for marching cubes.

Returns

Mesh if there is any geometry

def union(*geometries, dim=(unionⁱ=None)) ‑> phi.geom._geom.Geometry
Expand source code
def union(*geometries, dim=instance('union')) -> Geometry:
    """
    Union of the given geometries.
    A point lies inside the union if it lies within at least one of the geometries.

    Args:
        *geometries: arbitrary geometries with same spatial dims. Arbitrary batch dims are allowed.
        dim: Union dimension. This must be an instance dimension.

    Returns:
        union `Geometry`
    """
    return _stack_geometries(geometries, 'union', dim)

Union of the given geometries. A point lies inside the union if it lies within at least one of the geometries.

Args

*geometries
arbitrary geometries with same spatial dims. Arbitrary batch dims are allowed.
dim
Union dimension. This must be an instance dimension.

Returns

union Geometry

Classes

class BaseBox
Expand source code
class BaseBox(Geometry):  # not a Subwoofer
    """
    Abstract base type for box-like geometries.
    """

    def __ne__(self, other):
        return not self == other

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

    @property
    def center(self) -> Tensor:
        raise NotImplementedError()

    @property
    def size(self) -> Tensor:
        raise NotImplementedError(self)

    @property
    def half_size(self) -> Tensor:
        raise NotImplementedError(self)

    @property
    def lower(self) -> Tensor:
        raise NotImplementedError(self)

    @property
    def upper(self) -> Tensor:
        raise NotImplementedError(self)

    @property
    def rotation_matrix(self) -> Tensor:
        return rotation_matrix(self._rot_or_none, self.shape['vector'], none_to_unit=True)

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

    @property
    def volume(self) -> Tensor:
        return math.prod(self.size, 'vector')

    def bounding_radius(self):
        return math.vec_length(self.half_size)

    def global_to_local(self, global_position: Tensor, scale=True, origin='lower') -> Tensor:
        """
        Transform world-space coordinates into box-space coordinates.

        Args:
            global_position: World-space coordinates.
            scale: Whether to re-scale the output so that [0, 1] or [-1, 1] represent the box for `origin='lower'` or `origin='center'`, respectively.
            origin: 'lower' or 'center'

        Returns:
            Box-space coordinate `Tensor`
        """
        assert origin in ['lower', 'center', 'upper']
        origin_loc = getattr(self, origin)
        pos = global_position if math.always_close(origin_loc, 0) else global_position - origin_loc
        pos = rotate(pos, self._rot_or_none, invert=True)
        if scale:
            pos /= (self.half_size if origin == 'center' else self.size)
        return pos

    def local_to_global(self, local_position, scale=True, origin='lower'):
        assert origin in ['lower', 'center', 'upper']
        origin_loc = getattr(self, origin)
        pos = local_position * (self.half_size if origin == 'center' else self.size) if scale else local_position
        return rotate(pos, self._rot_or_none) + origin_loc

    @property
    def _rot_or_none(self) -> Optional[Tensor]:
        raise NotImplementedError

    def largest(self, dim: DimFilter) -> 'BaseBox':
        dim = self.shape.without('vector').only(dim)
        if not dim:
            return self
        return Box(math.min(self.lower, dim), math.max(self.upper, dim))

    def smallest(self, dim: DimFilter) -> 'BaseBox':
        dim = self.shape.without('vector').only(dim)
        if not dim:
            return self
        return Box(math.max(self.lower, dim), math.min(self.upper, dim))

    def lies_inside(self, location: Tensor):
        assert self._rot_or_none is None, f"Please override lies_inside() for rotated boxes"
        bool_inside = (location >= self.lower) & (location <= self.upper)
        bool_inside = math.all(bool_inside, 'vector')
        bool_inside = math.any(bool_inside, self.shape.instance - instance(location))  # union for instance dimensions
        return bool_inside

    def approximate_signed_distance(self, location: Union[Tensor, tuple]):
        """
        Computes the signed L-infinity norm (manhattan distance) from the location to the nearest side of the box.
        For an outside location `l` with the closest surface point `s`, the distance is `max(abs(l - s))`.
        For inside locations it is `-max(abs(l - s))`.

        Args:
          location: float tensor of shape (batch_size, ..., rank)

        Returns:
          float tensor of shape (*location.shape[:-1], 1).

        """
        # ToDo this underestimates diagonally outside points
        location = self.global_to_local(location, scale=False, origin='center')
        distance = math.abs(location) - self.half_size
        distance = math.max(distance, 'vector')
        distance = math.min(distance, self.shape.instance)  # union for instance dimensions
        return distance

    def push(self, positions: Tensor, outward: bool = True, shift_amount: float = 0) -> Tensor:
        loc_to_center = self.global_to_local(positions, scale=False, origin='center')
        sgn_dist_from_surface = math.abs(loc_to_center) - self.half_size
        rotation_matrix = self.rotation_matrix
        if outward:
            # --- get negative distances (particles are inside) towards the nearest boundary and add shift_amount ---
            distances_of_interest = (sgn_dist_from_surface == math.max(sgn_dist_from_surface, 'vector')) & (sgn_dist_from_surface < 0)
            shift = distances_of_interest * (sgn_dist_from_surface - shift_amount)
            # ToDo reduce instance dim
        else:  # inward
            shift = (sgn_dist_from_surface + shift_amount) * (sgn_dist_from_surface > 0)  # get positive distances (particles are outside) and add shift_amount
            if instance(self):
                shift, loc_to_center, rotation_matrix = math.at_min((shift, loc_to_center, rotation_matrix), key=math.vec_length(shift), dim=instance)
            shift = math.where(abs(shift) > abs(loc_to_center), abs(loc_to_center), shift)  # ensure inward shift ends at center
        shift = rotate(shift, rotation_matrix)
        return positions + math.where(loc_to_center < 0, 1, -1) * shift

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        loc_to_center = self.global_to_local(location, scale=False, origin='center')
        sgn_surf_delta = math.abs(loc_to_center) - self.half_size
        if instance(self):
            raise NotImplementedError
            self_center, self_radius, sgn_dist, center_delta, center_dist = math.at_min((self.center, self.radius, sgn_dist, center_delta, center_dist), key=abs(sgn_dist), dim=instance)
        # is_inside = math.all(sgn_surf_delta < 0, 'vector')
        # abs_surf_delta = abs(sgn_surf_delta)
        max_sgn_dist = math.max(sgn_surf_delta, 'vector')
        normal_axis = max_sgn_dist == sgn_surf_delta  # ToDo only one if inside
        normal = math.vec_normalize(normal_axis * math.sign(loc_to_center))
        normal = rotate(normal, self._rot_or_none)
        surf_to_center = math.where(normal_axis, math.sign(loc_to_center) * self.half_size, loc_to_center)
        closest_to_center = math.clip(surf_to_center, -self.half_size, self.half_size)
        surface_pos = self.local_to_global(closest_to_center, scale=False, origin='center')
        delta = surface_pos - location
        face_index = expand(0, non_channel(location))
        offset = normal.vector @ surface_pos.vector
        sgn_surf_dist = math.vec_length(delta) * math.sign(max_sgn_dist)
        return sgn_surf_dist, delta, normal, offset, face_index

    def project(self, *dimensions: str):
        """ Project this box into a lower-dimensional space. """
        warnings.warn("Box.project(dims) is deprecated. Use Box.vector[dims] instead", DeprecationWarning, stacklevel=2)
        return self.vector[dimensions]

    def sample_uniform(self, *shape: Shape) -> Tensor:
        uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector'])
        return self.lower + uniform * self.size

    def sample_uniform_surface(self, *shape: Shape) -> Tensor:
        assert not instance(self), "sample_uniform_surface not yet supported for unions of boxes"
        samples = math.random_uniform(self.shape.non_singleton.non_instance, *shape, low=self.lower, high=self.upper)
        which = math.random_uniform(samples.shape.without('vector'))
        lo_or_up = math.where(which > .5, self.upper, self.lower)
        which = which * 2 % 1
        # --- which axis ---
        areas = self.face_areas
        total_area = math.sum(areas)
        frac_area = math.sum(areas / total_area, '~side')
        cum_area = math.cumulative_sum(frac_area, '~vector')
        axis = math.min(math.where(which <= cum_area, math.range(self.shape['vector'].as_dual()), self.spatial_rank), '~vector')
        axis_one_hot = math.scatter(math.zeros(samples.shape, dtype=bool), expand(axis, channel(index='vector')), True, treat_as_batch=samples.shape.without('vector'))
        math.assert_close(1, math.sum(axis_one_hot, 'vector'))
        samples = math.where(axis_one_hot, lo_or_up, samples)
        return samples

    def corner_representation(self) -> 'Box':
        assert self._rot_or_none is None, f"corner_representation does not support rotations"
        return Box(self.lower, self.upper)

    box = corner_representation

    def center_representation(self, size_variable=True) -> 'Cuboid':
        return Cuboid(self.center, self.half_size, size_variable=size_variable)

    cuboid = center_representation

    def contains(self, other: 'BaseBox'):
        """ Tests if the other box lies fully inside this box. """
        return np.all(other.lower >= self.lower) and np.all(other.upper <= self.upper)

    def scaled(self, factor: Union[float, Tensor]) -> 'BaseBox':
        return Cuboid(self.center, self.half_size * factor, size_variable=True)

    @property
    def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def faces(self) -> 'Geometry':
        return Cuboid(self.face_centers, self.half_size, self._rot_or_none, size_variable=False)

    @property
    def face_centers(self) -> Tensor:
        return self.center + self.face_normals * self.half_size

    @property
    def face_normals(self) -> Tensor:
        unit_vectors = math.to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict)))
        vectors = rotate(unit_vectors, self._rot_or_none)
        return vectors * math.vec(dual('side'), lower=-1, upper=1)

    @property
    def face_areas(self) -> Tensor:
        others_mask = math.range(self.shape['vector']) != math.range(dual(**self.shape['vector'].untyped_dict))
        result = math.exp(math.sum(math.log(self.size) * others_mask, 'vector'))
        return expand(result, dual(side='lower,upper'))  # ~vector

    @property
    def face_shape(self) -> Shape:
        return self.shape.without('vector') & dual(side='lower,upper') & dual(**self.shape['vector'].untyped_dict)

    @property
    def corners(self) -> Tensor:
        to_face = self.face_normals[{'~side': 'upper'}] * math.rename_dims(self.half_size, 'vector', dual)
        lower_upper = math.meshgrid(math.dual, **{dim: [-1, 1] for dim in self.vector.item_names}, stack_dim=dual('vector'))  # (x=2, y=2, ... vector=x,y,...)
        to_corner = math.sum(lower_upper * to_face, '~vector')
        return self.center + to_corner

Abstract base type for box-like geometries.

Ancestors

  • phi.geom._geom.Geometry

Subclasses

  • phi.geom._box.Box
  • phi.geom._box.Cuboid
  • phi.geom._grid.UniformGrid

Instance variables

prop boundary_elements : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    raise NotImplementedError()

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    to_face = self.face_normals[{'~side': 'upper'}] * math.rename_dims(self.half_size, 'vector', dual)
    lower_upper = math.meshgrid(math.dual, **{dim: [-1, 1] for dim in self.vector.item_names}, stack_dim=dual('vector'))  # (x=2, y=2, ... vector=x,y,...)
    to_corner = math.sum(lower_upper * to_face, '~vector')
    return self.center + to_corner

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    others_mask = math.range(self.shape['vector']) != math.range(dual(**self.shape['vector'].untyped_dict))
    result = math.exp(math.sum(math.log(self.size) * others_mask, 'vector'))
    return expand(result, dual(side='lower,upper'))  # ~vector

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    return self.center + self.face_normals * self.half_size

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    unit_vectors = math.to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict)))
    vectors = rotate(unit_vectors, self._rot_or_none)
    return vectors * math.vec(dual('side'), lower=-1, upper=1)

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return self.shape.without('vector') & dual(side='lower,upper') & dual(**self.shape['vector'].untyped_dict)

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    return Cuboid(self.face_centers, self.half_size, self._rot_or_none, size_variable=False)
prop half_size : phiml.math._tensors.Tensor
Expand source code
@property
def half_size(self) -> Tensor:
    raise NotImplementedError(self)
prop is_size_variable
Expand source code
@property
def is_size_variable(self):
    raise NotImplementedError(self)
prop lower : phiml.math._tensors.Tensor
Expand source code
@property
def lower(self) -> Tensor:
    raise NotImplementedError(self)
prop rotation_matrix : phiml.math._tensors.Tensor
Expand source code
@property
def rotation_matrix(self) -> Tensor:
    return rotation_matrix(self._rot_or_none, self.shape['vector'], none_to_unit=True)
prop shape
Expand source code
@property
def shape(self):
    raise NotImplementedError()

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size : phiml.math._tensors.Tensor
Expand source code
@property
def size(self) -> Tensor:
    raise NotImplementedError(self)
prop upper : phiml.math._tensors.Tensor
Expand source code
@property
def upper(self) -> Tensor:
    raise NotImplementedError(self)
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return math.prod(self.size, 'vector')

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    loc_to_center = self.global_to_local(location, scale=False, origin='center')
    sgn_surf_delta = math.abs(loc_to_center) - self.half_size
    if instance(self):
        raise NotImplementedError
        self_center, self_radius, sgn_dist, center_delta, center_dist = math.at_min((self.center, self.radius, sgn_dist, center_delta, center_dist), key=abs(sgn_dist), dim=instance)
    # is_inside = math.all(sgn_surf_delta < 0, 'vector')
    # abs_surf_delta = abs(sgn_surf_delta)
    max_sgn_dist = math.max(sgn_surf_delta, 'vector')
    normal_axis = max_sgn_dist == sgn_surf_delta  # ToDo only one if inside
    normal = math.vec_normalize(normal_axis * math.sign(loc_to_center))
    normal = rotate(normal, self._rot_or_none)
    surf_to_center = math.where(normal_axis, math.sign(loc_to_center) * self.half_size, loc_to_center)
    closest_to_center = math.clip(surf_to_center, -self.half_size, self.half_size)
    surface_pos = self.local_to_global(closest_to_center, scale=False, origin='center')
    delta = surface_pos - location
    face_index = expand(0, non_channel(location))
    offset = normal.vector @ surface_pos.vector
    sgn_surf_dist = math.vec_length(delta) * math.sign(max_sgn_dist)
    return sgn_surf_dist, delta, normal, offset, face_index

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor | tuple)
Expand source code
def approximate_signed_distance(self, location: Union[Tensor, tuple]):
    """
    Computes the signed L-infinity norm (manhattan distance) from the location to the nearest side of the box.
    For an outside location `l` with the closest surface point `s`, the distance is `max(abs(l - s))`.
    For inside locations it is `-max(abs(l - s))`.

    Args:
      location: float tensor of shape (batch_size, ..., rank)

    Returns:
      float tensor of shape (*location.shape[:-1], 1).

    """
    # ToDo this underestimates diagonally outside points
    location = self.global_to_local(location, scale=False, origin='center')
    distance = math.abs(location) - self.half_size
    distance = math.max(distance, 'vector')
    distance = math.min(distance, self.shape.instance)  # union for instance dimensions
    return distance

Computes the signed L-infinity norm (manhattan distance) from the location to the nearest side of the box. For an outside location l with the closest surface point s, the distance is max(abs(l - s)). For inside locations it is -max(abs(l - s)).

Args

location
float tensor of shape (batch_size, …, rank)

Returns

float tensor of shape (*location.shape[:-1], 1).

def bounding_radius(self)
Expand source code
def bounding_radius(self):
    return math.vec_length(self.half_size)

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def box(self) ‑> phi.geom._box.Box
Expand source code
def corner_representation(self) -> 'Box':
    assert self._rot_or_none is None, f"corner_representation does not support rotations"
    return Box(self.lower, self.upper)
def center_representation(self, size_variable=True) ‑> phi.geom._box.Cuboid
Expand source code
def center_representation(self, size_variable=True) -> 'Cuboid':
    return Cuboid(self.center, self.half_size, size_variable=size_variable)
def contains(self, other: BaseBox)
Expand source code
def contains(self, other: 'BaseBox'):
    """ Tests if the other box lies fully inside this box. """
    return np.all(other.lower >= self.lower) and np.all(other.upper <= self.upper)

Tests if the other box lies fully inside this box.

def corner_representation(self) ‑> phi.geom._box.Box
Expand source code
def corner_representation(self) -> 'Box':
    assert self._rot_or_none is None, f"corner_representation does not support rotations"
    return Box(self.lower, self.upper)
def cuboid(self, size_variable=True) ‑> phi.geom._box.Cuboid
Expand source code
def center_representation(self, size_variable=True) -> 'Cuboid':
    return Cuboid(self.center, self.half_size, size_variable=size_variable)
def global_to_local(self, global_position: phiml.math._tensors.Tensor, scale=True, origin='lower') ‑> phiml.math._tensors.Tensor
Expand source code
def global_to_local(self, global_position: Tensor, scale=True, origin='lower') -> Tensor:
    """
    Transform world-space coordinates into box-space coordinates.

    Args:
        global_position: World-space coordinates.
        scale: Whether to re-scale the output so that [0, 1] or [-1, 1] represent the box for `origin='lower'` or `origin='center'`, respectively.
        origin: 'lower' or 'center'

    Returns:
        Box-space coordinate `Tensor`
    """
    assert origin in ['lower', 'center', 'upper']
    origin_loc = getattr(self, origin)
    pos = global_position if math.always_close(origin_loc, 0) else global_position - origin_loc
    pos = rotate(pos, self._rot_or_none, invert=True)
    if scale:
        pos /= (self.half_size if origin == 'center' else self.size)
    return pos

Transform world-space coordinates into box-space coordinates.

Args

global_position
World-space coordinates.
scale
Whether to re-scale the output so that [0, 1] or [-1, 1] represent the box for origin='lower' or origin='center', respectively.
origin
'lower' or 'center'

Returns

Box-space coordinate Tensor

def largest(self, dim: str | Sequence | set | ForwardRef('Shape') | Callable | None) ‑> phi.geom._box.BaseBox
Expand source code
def largest(self, dim: DimFilter) -> 'BaseBox':
    dim = self.shape.without('vector').only(dim)
    if not dim:
        return self
    return Box(math.min(self.lower, dim), math.max(self.upper, dim))
def lies_inside(self, location: phiml.math._tensors.Tensor)
Expand source code
def lies_inside(self, location: Tensor):
    assert self._rot_or_none is None, f"Please override lies_inside() for rotated boxes"
    bool_inside = (location >= self.lower) & (location <= self.upper)
    bool_inside = math.all(bool_inside, 'vector')
    bool_inside = math.any(bool_inside, self.shape.instance - instance(location))  # union for instance dimensions
    return bool_inside

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def local_to_global(self, local_position, scale=True, origin='lower')
Expand source code
def local_to_global(self, local_position, scale=True, origin='lower'):
    assert origin in ['lower', 'center', 'upper']
    origin_loc = getattr(self, origin)
    pos = local_position * (self.half_size if origin == 'center' else self.size) if scale else local_position
    return rotate(pos, self._rot_or_none) + origin_loc
def project(self, *dimensions: str)
Expand source code
def project(self, *dimensions: str):
    """ Project this box into a lower-dimensional space. """
    warnings.warn("Box.project(dims) is deprecated. Use Box.vector[dims] instead", DeprecationWarning, stacklevel=2)
    return self.vector[dimensions]

Project this box into a lower-dimensional space.

def push(self,
positions: phiml.math._tensors.Tensor,
outward: bool = True,
shift_amount: float = 0) ‑> phiml.math._tensors.Tensor
Expand source code
def push(self, positions: Tensor, outward: bool = True, shift_amount: float = 0) -> Tensor:
    loc_to_center = self.global_to_local(positions, scale=False, origin='center')
    sgn_dist_from_surface = math.abs(loc_to_center) - self.half_size
    rotation_matrix = self.rotation_matrix
    if outward:
        # --- get negative distances (particles are inside) towards the nearest boundary and add shift_amount ---
        distances_of_interest = (sgn_dist_from_surface == math.max(sgn_dist_from_surface, 'vector')) & (sgn_dist_from_surface < 0)
        shift = distances_of_interest * (sgn_dist_from_surface - shift_amount)
        # ToDo reduce instance dim
    else:  # inward
        shift = (sgn_dist_from_surface + shift_amount) * (sgn_dist_from_surface > 0)  # get positive distances (particles are outside) and add shift_amount
        if instance(self):
            shift, loc_to_center, rotation_matrix = math.at_min((shift, loc_to_center, rotation_matrix), key=math.vec_length(shift), dim=instance)
        shift = math.where(abs(shift) > abs(loc_to_center), abs(loc_to_center), shift)  # ensure inward shift ends at center
    shift = rotate(shift, rotation_matrix)
    return positions + math.where(loc_to_center < 0, 1, -1) * shift

Shifts positions either into or out of geometry.

Args

positions
Tensor holding positions to shift
outward
Flag for indicating inward (False) or outward (True) shift
shift_amount
Minimum distance between positions and surface after shifting.

Returns

Tensor holding shifted positions.

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: Shape) -> Tensor:
    uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector'])
    return self.lower + uniform * self.size

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def sample_uniform_surface(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform_surface(self, *shape: Shape) -> Tensor:
    assert not instance(self), "sample_uniform_surface not yet supported for unions of boxes"
    samples = math.random_uniform(self.shape.non_singleton.non_instance, *shape, low=self.lower, high=self.upper)
    which = math.random_uniform(samples.shape.without('vector'))
    lo_or_up = math.where(which > .5, self.upper, self.lower)
    which = which * 2 % 1
    # --- which axis ---
    areas = self.face_areas
    total_area = math.sum(areas)
    frac_area = math.sum(areas / total_area, '~side')
    cum_area = math.cumulative_sum(frac_area, '~vector')
    axis = math.min(math.where(which <= cum_area, math.range(self.shape['vector'].as_dual()), self.spatial_rank), '~vector')
    axis_one_hot = math.scatter(math.zeros(samples.shape, dtype=bool), expand(axis, channel(index='vector')), True, treat_as_batch=samples.shape.without('vector'))
    math.assert_close(1, math.sum(axis_one_hot, 'vector'))
    samples = math.where(axis_one_hot, lo_or_up, samples)
    return samples
def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._box.BaseBox
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'BaseBox':
    return Cuboid(self.center, self.half_size * factor, size_variable=True)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def smallest(self, dim: str | Sequence | set | ForwardRef('Shape') | Callable | None) ‑> phi.geom._box.BaseBox
Expand source code
def smallest(self, dim: DimFilter) -> 'BaseBox':
    dim = self.shape.without('vector').only(dim)
    if not dim:
        return self
    return Box(math.max(self.lower, dim), math.min(self.upper, dim))
class Box (lower: phiml.math._tensors.Tensor = None,
upper: phiml.math._tensors.Tensor = None,
**size: float | phiml.math._tensors.Tensor | tuple | list | None)
Expand source code
class Box(BaseBox, metaclass=BoxType):
    """
    Simple cuboid defined by location of lower and upper corner in physical space.

    Boxes can be constructed either from two positional vector arguments `(lower, upper)` or by specifying the limits by dimension name as `kwargs`.

    Examples:
        >>> Box(x=1, y=1)  # creates a two-dimensional unit box with `lower=(0, 0)` and `upper=(1, 1)`.
        >>> Box(x=(None, 1), y=(0, None)  # creates a Box with `lower=(-inf, 0)` and `upper=(1, inf)`.

        The slicing constructor was updated in version 2.2 and now requires the dimension order as the first argument.

        >>> Box['x,y', 0:1, 0:1]  # creates a two-dimensional unit box with `lower=(0, 0)` and `upper=(1, 1)`.
        >>> Box['x,y', :1, 0:]  # creates a Box with `lower=(-inf, 0)` and `upper=(1, inf)`.
    """

    def __init__(self, lower: Tensor = None, upper: Tensor = None, **size: Optional[Union[float, Tensor, tuple, list]]):
        """
        Args:
          lower: physical location of lower corner
          upper: physical location of upper corner
          **size: Specify size by dimension, either as `int` or `tuple` containing (lower, upper).
        """
        if lower is not None:
            assert isinstance(lower, Tensor), f"lower must be a Tensor but got {type(lower)}"
            assert 'vector' in lower.shape, "lower must have a vector dimension"
            assert lower.vector.item_names is not None, "vector dimension of lower must list spatial dimension order"
            self._lower = lower
        if upper is not None:
            assert isinstance(upper, Tensor), f"upper must be a Tensor but got {type(upper)}"
            assert 'vector' in upper.shape, "lower must have a vector dimension"
            assert upper.vector.item_names is not None, "vector dimension of lower must list spatial dimension order"
            self._upper = upper
        else:
            lower = []
            upper = []
            for item in size.values():
                if isinstance(item, (tuple, list)):
                    assert len(item) == 2, f"Box kwargs must be either dim=upper or dim=(lower,upper) but got {item}"
                    lo, up = item
                    lower.append(lo)
                    upper.append(up)
                elif item is None:
                    lower.append(-INF)
                    upper.append(INF)
                else:
                    lower.append(0)
                    upper.append(item)
            lower = [-INF if l is None else l for l in lower]
            upper = [INF if u is None else u for u in upper]
            self._upper = math.wrap(upper, math.channel(vector=tuple(size.keys())))
            self._lower = math.wrap(lower, math.channel(vector=tuple(size.keys())))
        vector_shape = self._lower.shape & self._upper.shape
        self._lower = math.expand(self._lower, vector_shape)
        self._upper = math.expand(self._upper, vector_shape)
        if self.size.vector.item_names is None:
            warnings.warn("Creating a Box without item names prevents certain operations like project()", DeprecationWarning, stacklevel=2)
        self._shape = self._lower.shape & self._upper.shape

    def __getitem__(self, item):
        item = _keep_vector(slicing_dict(self, item))
        return Box(self._lower[item], self._upper[item])

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Geometry':
        if all(isinstance(v, Box) for v in values):
            return NotImplemented  # stack attributes
        else:
            return Geometry.__stack__(values, dim, **kwargs)

    def without(self, dims: Tuple[str, ...]):
        remaining = list(self.shape.get_item_names('vector'))
        for dim in dims:
            if dim in remaining:
                remaining.remove(dim)
        return self.vector[remaining]

    def __variable_attrs__(self):
        return '_lower', '_upper'

    def __value_attrs__(self):
        return '_lower', '_upper'

    @property
    def _rot_or_none(self):
        return None

    @property
    def shape(self):
        if self._lower is None or self._upper is None:
            return self._shape
        return self._lower.shape & self._upper.shape

    @property
    def lower(self):
        return self._lower

    @property
    def upper(self):
        return self._upper

    @property
    def size(self):
        return self.upper - self.lower

    @property
    def center(self):
        return 0.5 * (self.lower + self.upper)

    @property
    def half_size(self):
        return self.size * 0.5

    @property
    def is_size_variable(self):
        return False

    def at(self, center: Tensor) -> 'BaseBox':
        return Cuboid(center, self.half_size, None)

    def shifted(self, delta, **delta_by_dim) -> 'Box':
        return Box(self.lower + delta, self.upper + delta)

    def rotated(self, angle) -> 'Cuboid':
        return self.center_representation().rotated(angle)

    def __mul__(self, other):
        if not isinstance(other, Box):
            return NotImplemented
        lower = self._lower.vector.unstack(self.spatial_rank) + other._lower.vector.unstack(other.spatial_rank)
        upper = self._upper.vector.unstack(self.spatial_rank) + other._upper.vector.unstack(other.spatial_rank)
        names = self._upper.vector.item_names + other._upper.vector.item_names
        lower = math.stack(lower, math.channel(vector=names))
        upper = math.stack(upper, math.channel(vector=names))
        return Box(lower, upper)

    def bounding_half_extent(self):
        return self.half_size

    def __repr__(self):
        if self._lower is None or self._upper is None:  # traced
            return f"Box[traced, shape={self._shape}]"
        if self.shape.non_channel.volume == 1:
            item_names = self.size.vector.item_names
            if item_names:
                return f"Box({', '.join([f'{dim}=({lo}, {up})' for dim, lo, up in zip(item_names, self._lower, self._upper)])})"
            else:  # deprecated
                return 'Box[%s at %s]' % ('x'.join([str(x) for x in self.size.numpy().flatten()]), ','.join([str(x) for x in self.lower.numpy().flatten()]))
        else:
            return f'Box[shape={self.shape}]'

Simple cuboid defined by location of lower and upper corner in physical space.

Boxes can be constructed either from two positional vector arguments (lower, upper) or by specifying the limits by dimension name as kwargs.

Examples

>>> Box(x=1, y=1)  # creates a two-dimensional unit box with `lower=(0, 0)` and `upper=(1, 1)`.
>>> Box(x=(None, 1), y=(0, None)  # creates a Box with `lower=(-inf, 0)` and `upper=(1, inf)`.

The slicing constructor was updated in version 2.2 and now requires the dimension order as the first argument.

>>> Box['x,y', 0:1, 0:1]  # creates a two-dimensional unit box with `lower=(0, 0)` and `upper=(1, 1)`.
>>> Box['x,y', :1, 0:]  # creates a Box with `lower=(-inf, 0)` and `upper=(1, inf)`.

Args

lower
physical location of lower corner
upper
physical location of upper corner
**size
Specify size by dimension, either as int or tuple containing (lower, upper).

Ancestors

  • phi.geom._box.BaseBox
  • phi.geom._geom.Geometry

Instance variables

prop center
Expand source code
@property
def center(self):
    return 0.5 * (self.lower + self.upper)

Center location in single channel dimension.

prop half_size
Expand source code
@property
def half_size(self):
    return self.size * 0.5
prop is_size_variable
Expand source code
@property
def is_size_variable(self):
    return False
prop lower
Expand source code
@property
def lower(self):
    return self._lower
prop shape
Expand source code
@property
def shape(self):
    if self._lower is None or self._upper is None:
        return self._shape
    return self._lower.shape & self._upper.shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size
Expand source code
@property
def size(self):
    return self.upper - self.lower
prop upper
Expand source code
@property
def upper(self):
    return self._upper

Methods

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._box.BaseBox
Expand source code
def at(self, center: Tensor) -> 'BaseBox':
    return Cuboid(center, self.half_size, None)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self)
Expand source code
def bounding_half_extent(self):
    return self.half_size

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def rotated(self, angle) ‑> phi.geom._box.Cuboid
Expand source code
def rotated(self, angle) -> 'Cuboid':
    return self.center_representation().rotated(angle)

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def shifted(self, delta, **delta_by_dim) ‑> phi.geom._box.Box
Expand source code
def shifted(self, delta, **delta_by_dim) -> 'Box':
    return Box(self.lower + delta, self.upper + delta)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
def without(self, dims: Tuple[str, ...])
Expand source code
def without(self, dims: Tuple[str, ...]):
    remaining = list(self.shape.get_item_names('vector'))
    for dim in dims:
        if dim in remaining:
            remaining.remove(dim)
    return self.vector[remaining]
class Cuboid (center: phiml.math._tensors.Tensor = 0,
half_size: phiml.math._tensors.Tensor | float = None,
rotation: phiml.math._tensors.Tensor | None = None,
size_variable=True,
**size: phiml.math._tensors.Tensor | float)
Expand source code
class Cuboid(BaseBox):
    """Box specified by center position and half size."""

    def __init__(self,
                 center: Tensor = 0,
                 half_size: Union[float, Tensor] = None,
                 rotation: Optional[Tensor] = None,
                 size_variable=True,
                 **size: Union[float, Tensor]):
        """
        Args:
            center: Center position
            half_size: Half-size of the cuboid as vector or scalar
            rotation: Rotation angle(s) or rotation matrix.
            **size: Alternative way of specifying the size. If used, `half_size` must not be specified.
        """
        if half_size is not None:
            assert isinstance(half_size, Tensor), "half_size must be a Tensor"
            assert 'vector' in half_size.shape, f"Cuboid size must have a 'vector' dimension."
            assert half_size.shape.get_item_names('vector') is not None, f"Vector dimension must list spatial dimensions as item names. Use the syntax Cuboid(x=x, y=y) to assign names."
            self._half_size = half_size
        else:
            self._half_size = math.wrap(tuple(size.values()), math.channel(vector=tuple(size.keys()))) * 0.5
        center = wrap(center)
        if 'vector' not in center.shape or center.shape.get_item_names('vector') is None:
            center = math.expand(center, channel(self._half_size))
        self._center = center
        self._rotation_matrix = None if rotation is None else rotation_matrix(rotation, matrix_dim=center.shape['vector'])
        self._size_variable = size_variable

    def __repr__(self):
        return f"Cuboid(center={self._center}, half_size={self._half_size})"

    def __getitem__(self, item) -> 'Cuboid':
        item = _keep_vector(slicing_dict(self, item))
        return Cuboid(self._center[item], self._half_size[item], math.slice(self._rotation_matrix, item), size_variable=self._size_variable)

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Geometry':
        if all(isinstance(v, Cuboid) for v in values):
            size_variable = any([c._size_variable for c in values])
            if any(v._rotation_matrix is not None for v in values):
                matrices = [v._rotation_matrix for v in values]
                if any(m is None for m in matrices):
                    any_angle = math.rotation_angles([m for m in matrices if m is not None][0])
                    unit_matrix = math.rotation_matrix(any_angle * 0)
                    matrices = [unit_matrix if m is None else m for m in matrices]
                rotation = stack(matrices, dim, **kwargs)
            else:
                rotation = None
            return Cuboid(stack([v.center for v in values], dim, **kwargs), stack([v.half_size for v in values], dim, **kwargs), rotation, size_variable=size_variable)
        else:
            return Geometry.__stack__(values, dim, **kwargs)

    def __variable_attrs__(self):
        return ('_center', '_half_size', '_rotation_matrix') if self._size_variable else ('_center', '_rotation_matrix')

    def __value_attrs__(self):
        return '_center',

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

    @property
    def half_size(self):
        return self._half_size

    @property
    def shape(self):
        if self._center is None or self._half_size is None:
            return None
        return self._center.shape & self._half_size.shape

    @property
    def size(self):
        return 2 * self._half_size

    @property
    def lower(self):
        return self._center - self._half_size

    @property
    def upper(self):
        return self._center + self._half_size

    @property
    def _rot_or_none(self):
        return self._rotation_matrix

    @property
    def is_size_variable(self):
        return self._size_variable

    def at(self, center: Tensor) -> 'Cuboid':
        return Cuboid(center, self.half_size, self._rotation_matrix, size_variable=self._size_variable)

    def rotated(self, angle) -> 'Cuboid':
        if self._rotation_matrix is None:
            return Cuboid(self._center, self._half_size, angle, size_variable=self._size_variable)
        else:
            matrix = self._rotation_matrix @ (angle if dual(angle) else math.rotation_matrix(angle))
            return Cuboid(self._center, self._half_size, matrix, size_variable=self._size_variable)

    def bounding_half_extent(self) -> Tensor:
        if self._rotation_matrix is not None:
            to_face = self.face_normals[{'~side': 0}] * math.rename_dims(self._half_size, 'vector', dual)
            return math.sum(abs(to_face), '~vector')
        return self.half_size

    def lies_inside(self, location: Tensor) -> Tensor:
        location = self.global_to_local(location, scale=False, origin='center')  # scale can only be performed for finite sizes
        bool_inside = abs(location) <= self._half_size
        bool_inside = math.all(bool_inside, 'vector')
        bool_inside = math.any(bool_inside, self.shape.instance - instance(location))  # union for instance dimensions
        return bool_inside

Box specified by center position and half size.

Args

center
Center position
half_size
Half-size of the cuboid as vector or scalar
rotation
Rotation angle(s) or rotation matrix.
**size
Alternative way of specifying the size. If used, half_size must not be specified.

Ancestors

  • phi.geom._box.BaseBox
  • phi.geom._geom.Geometry

Instance variables

prop center
Expand source code
@property
def center(self):
    return self._center

Center location in single channel dimension.

prop half_size
Expand source code
@property
def half_size(self):
    return self._half_size
prop is_size_variable
Expand source code
@property
def is_size_variable(self):
    return self._size_variable
prop lower
Expand source code
@property
def lower(self):
    return self._center - self._half_size
prop shape
Expand source code
@property
def shape(self):
    if self._center is None or self._half_size is None:
        return None
    return self._center.shape & self._half_size.shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size
Expand source code
@property
def size(self):
    return 2 * self._half_size
prop upper
Expand source code
@property
def upper(self):
    return self._center + self._half_size

Methods

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._box.Cuboid
Expand source code
def at(self, center: Tensor) -> 'Cuboid':
    return Cuboid(center, self.half_size, self._rotation_matrix, size_variable=self._size_variable)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    if self._rotation_matrix is not None:
        to_face = self.face_normals[{'~side': 0}] * math.rename_dims(self._half_size, 'vector', dual)
        return math.sum(abs(to_face), '~vector')
    return self.half_size

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    location = self.global_to_local(location, scale=False, origin='center')  # scale can only be performed for finite sizes
    bool_inside = abs(location) <= self._half_size
    bool_inside = math.all(bool_inside, 'vector')
    bool_inside = math.any(bool_inside, self.shape.instance - instance(location))  # union for instance dimensions
    return bool_inside

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle) ‑> phi.geom._box.Cuboid
Expand source code
def rotated(self, angle) -> 'Cuboid':
    if self._rotation_matrix is None:
        return Cuboid(self._center, self._half_size, angle, size_variable=self._size_variable)
    else:
        matrix = self._rotation_matrix @ (angle if dual(angle) else math.rotation_matrix(angle))
        return Cuboid(self._center, self._half_size, matrix, size_variable=self._size_variable)

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

class Cylinder (_center: phiml.math._tensors.Tensor,
radius: phiml.math._tensors.Tensor,
depth: phiml.math._tensors.Tensor,
rotation: phiml.math._tensors.Tensor,
axis: str,
variable_attrs: Tuple[str, ...] = ('_center', 'radius', 'depth', 'rotation'),
value_attrs: Tuple[str, ...] = ())
Expand source code
@sliceable(keepdims='vector')
@dataclass(frozen=True)
class Cylinder(Geometry):
    """
    N-dimensional cylinder.
    Defined by center position, radius, depth, alignment axis, rotation.

    For cylinders whose bottom and top lie outside the domain or are otherwise not needed, you may use `infinite_cylinder` instead, which simplifies computations.
    """

    _center: Tensor
    radius: Tensor
    depth: Tensor
    rotation: Tensor  # rotation matrix
    axis: str

    variable_attrs: Tuple[str, ...] = ('_center', 'radius', 'depth', 'rotation')
    value_attrs: Tuple[str, ...] = ()

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

    @cached_property
    def shape(self) -> Shape:
        return self._center.shape & self.radius.shape & self.depth.shape & batch(self.rotation)

    @cached_property
    def radial_axes(self) -> Sequence[str]:
        return [d for d in self.shape.get_item_names('vector') if d != self.axis]

    @cached_property
    def volume(self) -> math.Tensor:
        return Sphere.volume_from_radius(self.radius, self.spatial_rank - 1) * self.depth

    @cached_property
    def up(self):
        return rotate(vec(**{d: 1 if d == self.axis else 0 for d in self.shape.get_item_names('vector')}), self._rot_or_none)

    @cached_property
    def rotation_matrix(self):
        return rotation_matrix(self.rotation, self.shape['vector'], none_to_unit=True)

    @property
    def _rot_or_none(self):
        return None if self.rotation is None else self.rotation_matrix

    def with_radius(self, radius: Tensor) -> 'Cylinder':
        return replace(self, radius=wrap(radius))

    def with_depth(self, depth: Tensor) -> 'Cylinder':
        return replace(self, depth=wrap(depth))

    def lies_inside(self, location):
        pos = rotate(location - self._center, self._rot_or_none, invert=True)
        r = pos.vector[self.radial_axes]
        h = pos.vector[self.axis]
        inside = (vec_squared(r) <= self.radius**2) & (h >= -.5*self.depth) & (h <= .5*self.depth)
        return math.any(inside, instance(self))  # union for instance dimensions

    def approximate_signed_distance(self, location: Union[Tensor, tuple]):
        location = rotate(location - self._center, self._rot_or_none, invert=True)
        r = location.vector[self.radial_axes]
        h = location.vector[self.axis]
        top_h = .5*self.depth
        # bot_h = -.5*self.depth
        # --- Compute distances ---
        radial_outward = normalize(r, 'vector', epsilon=1e-5)
        surf_r = radial_outward * self.radius
        radial_dist2 = vec_squared(r)
        inside_cyl = radial_dist2 <= self.radius**2
        clamped_r = where(inside_cyl, r, surf_r)
        # --- Closest point on bottom / top ---
        sgn_dist_side = abs(h) - top_h
        # --- Closest point on cylinder ---
        sgn_dist_cyl = length(r, 'vector') - self.radius
        # inside (all <= 0) -> largest SDF, outside (any > 0) -> largest positive SDF
        sgn_dist = maximum(sgn_dist_cyl, sgn_dist_side)
        return math.min(sgn_dist, instance(self))

    def approximate_closest_surface(self, location: Tensor):
        location = rotate(location - self._center, self._rot_or_none, invert=True)
        r = location.vector[self.radial_axes]
        h = location.vector[self.axis]
        top_h = .5*self.depth
        bot_h = -.5*self.depth
        # --- Compute distances ---
        radial_outward = normalize(r, 'vector', epsilon=1e-5)
        surf_r = radial_outward * self.radius
        radial_dist2 = vec_squared(r)
        inside_cyl = radial_dist2 <= self.radius**2
        clamped_r = where(inside_cyl, r, surf_r)
        # --- Closest point on bottom / top ---
        above = h >= 0
        flat_h = where(above, top_h, bot_h)
        on_flat = ncat([flat_h, clamped_r], self._center.shape['vector'])
        normal_flat = where(above, self.up, -self.up)
        # --- Closest point on cylinder ---
        clamped_h = clip(h, bot_h, top_h)
        on_cyl = ncat([surf_r, clamped_h], self._center.shape['vector'])
        normal_cyl = ncat([radial_outward, 0], self._center.shape['vector'], expand_values=True)
        # --- Choose closest ---
        d_flat = length(on_flat - location, 'vector')
        d_cyl = length(on_cyl - location, 'vector')
        flat_closer = d_flat <= d_cyl
        surf_point = where(flat_closer, on_flat, on_cyl)
        inside = inside_cyl & (h >= bot_h) & (h <= top_h)
        sgn_dist = minimum(d_flat, d_cyl) * where(inside, -1, 1)
        delta = surf_point - location
        normal = where(flat_closer, normal_flat, normal_cyl)
        delta = rotate(delta, self._rot_or_none)
        normal = rotate(normal, self._rot_or_none)
        idx = None
        if instance(self):
            sgn_dist, delta, normal, idx = math.min((sgn_dist, delta, normal, range), instance(self), key=sgn_dist)
        return sgn_dist, delta, normal, None, idx

    def sample_uniform(self, *shape: math.Shape):
        r = Sphere(self._center[self.radial_axes], self.radius).sample_uniform(*shape)
        h = math.random_uniform(*shape, -.5*self.depth, .5*self.depth)
        rh = ncat([r, h], self._center.shape['vector'])
        return rotate(rh, self._rot_or_none)

    def bounding_radius(self):
        return length(vec(rad=self.radius, dep=.5*self.depth), 'vector')

    def bounding_half_extent(self, epsilon=1e-5):
        if self.rotation is not None:
            tip = abs(self.up) * .5 * self.depth
            return tip + self.radius * sqrt(maximum(epsilon, 1 - self.up**2))
        return ncat([.5*self.depth, expand(self.radius, channel(vector=self.radial_axes))], self._center.shape['vector'], expand_values=True)

    def at(self, center: Tensor) -> 'Geometry':
        return replace(self, _center=center)

    def rotated(self, angle):
        rot = self.rotation_matrix @ rotation_matrix(angle) if self.rotation is not None else rotation_matrix(angle)
        return replace(self, rotation=rot)

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        return replace(self, radius=self.radius * factor, depth=self.depth * factor)

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError(f"Cylinder.faces not implemented.")

    @property
    def face_centers(self) -> Tensor:
        fac = wrap([-.5, .5, 0], dual(shell='bottom,top,lateral'))
        return self.center + fac * self.depth * self.up

    @property
    def face_areas(self) -> Tensor:
        flat = Sphere.volume_from_radius(self.radius, self.spatial_rank - 1)
        lateral = 2*PI*self.radius * self.depth
        return stack({'bottom': flat, 'top': flat, 'lateral': lateral}, dual('shell'), expand_values=True)

    @property
    def face_normals(self) -> Tensor:
        raise NotImplementedError

    @property
    def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

    @property
    def face_shape(self) -> Shape:
        return self.shape.without('vector') & dual(shell='bottom,top,lateral')

    @property
    def corners(self) -> Tensor:
        return math.zeros(self.shape & dual(corners=0))

    def __eq__(self, other):
        return Geometry.__eq__(self, other)

    def vertex_rings(self, count: Shape) -> Tensor:
        if self.spatial_rank == 3:
            angle = linspace(0, 2*PI, count)
            h = stack({'bot': -.5 * self.depth, 'top': .5 * self.depth}, '~face')
            s = sin(angle) * self.radius
            c = cos(angle) * self.radius
            r = stack([s, c], channel(vector=self.radial_axes))
            x = ncat([h, r], self._center.shape['vector'], expand_values=True)
            return rotate(x, self._rot_or_none) + self._center
        raise NotImplementedError

N-dimensional cylinder. Defined by center position, radius, depth, alignment axis, rotation.

For cylinders whose bottom and top lie outside the domain or are otherwise not needed, you may use infinite_cylinder() instead, which simplifies computations.

Ancestors

  • phi.geom._geom.Geometry

Class variables

var axis : str
var depth : phiml.math._tensors.Tensor
var radius : phiml.math._tensors.Tensor
var rotation : phiml.math._tensors.Tensor
var value_attrs : Tuple[str, ...]
var variable_attrs : Tuple[str, ...]

Instance variables

prop boundary_elements : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._center

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    return math.zeros(self.shape & dual(corners=0))

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    flat = Sphere.volume_from_radius(self.radius, self.spatial_rank - 1)
    lateral = 2*PI*self.radius * self.depth
    return stack({'bottom': flat, 'top': flat, 'lateral': lateral}, dual('shell'), expand_values=True)

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    fac = wrap([-.5, .5, 0], dual(shell='bottom,top,lateral'))
    return self.center + fac * self.depth * self.up

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    raise NotImplementedError

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return self.shape.without('vector') & dual(shell='bottom,top,lateral')

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError(f"Cylinder.faces not implemented.")
var radial_axes : Sequence[str]
Expand source code
@cached_property
def radial_axes(self) -> Sequence[str]:
    return [d for d in self.shape.get_item_names('vector') if d != self.axis]
var rotation_matrix
Expand source code
@cached_property
def rotation_matrix(self):
    return rotation_matrix(self.rotation, self.shape['vector'], none_to_unit=True)
var shape : phiml.math._shape.Shape
Expand source code
@cached_property
def shape(self) -> Shape:
    return self._center.shape & self.radius.shape & self.depth.shape & batch(self.rotation)
var up
Expand source code
@cached_property
def up(self):
    return rotate(vec(**{d: 1 if d == self.axis else 0 for d in self.shape.get_item_names('vector')}), self._rot_or_none)
var volume : phiml.math._tensors.Tensor
Expand source code
@cached_property
def volume(self) -> math.Tensor:
    return Sphere.volume_from_radius(self.radius, self.spatial_rank - 1) * self.depth

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor)
Expand source code
def approximate_closest_surface(self, location: Tensor):
    location = rotate(location - self._center, self._rot_or_none, invert=True)
    r = location.vector[self.radial_axes]
    h = location.vector[self.axis]
    top_h = .5*self.depth
    bot_h = -.5*self.depth
    # --- Compute distances ---
    radial_outward = normalize(r, 'vector', epsilon=1e-5)
    surf_r = radial_outward * self.radius
    radial_dist2 = vec_squared(r)
    inside_cyl = radial_dist2 <= self.radius**2
    clamped_r = where(inside_cyl, r, surf_r)
    # --- Closest point on bottom / top ---
    above = h >= 0
    flat_h = where(above, top_h, bot_h)
    on_flat = ncat([flat_h, clamped_r], self._center.shape['vector'])
    normal_flat = where(above, self.up, -self.up)
    # --- Closest point on cylinder ---
    clamped_h = clip(h, bot_h, top_h)
    on_cyl = ncat([surf_r, clamped_h], self._center.shape['vector'])
    normal_cyl = ncat([radial_outward, 0], self._center.shape['vector'], expand_values=True)
    # --- Choose closest ---
    d_flat = length(on_flat - location, 'vector')
    d_cyl = length(on_cyl - location, 'vector')
    flat_closer = d_flat <= d_cyl
    surf_point = where(flat_closer, on_flat, on_cyl)
    inside = inside_cyl & (h >= bot_h) & (h <= top_h)
    sgn_dist = minimum(d_flat, d_cyl) * where(inside, -1, 1)
    delta = surf_point - location
    normal = where(flat_closer, normal_flat, normal_cyl)
    delta = rotate(delta, self._rot_or_none)
    normal = rotate(normal, self._rot_or_none)
    idx = None
    if instance(self):
        sgn_dist, delta, normal, idx = math.min((sgn_dist, delta, normal, range), instance(self), key=sgn_dist)
    return sgn_dist, delta, normal, None, idx

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor | tuple)
Expand source code
def approximate_signed_distance(self, location: Union[Tensor, tuple]):
    location = rotate(location - self._center, self._rot_or_none, invert=True)
    r = location.vector[self.radial_axes]
    h = location.vector[self.axis]
    top_h = .5*self.depth
    # bot_h = -.5*self.depth
    # --- Compute distances ---
    radial_outward = normalize(r, 'vector', epsilon=1e-5)
    surf_r = radial_outward * self.radius
    radial_dist2 = vec_squared(r)
    inside_cyl = radial_dist2 <= self.radius**2
    clamped_r = where(inside_cyl, r, surf_r)
    # --- Closest point on bottom / top ---
    sgn_dist_side = abs(h) - top_h
    # --- Closest point on cylinder ---
    sgn_dist_cyl = length(r, 'vector') - self.radius
    # inside (all <= 0) -> largest SDF, outside (any > 0) -> largest positive SDF
    sgn_dist = maximum(sgn_dist_cyl, sgn_dist_side)
    return math.min(sgn_dist, instance(self))

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    return replace(self, _center=center)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self, epsilon=1e-05)
Expand source code
def bounding_half_extent(self, epsilon=1e-5):
    if self.rotation is not None:
        tip = abs(self.up) * .5 * self.depth
        return tip + self.radius * sqrt(maximum(epsilon, 1 - self.up**2))
    return ncat([.5*self.depth, expand(self.radius, channel(vector=self.radial_axes))], self._center.shape['vector'], expand_values=True)

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self)
Expand source code
def bounding_radius(self):
    return length(vec(rad=self.radius, dep=.5*self.depth), 'vector')

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location)
Expand source code
def lies_inside(self, location):
    pos = rotate(location - self._center, self._rot_or_none, invert=True)
    r = pos.vector[self.radial_axes]
    h = pos.vector[self.axis]
    inside = (vec_squared(r) <= self.radius**2) & (h >= -.5*self.depth) & (h <= .5*self.depth)
    return math.any(inside, instance(self))  # union for instance dimensions

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle)
Expand source code
def rotated(self, angle):
    rot = self.rotation_matrix @ rotation_matrix(angle) if self.rotation is not None else rotation_matrix(angle)
    return replace(self, rotation=rot)

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape)
Expand source code
def sample_uniform(self, *shape: math.Shape):
    r = Sphere(self._center[self.radial_axes], self.radius).sample_uniform(*shape)
    h = math.random_uniform(*shape, -.5*self.depth, .5*self.depth)
    rh = ncat([r, h], self._center.shape['vector'])
    return rotate(rh, self._rot_or_none)

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    return replace(self, radius=self.radius * factor, depth=self.depth * factor)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def vertex_rings(self, count: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def vertex_rings(self, count: Shape) -> Tensor:
    if self.spatial_rank == 3:
        angle = linspace(0, 2*PI, count)
        h = stack({'bot': -.5 * self.depth, 'top': .5 * self.depth}, '~face')
        s = sin(angle) * self.radius
        c = cos(angle) * self.radius
        r = stack([s, c], channel(vector=self.radial_axes))
        x = ncat([h, r], self._center.shape['vector'], expand_values=True)
        return rotate(x, self._rot_or_none) + self._center
    raise NotImplementedError
def with_depth(self, depth: phiml.math._tensors.Tensor) ‑> phi.geom._cylinder.Cylinder
Expand source code
def with_depth(self, depth: Tensor) -> 'Cylinder':
    return replace(self, depth=wrap(depth))
def with_radius(self, radius: phiml.math._tensors.Tensor) ‑> phi.geom._cylinder.Cylinder
Expand source code
def with_radius(self, radius: Tensor) -> 'Cylinder':
    return replace(self, radius=wrap(radius))
class Geometry
Expand source code
class Geometry:
    """
    Abstract base class for N-dimensional shapes.

    Main implementing classes:

    * Sphere
    * box family: box (generator), Box, Cuboid, BaseBox

    All geometry objects support batching.
    Thereby any parameter defining the geometry can be varied along arbitrary batch dims.
    All batch dimensions are listed in Geometry.shape.

    Property getters (`@property`, such as `shape`), save for getters, must not depend on any variables marked as *variable* via `__variable_attrs__()` as these may be `None` during tracing.
    Equality checks must also take this into account.
    """

    @property
    def center(self) -> Tensor:
        """
        Center location in single channel dimension.
        """
        raise NotImplementedError(self.__class__)

    @property
    def shape(self) -> Shape:
        """
        The `shape` of a `Geometry` consists of the following dimensions:

        * A single *channel* dimension called `'vector'` specifying the physical space
        * Instance dimensions denote that this geometry consists of multiple copies in the same space
        * Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
        * Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
        """
        raise NotImplementedError(self.__class__)

    @property
    def volume(self) -> Tensor:
        """
        `phi.math.Tensor` representing the volume of each element.
        The result retains batch, spatial and instance dimensions.
        """
        raise NotImplementedError(self.__class__)

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError(self.__class__)

    @property
    def face_centers(self) -> Tensor:
        """
        Center of face connecting a pair of cells. Shape `(elements, ~, vector)`.
        Here, `~` represents arbitrary internal dual dimensions, such as `~staggered_direction` or `~elements`.
        Returns 0-vectors for unconnected cells.
        """
        raise NotImplementedError(self.__class__)

    @property
    def face_areas(self) -> Tensor:
        """
        Area of face connecting a pair of cells. Shape `(elements, ~)`.
        Returns 0 for unconnected cells.
        """
        raise NotImplementedError(self.__class__)

    @property
    def face_normals(self) -> Tensor:
        """
        Normal vectors of cell faces, including boundary faces. Shape `(elements, ~, vector)`.
        For meshes, The vectors point out of the primal cells and into the dual cells.

        Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.
        """
        raise NotImplementedError(self.__class__)

    @property
    def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
        """
        Slices on the primal dimensions to mark boundary elements.
        Grids and meshes have no boundary elements and return `{}`.
        Dynamic graphs can define boundary elements for obstacles and walls.

        Returns:
            Map from `name` to slicing `dict`.
        """
        raise NotImplementedError(self.__class__)

    @property
    def boundary_faces(self) -> Dict[str, Dict[str, slice]]:
        """
        Slices on the dual dimensions to mark boundary faces.

        Regular grids use the keys (dim, is_upper) to identify boundaries.
        Unstructured meshes use string identifiers for the boundaries.
        Dynamic graphs return slices along the dual dimensions.

        Returns:
            Map from `name` to slicing `dict`.
        """
        raise NotImplementedError(self.__class__)

    @property
    def face_shape(self) -> Shape:
        """
        Returns:
            Full Shape to identify each face of this `Geometry`, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element.
            If this `Geometry` has no faces, returns an empty `Shape`.
        """
        return None

    @property
    def sets(self) -> Dict[str, Shape]:
        if self.face_shape and self.face_shape != self.shape and self.face_shape.volume > 0:
            return {'center': non_batch(self)-'vector', 'face': self.face_shape.non_batch}
        else:
            return {'center': non_batch(self)-'vector'}

    def get_points(self, set_key: str) -> Tensor:
        if set_key == 'center':
            return self.center
        elif set_key == 'face':
            return self.face_centers
        else:
            raise ValueError(f"Unknown set: '{set_key}'")

    def get_boundary(self, set_key: str) -> Dict[str, Dict[str, slice]]:
        if set_key == 'center':
            return self.boundary_elements
        elif set_key == 'face':
            return self.boundary_faces
        else:
            raise ValueError(f"Unknown set: '{set_key}'")

    @property
    def corners(self) -> Tensor:
        """
        Returns:
            Corner locations as `phiml.math.Tensor`.
            Corners belonging to one object or cell are listed along dual dimensions.
            If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.
        """
        raise NotImplementedError(self.__class__)

    def integrate_surface(self, face_values: Tensor, divide_volume=False) -> Tensor:
        """
        Multiplies `values´ by the corresponding face area, computes the sum over all faces and divides by the cell volume.
        ∑ values * A.

        Args:
            face_values: Values sampled at the face centers.
            divide_volume: Whether to divide by the cell `volume´

        Returns:
            `Tensor` of values sampled at the centroids.
        """
        result = math.sum(face_values * self.face_areas, self.face_shape.dual)
        return result / self.volume if divide_volume else result

    def integrate_flux(self, flux: Tensor, divide_volume=False) -> Tensor:
        assert 'vector' in flux.shape, f"flux must have a 'vector' dimension but got {flux.shape}"
        result = math.sum(flux.vector @ (self.face_normals * self.face_areas).vector, self.face_shape.dual)
        return result / self.volume if divide_volume else result

    # def resample_to_faces(self, values: Tensor, boundary: Extrapolation, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def resample_to_centers(self, values: Tensor, boundary: Extrapolation, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def centered_gradient_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def staggered_gradient_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def divergence_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def laplace_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def centered_curl_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)
    #
    # def staggered_curl_of(self, values: Tensor, boundary: Extrapolation, dims=None, **kwargs):
    #     raise NotImplementedError(self.__class__)

    def unstack(self, dimension: str) -> tuple:
        """
        Unstacks this Geometry along the given dimension.
        The shapes of the returned geometries are reduced by `dimension`.

        Args:
            dimension: dimension along which to unstack

        Returns:
            geometries: tuple of length equal to `geometry.shape.get_size(dimension)`
        """
        warnings.warn(f"Geometry.unstack() is deprecated. Use math.unstack(geometry) instead.", DeprecationWarning)
        return math.unstack(self, dimension)

    @property
    def spatial_rank(self) -> int:
        """ Number of spatial dimensions of the geometry, 1 = 1D, 2 = 2D, 3 = 3D, etc. """
        return self.shape.get_size('vector')

    def lies_inside(self, location: Tensor) -> Tensor:
        """
        Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

        When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

        Args:
          location: float tensor of shape (batch_size, ..., rank)

        Returns:
          bool tensor of shape (*location.shape[:-1], 1).

        """
        raise NotImplementedError(self.__class__)

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        """
        Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

        Args:
            location: `Tensor` with a single channel dimension called vector. Can have arbitrary other dimensions.

        Returns:
            signed_distance: Scalar signed distance from `location`  to the closest point on the surface.
                Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
            delta: Vector-valued distance vector from `location` to the closest point on the surface.
            normal: Closest surface normal vector.
            offset: Min distance of a surface-tangential plane from 0 as a scalar.
            face_index: (Optional) An index vector pointing at the closest face.
        """
        raise NotImplementedError(self.__class__)

    def approximate_signed_distance(self, location: Tensor) -> Tensor:
        """
        Computes the approximate distance from location to the surface of the geometry.
        Locations outside return positive values, inside negative values and zero exactly at the boundary.

        The exact distance metric used depends on the geometry.
        The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry.
        The distance metric is differentiable and its gradients are bounded at every point in space.

        When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned.
        This also holds for negative distances.

        Args:
            location: `Tensor` with one channel dim `vector` matching the geometry's `vector` dim.

        Returns:
            Float `Tensor`
        """
        raise NotImplementedError(self.__class__)

    def approximate_fraction_inside(self, other_geometry: 'Geometry', balance: Union[Tensor, Number] = 0.5) -> Tensor:
        """
        Computes the approximate overlap between the geometry and a small other geometry.
        Returns 1.0 if `other_geometry` is fully enclosed in this geometry and 0.0 if there is no overlap.
        Close to the surface of this geometry, the fraction filled is differentiable w.r.t. the location and size of `other_geometry`.

        To call this method on batches of geometries of same shape, pass a batched Geometry instance.
        The result tensor will match the batch shape of `other_geometry`.

        The result may only be accurate in special cases.
        The given geometries may be approximated as spheres or boxes using `bounding_radius()` and `bounding_half_extent()`.

        The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using `approximate_signed_distance()`.

        Args:
            other_geometry: `Geometry` or geometry batch for which to compute the overlap with `self`.
            balance: Mid-level between 0 and 1, default 0.5.
                This value is returned when exactly half of `other_geometry` lies inside `self`.
                `0.5 < balance <= 1` makes `self` seem larger while `0 <= balance < 0.5`makes `self` seem smaller.

        Returns:
          fraction of cell volume lying inside the geometry. float tensor of shape (other_geometry.batch_shape, 1).

        """
        assert isinstance(other_geometry, Geometry)
        radius = other_geometry.bounding_radius()
        location = other_geometry.center
        distance = self.approximate_signed_distance(location)
        inside_fraction = balance - distance / radius
        inside_fraction = math.clip(inside_fraction, 0, 1)
        return inside_fraction

    def push(self, positions: Tensor, outward: bool = True, shift_amount: float = 0) -> Tensor:
        """
        Shifts positions either into or out of geometry.

        Args:
            positions: Tensor holding positions to shift
            outward: Flag for indicating inward (False) or outward (True) shift
            shift_amount: Minimum distance between positions and surface after shifting.

        Returns:
            Tensor holding shifted positions.
        """
        from ._geom_ops import expel
        return expel(self, positions, min_separation=shift_amount, invert=not outward)

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        """
        Samples uniformly distributed random points inside this volume.

        Args:
            *shape: How many points to sample per individual geometry.

        Returns:
            `Tensor` containing all dimensions from `Geometry.shape`, `shape` as well as a `channel` dimension `vector` matching the dimensionality of this `Geometry`.
        """
        raise NotImplementedError(self.__class__)

    def bounding_radius(self) -> Tensor:
        """
        Returns the radius of a Sphere object that fully encloses this geometry.
        The sphere is centered at the center of this geometry.

        If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part.
        If these dims are not present on the result, all parts are assumed to have the same bounds.
        """
        raise NotImplementedError(self.__class__)

    def bounding_half_extent(self) -> Tensor:
        """
        The bounding half-extent sets a limit on the outer-most point for each coordinate axis.
        Each component is non-negative.

        Let the bounding half-extent have value `e` in dimension `d` (`extent[...,d] = e`).
        Then, no point of the geometry lies further away from its center point than `e` along `d` (in both axis directions).

        If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part.
        If these dims are not present on the result, all parts are assumed to have the same bounds.
        """
        raise NotImplementedError(self.__class__)

    def bounding_box(self) -> 'BaseBox':
        """
        Returns the approximately smallest axis-aligned box that contains this `Geometry`.
        The center of the box may not be equal to `self.center`.

        Returns:
            `Box` or `Cuboid` that fully contains this `Geometry`.
        """
        center = self.center
        half = self.bounding_half_extent()
        min_vec = math.min(center - half, dim=center.shape.non_batch.non_channel)
        max_vec = math.max(center + half, dim=center.shape.non_batch.non_channel)
        from ._box import Box
        return Box(min_vec, max_vec)

    def bounding_sphere(self):
        center = self.bounding_box().center
        dist = length(self.center - center) + self.bounding_radius()
        from ._sphere import Sphere
        return Sphere(center, dist)

    def shifted(self, delta: Tensor) -> 'Geometry':
        """
        Returns a translated version of this geometry.

        See Also:
            `Geometry.at()`.

        Args:
          delta: direction vector
          delta: Tensor:

        Returns:
          Geometry: shifted geometry

        """
        return self.at(self.center + delta)

    def at(self, center: Tensor) -> 'Geometry':
        """
        Returns a copy of this `Geometry` with the center at `center`.
        This is equal to calling `self @ center`.

        See Also:
            `Geometry.shifted()`.

        Args:
            center: New center as `Tensor`.

        Returns:
            `Geometry`.
        """
        raise NotImplementedError(self.__class__)

    def __matmul__(self, other):
        if isinstance(other, (Tensor, float, int)):
            return self.at(other)
        return NotImplemented

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        """
        Returns a rotated version of this geometry.
        The geometry is rotated about its center point.

        Args:
            angle: Delta rotation.
                Either

                * Angle(s): scalar angle in 2d or euler angles along `vector` in 3D or higher.
                * Matrix: d⨯d rotation matrix

        Returns:
            Rotated `Geometry`
        """
        raise NotImplementedError(self.__class__)

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        """
        Scales each individual geometry by `factor`.
        The individual `center` points act as pivots for the operation.

        Args:
            factor:

        Returns:

        """
        raise NotImplementedError(self.__class__)

    def __invert__(self):
        return InvertedGeometry(self)

    def __eq__(self, other):
        """
        Slow equality check.
        Unlike `==`, this method compares all tensor elements to check whether they are equal.
        Use `==` for a faster check which only checks whether the referenced tensors are the same.

        See Also:
            `shallow_equals()`
        """
        def tensor_equality(a, b):
            if a is None or b is None:
                return True  # stored mode, tensors unavailable
            return math.close(a, b, rel_tolerance=1e-5, equal_nan=True)
        differences = find_differences(self, other, attr_type=variable_attributes, tensor_equality=tensor_equality)
        return not differences

    def shallow_equals(self, other):
        """
        Quick equality check.
        May return `False` even if `other == self`.
        However, if `True` is returned, the geometries are guaranteed to be equal.

        The `shallow_equals()` check does not compare all tensor elements but merely checks whether the same tensors are referenced.
        """
        differences = find_differences(self, other, compare_tensors_by_id=True)
        return not differences

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Geometry':
        if all(type(v) == type(values[0]) for v in values):
            return NotImplemented  # let attributes be stacked
        else:
            from ._geom_ops import GeometryStack
            set_op = kwargs.get('set_op')
            return GeometryStack(math.layout(values, dim), set_op)

    def __flatten__(self, flat_dim: Shape, flatten_batch: bool, **kwargs) -> 'Geometry':
        dims = self.shape.without('vector')
        if not flatten_batch:
            dims = dims.non_batch
        return math.pack_dims(self, dims, flat_dim, **kwargs)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return id(self.__class__) + hash(self.shape)

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

    def __getattr__(self, name: str) -> BoundDim:
        if name in ('shape', '__setstate__', '__all_attrs__', '__variable_attrs__', '__value_attrs__'):
            raise AttributeError
        try:
            self_shape = self.shape
            if self_shape is None:
                raise AttributeError(f"Invalid object of type {self.__class__.__name__}. Failed to get attribute {name} because it has no shape.")
        except BaseException as err:  # invalid object, probably None
            raise AttributeError(err)
        if name in self_shape:
            return BoundDim(self, name)
        if hasattr(self.__class__, name):
            raise RuntimeError(f"Evaluating {self.__class__.__name__}.{name} failed with an error.")
        raise AttributeError(f"{self.__class__.__name__}.{name}")

Abstract base class for N-dimensional shapes.

Main implementing classes:

  • Sphere
  • box family: box (generator), Box, Cuboid, BaseBox

All geometry objects support batching. Thereby any parameter defining the geometry can be varied along arbitrary batch dims. All batch dimensions are listed in Geometry.shape.

Property getters (@property, such as shape), save for getters, must not depend on any variables marked as variable via __variable_attrs__() as these may be None during tracing. Equality checks must also take this into account.

Subclasses

  • phi.geom._box.BaseBox
  • phi.geom._cylinder.Cylinder
  • phi.geom._embed._EmbeddedGeometry
  • phi.geom._geom.InvertedGeometry
  • phi.geom._geom.NoGeometry
  • phi.geom._geom.Point
  • phi.geom._geom_ops.GeometryStack
  • phi.geom._graph.Graph
  • phi.geom._heightmap.Heightmap
  • phi.geom._mesh.Mesh
  • phi.geom._sdf.SDF
  • phi.geom._sdf_grid.SDFGrid
  • phi.geom._sphere.Sphere
  • phi.geom._spline_solid.SplineSolid

Instance variables

prop boundary_elements : Dict[str, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
    """
    Slices on the primal dimensions to mark boundary elements.
    Grids and meshes have no boundary elements and return `{}`.
    Dynamic graphs can define boundary elements for obstacles and walls.

    Returns:
        Map from `name` to slicing `dict`.
    """
    raise NotImplementedError(self.__class__)

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[str, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[str, Dict[str, slice]]:
    """
    Slices on the dual dimensions to mark boundary faces.

    Regular grids use the keys (dim, is_upper) to identify boundaries.
    Unstructured meshes use string identifiers for the boundaries.
    Dynamic graphs return slices along the dual dimensions.

    Returns:
        Map from `name` to slicing `dict`.
    """
    raise NotImplementedError(self.__class__)

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    """
    Center location in single channel dimension.
    """
    raise NotImplementedError(self.__class__)

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    """
    Returns:
        Corner locations as `phiml.math.Tensor`.
        Corners belonging to one object or cell are listed along dual dimensions.
        If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.
    """
    raise NotImplementedError(self.__class__)

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    """
    Area of face connecting a pair of cells. Shape `(elements, ~)`.
    Returns 0 for unconnected cells.
    """
    raise NotImplementedError(self.__class__)

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    """
    Center of face connecting a pair of cells. Shape `(elements, ~, vector)`.
    Here, `~` represents arbitrary internal dual dimensions, such as `~staggered_direction` or `~elements`.
    Returns 0-vectors for unconnected cells.
    """
    raise NotImplementedError(self.__class__)

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    """
    Normal vectors of cell faces, including boundary faces. Shape `(elements, ~, vector)`.
    For meshes, The vectors point out of the primal cells and into the dual cells.

    Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.
    """
    raise NotImplementedError(self.__class__)

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    """
    Returns:
        Full Shape to identify each face of this `Geometry`, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element.
        If this `Geometry` has no faces, returns an empty `Shape`.
    """
    return None

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError(self.__class__)
prop sets : Dict[str, phiml.math._shape.Shape]
Expand source code
@property
def sets(self) -> Dict[str, Shape]:
    if self.face_shape and self.face_shape != self.shape and self.face_shape.volume > 0:
        return {'center': non_batch(self)-'vector', 'face': self.face_shape.non_batch}
    else:
        return {'center': non_batch(self)-'vector'}
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    """
    The `shape` of a `Geometry` consists of the following dimensions:

    * A single *channel* dimension called `'vector'` specifying the physical space
    * Instance dimensions denote that this geometry consists of multiple copies in the same space
    * Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
    * Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
    """
    raise NotImplementedError(self.__class__)

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop spatial_rank : int
Expand source code
@property
def spatial_rank(self) -> int:
    """ Number of spatial dimensions of the geometry, 1 = 1D, 2 = 2D, 3 = 3D, etc. """
    return self.shape.get_size('vector')

Number of spatial dimensions of the geometry, 1 = 1D, 2 = 2D, 3 = 3D, etc.

prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    """
    `phi.math.Tensor` representing the volume of each element.
    The result retains batch, spatial and instance dimensions.
    """
    raise NotImplementedError(self.__class__)

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    """
    Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

    Args:
        location: `Tensor` with a single channel dimension called vector. Can have arbitrary other dimensions.

    Returns:
        signed_distance: Scalar signed distance from `location`  to the closest point on the surface.
            Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
        delta: Vector-valued distance vector from `location` to the closest point on the surface.
        normal: Closest surface normal vector.
        offset: Min distance of a surface-tangential plane from 0 as a scalar.
        face_index: (Optional) An index vector pointing at the closest face.
    """
    raise NotImplementedError(self.__class__)

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_fraction_inside(self,
other_geometry: Geometry,
balance: phiml.math._tensors.Tensor | numbers.Number = 0.5) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_fraction_inside(self, other_geometry: 'Geometry', balance: Union[Tensor, Number] = 0.5) -> Tensor:
    """
    Computes the approximate overlap between the geometry and a small other geometry.
    Returns 1.0 if `other_geometry` is fully enclosed in this geometry and 0.0 if there is no overlap.
    Close to the surface of this geometry, the fraction filled is differentiable w.r.t. the location and size of `other_geometry`.

    To call this method on batches of geometries of same shape, pass a batched Geometry instance.
    The result tensor will match the batch shape of `other_geometry`.

    The result may only be accurate in special cases.
    The given geometries may be approximated as spheres or boxes using `bounding_radius()` and `bounding_half_extent()`.

    The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using `approximate_signed_distance()`.

    Args:
        other_geometry: `Geometry` or geometry batch for which to compute the overlap with `self`.
        balance: Mid-level between 0 and 1, default 0.5.
            This value is returned when exactly half of `other_geometry` lies inside `self`.
            `0.5 < balance <= 1` makes `self` seem larger while `0 <= balance < 0.5`makes `self` seem smaller.

    Returns:
      fraction of cell volume lying inside the geometry. float tensor of shape (other_geometry.batch_shape, 1).

    """
    assert isinstance(other_geometry, Geometry)
    radius = other_geometry.bounding_radius()
    location = other_geometry.center
    distance = self.approximate_signed_distance(location)
    inside_fraction = balance - distance / radius
    inside_fraction = math.clip(inside_fraction, 0, 1)
    return inside_fraction

Computes the approximate overlap between the geometry and a small other geometry. Returns 1.0 if other_geometry is fully enclosed in this geometry and 0.0 if there is no overlap. Close to the surface of this geometry, the fraction filled is differentiable w.r.t. the location and size of other_geometry.

To call this method on batches of geometries of same shape, pass a batched Geometry instance. The result tensor will match the batch shape of other_geometry.

The result may only be accurate in special cases. The given geometries may be approximated as spheres or boxes using bounding_radius() and bounding_half_extent().

The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using approximate_signed_distance().

Args

other_geometry
Geometry or geometry batch for which to compute the overlap with self.
balance
Mid-level between 0 and 1, default 0.5. This value is returned when exactly half of other_geometry lies inside self. 0.5 < balance <= 1 makes self seem larger while 0 <= balance < 0.5makes self seem smaller.

Returns

fraction of cell volume lying inside the geometry. float tensor of shape (other_geometry.batch_shape, 1).

def approximate_signed_distance(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Tensor) -> Tensor:
    """
    Computes the approximate distance from location to the surface of the geometry.
    Locations outside return positive values, inside negative values and zero exactly at the boundary.

    The exact distance metric used depends on the geometry.
    The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry.
    The distance metric is differentiable and its gradients are bounded at every point in space.

    When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned.
    This also holds for negative distances.

    Args:
        location: `Tensor` with one channel dim `vector` matching the geometry's `vector` dim.

    Returns:
        Float `Tensor`
    """
    raise NotImplementedError(self.__class__)

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    """
    Returns a copy of this `Geometry` with the center at `center`.
    This is equal to calling `self @ center`.

    See Also:
        `Geometry.shifted()`.

    Args:
        center: New center as `Tensor`.

    Returns:
        `Geometry`.
    """
    raise NotImplementedError(self.__class__)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_box(self) ‑> BaseBox
Expand source code
def bounding_box(self) -> 'BaseBox':
    """
    Returns the approximately smallest axis-aligned box that contains this `Geometry`.
    The center of the box may not be equal to `self.center`.

    Returns:
        `Box` or `Cuboid` that fully contains this `Geometry`.
    """
    center = self.center
    half = self.bounding_half_extent()
    min_vec = math.min(center - half, dim=center.shape.non_batch.non_channel)
    max_vec = math.max(center + half, dim=center.shape.non_batch.non_channel)
    from ._box import Box
    return Box(min_vec, max_vec)

Returns the approximately smallest axis-aligned box that contains this Geometry. The center of the box may not be equal to self.center.

Returns

Box or Cuboid that fully contains this Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    """
    The bounding half-extent sets a limit on the outer-most point for each coordinate axis.
    Each component is non-negative.

    Let the bounding half-extent have value `e` in dimension `d` (`extent[...,d] = e`).
    Then, no point of the geometry lies further away from its center point than `e` along `d` (in both axis directions).

    If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part.
    If these dims are not present on the result, all parts are assumed to have the same bounds.
    """
    raise NotImplementedError(self.__class__)

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    """
    Returns the radius of a Sphere object that fully encloses this geometry.
    The sphere is centered at the center of this geometry.

    If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part.
    If these dims are not present on the result, all parts are assumed to have the same bounds.
    """
    raise NotImplementedError(self.__class__)

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_sphere(self)
Expand source code
def bounding_sphere(self):
    center = self.bounding_box().center
    dist = length(self.center - center) + self.bounding_radius()
    from ._sphere import Sphere
    return Sphere(center, dist)
def get_boundary(self, set_key: str) ‑> Dict[str, Dict[str, slice]]
Expand source code
def get_boundary(self, set_key: str) -> Dict[str, Dict[str, slice]]:
    if set_key == 'center':
        return self.boundary_elements
    elif set_key == 'face':
        return self.boundary_faces
    else:
        raise ValueError(f"Unknown set: '{set_key}'")
def get_points(self, set_key: str) ‑> phiml.math._tensors.Tensor
Expand source code
def get_points(self, set_key: str) -> Tensor:
    if set_key == 'center':
        return self.center
    elif set_key == 'face':
        return self.face_centers
    else:
        raise ValueError(f"Unknown set: '{set_key}'")
def integrate_flux(self, flux: phiml.math._tensors.Tensor, divide_volume=False) ‑> phiml.math._tensors.Tensor
Expand source code
def integrate_flux(self, flux: Tensor, divide_volume=False) -> Tensor:
    assert 'vector' in flux.shape, f"flux must have a 'vector' dimension but got {flux.shape}"
    result = math.sum(flux.vector @ (self.face_normals * self.face_areas).vector, self.face_shape.dual)
    return result / self.volume if divide_volume else result
def integrate_surface(self, face_values: phiml.math._tensors.Tensor, divide_volume=False) ‑> phiml.math._tensors.Tensor
Expand source code
def integrate_surface(self, face_values: Tensor, divide_volume=False) -> Tensor:
    """
    Multiplies `values´ by the corresponding face area, computes the sum over all faces and divides by the cell volume.
    ∑ values * A.

    Args:
        face_values: Values sampled at the face centers.
        divide_volume: Whether to divide by the cell `volume´

    Returns:
        `Tensor` of values sampled at the centroids.
    """
    result = math.sum(face_values * self.face_areas, self.face_shape.dual)
    return result / self.volume if divide_volume else result

Multiplies `values´ by the corresponding face area, computes the sum over all faces and divides by the cell volume. ∑ values * A.

Args

face_values
Values sampled at the face centers.
divide_volume
Whether to divide by the cell `volume´

Returns

Tensor of values sampled at the centroids.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    """
    Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

    When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

    Args:
      location: float tensor of shape (batch_size, ..., rank)

    Returns:
      bool tensor of shape (*location.shape[:-1], 1).

    """
    raise NotImplementedError(self.__class__)

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def push(self,
positions: phiml.math._tensors.Tensor,
outward: bool = True,
shift_amount: float = 0) ‑> phiml.math._tensors.Tensor
Expand source code
def push(self, positions: Tensor, outward: bool = True, shift_amount: float = 0) -> Tensor:
    """
    Shifts positions either into or out of geometry.

    Args:
        positions: Tensor holding positions to shift
        outward: Flag for indicating inward (False) or outward (True) shift
        shift_amount: Minimum distance between positions and surface after shifting.

    Returns:
        Tensor holding shifted positions.
    """
    from ._geom_ops import expel
    return expel(self, positions, min_separation=shift_amount, invert=not outward)

Shifts positions either into or out of geometry.

Args

positions
Tensor holding positions to shift
outward
Flag for indicating inward (False) or outward (True) shift
shift_amount
Minimum distance between positions and surface after shifting.

Returns

Tensor holding shifted positions.

def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    """
    Returns a rotated version of this geometry.
    The geometry is rotated about its center point.

    Args:
        angle: Delta rotation.
            Either

            * Angle(s): scalar angle in 2d or euler angles along `vector` in 3D or higher.
            * Matrix: d⨯d rotation matrix

    Returns:
        Rotated `Geometry`
    """
    raise NotImplementedError(self.__class__)

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    """
    Samples uniformly distributed random points inside this volume.

    Args:
        *shape: How many points to sample per individual geometry.

    Returns:
        `Tensor` containing all dimensions from `Geometry.shape`, `shape` as well as a `channel` dimension `vector` matching the dimensionality of this `Geometry`.
    """
    raise NotImplementedError(self.__class__)

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    """
    Scales each individual geometry by `factor`.
    The individual `center` points act as pivots for the operation.

    Args:
        factor:

    Returns:

    """
    raise NotImplementedError(self.__class__)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def shallow_equals(self, other)
Expand source code
def shallow_equals(self, other):
    """
    Quick equality check.
    May return `False` even if `other == self`.
    However, if `True` is returned, the geometries are guaranteed to be equal.

    The `shallow_equals()` check does not compare all tensor elements but merely checks whether the same tensors are referenced.
    """
    differences = find_differences(self, other, compare_tensors_by_id=True)
    return not differences

Quick equality check. May return False even if other == self. However, if True is returned, the geometries are guaranteed to be equal.

The shallow_equals() check does not compare all tensor elements but merely checks whether the same tensors are referenced.

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def shifted(self, delta: Tensor) -> 'Geometry':
    """
    Returns a translated version of this geometry.

    See Also:
        `Geometry.at()`.

    Args:
      delta: direction vector
      delta: Tensor:

    Returns:
      Geometry: shifted geometry

    """
    return self.at(self.center + delta)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
def unstack(self, dimension: str) ‑> tuple
Expand source code
def unstack(self, dimension: str) -> tuple:
    """
    Unstacks this Geometry along the given dimension.
    The shapes of the returned geometries are reduced by `dimension`.

    Args:
        dimension: dimension along which to unstack

    Returns:
        geometries: tuple of length equal to `geometry.shape.get_size(dimension)`
    """
    warnings.warn(f"Geometry.unstack() is deprecated. Use math.unstack(geometry) instead.", DeprecationWarning)
    return math.unstack(self, dimension)

Unstacks this Geometry along the given dimension. The shapes of the returned geometries are reduced by dimension.

Args

dimension
dimension along which to unstack

Returns

geometries
tuple of length equal to geometry.shape.get_size(dimension)
class GeometryException (*args, **kwargs)
Expand source code
class GeometryException(BaseException):
    """
    Raised when an operation is fundamentally not possible for a `Geometry`.
    Possible causes:

    * Trying to get the interior of a non-surface `Geometry`
    * Trying to get the surface of a point-like `Geometry`
    """

Raised when an operation is fundamentally not possible for a Geometry. Possible causes:

  • Trying to get the interior of a non-surface Geometry
  • Trying to get the surface of a point-like Geometry

Ancestors

  • builtins.BaseException
class Graph (nodes: phi.geom._geom.Geometry | phiml.math._tensors.Tensor,
edges: phiml.math._tensors.Tensor,
boundary: Dict[str, Dict[str, slice]],
deltas: phiml.math._tensors.Tensor | None = None,
distances: phiml.math._tensors.Tensor | None = None,
bounding_distance: float | phiml.math._tensors.Tensor | None = None)
Expand source code
class Graph(Geometry):
    """
    A graph consists of multiple geometry nodes and corresponding edge information.

    Edges are stored as a Tensor with the same axes ad `geometry` plus their dual counterparts.
    Additional dimensions can be added to `edges` to store vector-valued connectivity weights.
    """

    def __init__(self,
                 nodes: Union[Geometry, Tensor],
                 edges: Tensor,
                 boundary: Dict[str, Dict[str, slice]],
                 deltas: Optional[Tensor] = None,
                 distances: Optional[Tensor] = None,
                 bounding_distance: Tensor | float | None = None):
        """
        Create a graph where `nodes` are connected by `edges`.

        Args:
            nodes: `Geometry` collection or `Tensor` to denote points.
            edges: Edge weight matrix. Must have the instance and spatial dims of `nodes` plus their dual counterparts.
            boundary: Marks ranges of nodes as boundary elements.
            deltas: (Optional) Pre-computed position difference matrix.
            distances: (Optional) Pre-computed distance matrix.
            bounding_distance: (Optional) Pre-computed distance bounds. No distance is larger than this value. If `True`, will be computed now, if `False`, will not be computed.
        """
        assert isinstance(nodes, Geometry), f"nodes must be a Geometry  but got {nodes}"
        node_dims = non_batch(nodes).non_channel
        assert node_dims in edges.shape and edges.shape.dual.rank == node_dims.rank, f"edges must contain all node dims {node_dims} as primal and dual but got {edges.shape}"
        self._nodes: Geometry = nodes if isinstance(nodes, Geometry) else Point(nodes)
        self._edges = edges
        self._boundary = boundary
        self._deltas = deltas
        self._distances = distances
        self._connectivity = math.tensor_like(edges, 1) if math.is_sparse(edges) else (edges != 0) & ~math.is_nan(edges)
        if isinstance(bounding_distance, bool):
            self._bounding_distance = math.max(self._distances) if bounding_distance else None
        else:
            self._bounding_distance = bounding_distance

    def __variable_attrs__(self):
        return '_nodes', '_edges', '_deltas', '_distances', '_connectivity'

    def __value_attrs__(self):
        return '_nodes',

    @property
    def edges(self):
        return self._edges

    @property
    def connectivity(self) -> Tensor:
        return self._connectivity

    @property
    def nodes(self) -> Geometry:
        return self._nodes

    def as_points(self):
        return Graph(Point(self._nodes.center), self._edges, self._boundary, self._deltas, self._distances, self._bounding_distance)

    @property
    def deltas(self):
        return self._deltas

    @property
    def unit_deltas(self):
        return math.safe_div(self._deltas, self._distances)

    @property
    def distances(self):
        return self._distances

    @property
    def bounding_distance(self) -> Optional[Tensor]:
        return self._bounding_distance

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

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

    @property
    def volume(self) -> Tensor:
        return self._nodes.volume

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError

    @property
    def face_centers(self) -> Tensor:
        raise NotImplementedError

    @property
    def face_areas(self) -> Tensor:
        raise NotImplementedError

    @property
    def face_normals(self) -> Tensor:
        raise NotImplementedError

    @property
    def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
        return self._boundary

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        raise NotImplementedError  # connections between boundary elements

    @property
    def face_shape(self) -> Shape:
        return non_batch(self._edges).non_channel

    def lies_inside(self, location: Tensor) -> Tensor:
        raise NotImplementedError

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        raise NotImplementedError

    def approximate_signed_distance(self, location: Tensor) -> Tensor:
        raise NotImplementedError

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        raise NotImplementedError

    def bounding_radius(self) -> Tensor:
        return self._nodes.bounding_radius()

    def bounding_half_extent(self) -> Tensor:
        return self._nodes.bounding_half_extent()

    def at(self, center: Tensor) -> 'Geometry':
        raise NotImplementedError("Changing the node positions of a Graph is not supported as it would invalidate distances.")
        # warnings.warn("Changing the node positions of a graph triggers re-evaluation of distances.", RuntimeWarning, stacklevel=2)
        # return Graph(self.nodes.at(center), self._edges, self._boundary, bounding_distance=self._bounding_distance is not None)

    def shifted(self, delta: Tensor) -> 'Geometry':
        if non_batch(delta).non_channel.only(self._nodes.shape) and self._deltas is not None:  # shift varies between elements
            raise NotImplementedError("Shifting the node positions of a Graph is not supported as it would invalidate distances.")
        return Graph(self.nodes.shifted(delta), self._edges, self._boundary, deltas=self._deltas, distances=self._distances, bounding_distance=self._bounding_distance is not None)

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        node_dims = non_batch(self._nodes).non_channel
        edge_sel = {}
        for i, (dim, sel) in enumerate(item.items()):
            if dim in node_dims:
                dual_dim = '~' + dim
                if dual_dim not in self._edges.shape:
                    dual_dim = dual(self._edges).shape.names[i]
                edge_sel[dim] = edge_sel[dual_dim] = sel
            elif dim in batch(self):
                edge_sel[dim] = sel
        deltas = self._deltas[edge_sel] if self._deltas is not None else None
        distances = self._distances[edge_sel] if self._distances is not None else None
        bounding_distance = self._bounding_distance[item] if self._bounding_distance is not None else None
        return Graph(self._nodes[item], self._edges[edge_sel], self._boundary, deltas, distances, bounding_distance)

A graph consists of multiple geometry nodes and corresponding edge information.

Edges are stored as a Tensor with the same axes ad geometry plus their dual counterparts. Additional dimensions can be added to edges to store vector-valued connectivity weights.

Create a graph where nodes are connected by edges.

Args

nodes
Geometry collection or Tensor to denote points.
edges
Edge weight matrix. Must have the instance and spatial dims of nodes plus their dual counterparts.
boundary
Marks ranges of nodes as boundary elements.
deltas
(Optional) Pre-computed position difference matrix.
distances
(Optional) Pre-computed distance matrix.
bounding_distance
(Optional) Pre-computed distance bounds. No distance is larger than this value. If True, will be computed now, if False, will not be computed.

Ancestors

  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[str, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
    return self._boundary

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    raise NotImplementedError  # connections between boundary elements

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounding_distance : phiml.math._tensors.Tensor | None
Expand source code
@property
def bounding_distance(self) -> Optional[Tensor]:
    return self._bounding_distance
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._nodes.center

Center location in single channel dimension.

prop connectivity : phiml.math._tensors.Tensor
Expand source code
@property
def connectivity(self) -> Tensor:
    return self._connectivity
prop deltas
Expand source code
@property
def deltas(self):
    return self._deltas
prop distances
Expand source code
@property
def distances(self):
    return self._distances
prop edges
Expand source code
@property
def edges(self):
    return self._edges
prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    raise NotImplementedError

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    raise NotImplementedError

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    raise NotImplementedError

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return non_batch(self._edges).non_channel

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError
prop nodes : phi.geom._geom.Geometry
Expand source code
@property
def nodes(self) -> Geometry:
    return self._nodes
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    return self._nodes.shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop unit_deltas
Expand source code
@property
def unit_deltas(self):
    return math.safe_div(self._deltas, self._distances)
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return self._nodes.volume

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    raise NotImplementedError

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Tensor) -> Tensor:
    raise NotImplementedError

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def as_points(self)
Expand source code
def as_points(self):
    return Graph(Point(self._nodes.center), self._edges, self._boundary, self._deltas, self._distances, self._bounding_distance)
def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    raise NotImplementedError("Changing the node positions of a Graph is not supported as it would invalidate distances.")
    # warnings.warn("Changing the node positions of a graph triggers re-evaluation of distances.", RuntimeWarning, stacklevel=2)
    # return Graph(self.nodes.at(center), self._edges, self._boundary, bounding_distance=self._bounding_distance is not None)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    return self._nodes.bounding_half_extent()

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    return self._nodes.bounding_radius()

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    raise NotImplementedError

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def shifted(self, delta: Tensor) -> 'Geometry':
    if non_batch(delta).non_channel.only(self._nodes.shape) and self._deltas is not None:  # shift varies between elements
        raise NotImplementedError("Shifting the node positions of a Graph is not supported as it would invalidate distances.")
    return Graph(self.nodes.shifted(delta), self._edges, self._boundary, deltas=self._deltas, distances=self._distances, bounding_distance=self._bounding_distance is not None)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
class Heightmap (height: phiml.math._tensors.Tensor,
bounds: phi.geom._box.Box,
max_dist: phiml.math._tensors.Tensor | float,
fill_below: bool | phiml.math._tensors.Tensor = True,
extrapolation: float | str | phiml.math.extrapolation.Extrapolation = None,
faces=None)
Expand source code
class Heightmap(Geometry):

    def __init__(self,
                 height: Tensor,
                 bounds: Box,
                 max_dist: Union[float, Tensor],
                 fill_below: Union[bool, Tensor] = True,
                 extrapolation: Union[float, str, math.Extrapolation] = None,
                 faces=None):
        """

        Args:
            height: Heightmap `Tensor` of absolute (world-space) height values.
                Scalar height values on a d-1 dimensional grid.
            bounds: d-dimensional bounds.
                Locations outside `bounds' can never lie inside this geometry if `extrapolation is None`.
                Otherwise, only the height dimension is checked.
                The grid dimensions of `bounds` must be finite but the height dimension may be infinite to count all values above/below `height` as inside.
            max_dist: Maximum distance up to which the distance approximations should be valid.
                This does not affect the number of computations performed to compute the distance.
                Low values increase accuracy close to the surface but trade off possibly very wrong distances further away.
            fill_below: Whether the inside is below or above the height values.
            extrapolation: Surface height outside `bounds´. Can be any valid `phiml.math.Extrapolation`, such as a constant.
                If not `None`, values outside `bounds` will be checked against the extrapolated `height` values.
                Otherwise, values outside `bounds` always lie on the outside.
        """
        assert channel(height).is_empty, f"height must be a scalar quantity but got {height.shape}"
        assert spatial(height), f"height field must have at least one spatial dim but got {height}"
        assert bounds.vector.size == spatial(height).rank + 1, f"bounds must include the spatial grid dimensions {spatial(height)} and the height dimension but got {bounds}"
        dims = bounds.vector.item_names
        self._hdim = spatial(*dims).without(height.shape).name
        if math.all_available(height, bounds.lower, bounds.upper):
            assert bounds[self._hdim].lies_inside(height).all, f"All height values should be within the {self._hdim}-range given by bounds but got height={height}"
        self._height = height
        self._fill_below = wrap(fill_below)
        self._bounds = bounds
        self._max_dist = wrap(max_dist)
        self._extrapolation = math.as_extrapolation(extrapolation)
        if faces is None:
            proj_faces = build_faces(self)
            with numpy.errstate(divide='ignore', invalid='ignore'):
                secondary_idx = math.map(find_most_important_neighbor, proj_faces, self.dx, self.resolution, self._hdim, self._fill_below, self._max_dist, dims=instance, unwrap_scalars=False)
                secondary_faces = math.map(math.gather, proj_faces, secondary_idx, dims=instance)
            self._faces: Face = stack([proj_faces, *unstack(secondary_faces, 'side')], batch(consider='self,outside,inside'), expand_values=True)
            self._faces = cached(self._faces)  # otherwise, this may get expanded during tracing
        else:
            self._faces = faces

    @property
    def height(self):
        return self._height

    @property
    def bounds(self):
        return self._bounds

    @property
    def max_dist(self):
        return self._max_dist

    @property
    def fill_below(self):
        return self._fill_below

    @property
    def extrapolation(self):
        return self._extrapolation

    @property
    def shape(self) -> Shape:
        return (self._height.shape - 1) & channel(self._bounds)

    @property
    def resolution(self):
        return spatial(self._height) - 1

    @property
    def grid_bounds(self):
        return self._bounds[self.resolution.name_list]

    @property
    def up(self):
        dims = self._bounds.vector.item_names
        height_unit = vec(**{d: 1 if d == self._hdim else 0 for d in dims})
        return math.where(self._fill_below, height_unit, -height_unit)

    @property
    def dx(self):
        return self._bounds.size[self.resolution.name_list] / spatial(self.resolution)

    @property
    def vertices(self):
        hdim = self._hdim
        space = self.vector.item_names
        pos = self.grid_bounds.local_to_global(math.meshgrid(spatial(self._height)) / self.resolution)
        vert = stack({dim: self.height if dim == hdim else pos[dim] for dim in space}, channel('vector'))
        return vert

    def lies_inside(self, location: Tensor) -> Tensor:
        location = rename_dims(location, self.resolution.names, ['loc_' + n for n in self.resolution.names])
        projected_loc = location[self.resolution.name_list]
        @math.map_i2b
        def lies_inside_(height, grid_bounds, bounds, fill_below, extrapolation):
            float_idx = (projected_loc - grid_bounds.lower) / grid_bounds.size * self.resolution
            if extrapolation is None:
                within_bounds = bounds.lies_inside(location)
            else:
                within_bounds = bounds[self._hdim].lies_inside(location[self._hdim])
            surface_height = math.grid_sample(height, float_idx - 1, math.NAN if extrapolation is None else extrapolation)
            is_below = location[self._hdim] <= surface_height
            inside = is_below == fill_below
            result = math.where(within_bounds, inside, False)
            return rename_dims(result, ['loc_' + n for n in self.resolution.names], self.resolution.names)
        return math.any(lies_inside_(self._height, self.grid_bounds, self._bounds, self._fill_below, self._extrapolation), instance(self))

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        grid_bounds = math.i2b(self.grid_bounds)
        faces = math.i2b(self._faces)
        cell_idx = cell_index(location, grid_bounds, self.resolution, clip=True)
        # --- gather face infos at projected cell ---
        normals = faces.normal[cell_idx]
        offsets = faces.origin_distance[cell_idx]
        face_idx = faces.index[cell_idx]
        # --- test location against all considered faces and boundaries ---
        # distances = plane_sgn_dist(-offsets, normals, location)  # offset has the - convention here
        distances = normals.vector @ location.vector + offsets
        projected_onto_face = location - normals * distances
        projected_idx = cell_index(projected_onto_face, grid_bounds, self.resolution, clip=False)
        projects_onto_face = math.all(projected_idx == face_idx, channel)
        proj_delta = normals * -distances
        # --- if not projected onto face, use distance to highest point instead ---
        delta_highest = faces.extrema_points[cell_idx] - location
        flat_normal = math.vec_normalize(normals[self.resolution.name_list], epsilon=1e-5)
        delta_edge = flat_normal * (delta_highest[self.resolution].vector @ flat_normal.vector)  # project onto flat normal
        delta_edge = concat([delta_edge, delta_highest[[self._hdim]]], 'vector')
        distance_edge = math.vec_length(delta_edge, eps=1e-5)
        delta_highest, distance_edge = math.at_min((delta_highest, distance_edge), distance_edge, 'extremum')
        distance_edge = math.where(distances < 0, -distance_edge, distance_edge)  # copy sign of distances onto distance_edges to always return the signed distance
        distances = math.where(projects_onto_face, distances, distance_edge)
        # --- use closest face from considered ---
        delta = math.where(projects_onto_face, proj_delta, delta_highest)
        return math.at_min((distances, delta, normals, offsets, face_idx), key=abs(distances), dim=batch('consider') & instance(self).as_batch())

    def shallow_equals(self, other):
        return self == other

    def __repr__(self):
        return f"Heightmap {self.resolution}, bounds={self._bounds}"

    def __variable_attrs__(self):
        return '_height', '_bounds', '_max_dist', '_fill_below', '_extrapolation', '_faces'

    def __value_attrs__(self):
        return ()

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        return Heightmap(self._height[item], self._bounds[item], self._max_dist[item], self._fill_below[item], self._extrapolation[item] if self._extrapolation is not None else None, math.slice(self._faces, item))

    def bounding_half_extent(self) -> Tensor:
        h_min, h_max = self._faces.extrema_points[{'consider': 0, 'vector': self._hdim}].extremum
        dh = h_max - h_min
        return stack({d: self.dx[d] if d in self.resolution else dh for d in self.vector.item_names}, channel('vector'), expand_values=True) * .5

    @property
    def center(self) -> Tensor:
        return self._faces.center.consider[0]

    @property
    def volume(self) -> Tensor:
        return math.prod(self.bounding_half_extent() * 2, channel)

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError

    @property
    def face_centers(self) -> Tensor:
        return self._faces.center

    @property
    def face_areas(self) -> Tensor:
        raise NotImplementedError

    @property
    def face_normals(self) -> Tensor:
        return self._faces.normal

    @property
    def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def face_shape(self) -> Shape:
        return non_channel(self._faces.center)

    def approximate_signed_distance(self, location: Tensor) -> Tensor:
        return self.approximate_closest_surface(location)[0]

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        raise NotImplementedError

    def bounding_radius(self) -> Tensor:
        return self._bounds.bounding_radius()

    def at(self, center: Tensor) -> 'Geometry':
        raise NotImplementedError

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError

Abstract base class for N-dimensional shapes.

Main implementing classes:

  • Sphere
  • box family: box (generator), Box, Cuboid, BaseBox

All geometry objects support batching. Thereby any parameter defining the geometry can be varied along arbitrary batch dims. All batch dimensions are listed in Geometry.shape.

Property getters (@property, such as shape), save for getters, must not depend on any variables marked as variable via __variable_attrs__() as these may be None during tracing. Equality checks must also take this into account.

Args

height
Heightmap Tensor of absolute (world-space) height values. Scalar height values on a d-1 dimensional grid.
bounds
d-dimensional bounds. Locations outside bounds' can never lie inside this geometry ifextrapolation is None`. Otherwise, only the height dimension is checked. The grid dimensions of bounds must be finite but the height dimension may be infinite to count all values above/below height as inside.
max_dist
Maximum distance up to which the distance approximations should be valid. This does not affect the number of computations performed to compute the distance. Low values increase accuracy close to the surface but trade off possibly very wrong distances further away.
fill_below
Whether the inside is below or above the height values.
extrapolation
Surface height outside bounds´. Can be any validphiml.math.Extrapolation`, such as a constant. If not None, values outside bounds will be checked against the extrapolated height values. Otherwise, values outside bounds always lie on the outside.

Ancestors

  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounds
Expand source code
@property
def bounds(self):
    return self._bounds
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._faces.center.consider[0]

Center location in single channel dimension.

prop dx
Expand source code
@property
def dx(self):
    return self._bounds.size[self.resolution.name_list] / spatial(self.resolution)
prop extrapolation
Expand source code
@property
def extrapolation(self):
    return self._extrapolation
prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    raise NotImplementedError

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    return self._faces.center

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    return self._faces.normal

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return non_channel(self._faces.center)

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError
prop fill_below
Expand source code
@property
def fill_below(self):
    return self._fill_below
prop grid_bounds
Expand source code
@property
def grid_bounds(self):
    return self._bounds[self.resolution.name_list]
prop height
Expand source code
@property
def height(self):
    return self._height
prop max_dist
Expand source code
@property
def max_dist(self):
    return self._max_dist
prop resolution
Expand source code
@property
def resolution(self):
    return spatial(self._height) - 1
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    return (self._height.shape - 1) & channel(self._bounds)

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop up
Expand source code
@property
def up(self):
    dims = self._bounds.vector.item_names
    height_unit = vec(**{d: 1 if d == self._hdim else 0 for d in dims})
    return math.where(self._fill_below, height_unit, -height_unit)
prop vertices
Expand source code
@property
def vertices(self):
    hdim = self._hdim
    space = self.vector.item_names
    pos = self.grid_bounds.local_to_global(math.meshgrid(spatial(self._height)) / self.resolution)
    vert = stack({dim: self.height if dim == hdim else pos[dim] for dim in space}, channel('vector'))
    return vert
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return math.prod(self.bounding_half_extent() * 2, channel)

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    grid_bounds = math.i2b(self.grid_bounds)
    faces = math.i2b(self._faces)
    cell_idx = cell_index(location, grid_bounds, self.resolution, clip=True)
    # --- gather face infos at projected cell ---
    normals = faces.normal[cell_idx]
    offsets = faces.origin_distance[cell_idx]
    face_idx = faces.index[cell_idx]
    # --- test location against all considered faces and boundaries ---
    # distances = plane_sgn_dist(-offsets, normals, location)  # offset has the - convention here
    distances = normals.vector @ location.vector + offsets
    projected_onto_face = location - normals * distances
    projected_idx = cell_index(projected_onto_face, grid_bounds, self.resolution, clip=False)
    projects_onto_face = math.all(projected_idx == face_idx, channel)
    proj_delta = normals * -distances
    # --- if not projected onto face, use distance to highest point instead ---
    delta_highest = faces.extrema_points[cell_idx] - location
    flat_normal = math.vec_normalize(normals[self.resolution.name_list], epsilon=1e-5)
    delta_edge = flat_normal * (delta_highest[self.resolution].vector @ flat_normal.vector)  # project onto flat normal
    delta_edge = concat([delta_edge, delta_highest[[self._hdim]]], 'vector')
    distance_edge = math.vec_length(delta_edge, eps=1e-5)
    delta_highest, distance_edge = math.at_min((delta_highest, distance_edge), distance_edge, 'extremum')
    distance_edge = math.where(distances < 0, -distance_edge, distance_edge)  # copy sign of distances onto distance_edges to always return the signed distance
    distances = math.where(projects_onto_face, distances, distance_edge)
    # --- use closest face from considered ---
    delta = math.where(projects_onto_face, proj_delta, delta_highest)
    return math.at_min((distances, delta, normals, offsets, face_idx), key=abs(distances), dim=batch('consider') & instance(self).as_batch())

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Tensor) -> Tensor:
    return self.approximate_closest_surface(location)[0]

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    raise NotImplementedError

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    h_min, h_max = self._faces.extrema_points[{'consider': 0, 'vector': self._hdim}].extremum
    dh = h_max - h_min
    return stack({d: self.dx[d] if d in self.resolution else dh for d in self.vector.item_names}, channel('vector'), expand_values=True) * .5

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    return self._bounds.bounding_radius()

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    location = rename_dims(location, self.resolution.names, ['loc_' + n for n in self.resolution.names])
    projected_loc = location[self.resolution.name_list]
    @math.map_i2b
    def lies_inside_(height, grid_bounds, bounds, fill_below, extrapolation):
        float_idx = (projected_loc - grid_bounds.lower) / grid_bounds.size * self.resolution
        if extrapolation is None:
            within_bounds = bounds.lies_inside(location)
        else:
            within_bounds = bounds[self._hdim].lies_inside(location[self._hdim])
        surface_height = math.grid_sample(height, float_idx - 1, math.NAN if extrapolation is None else extrapolation)
        is_below = location[self._hdim] <= surface_height
        inside = is_below == fill_below
        result = math.where(within_bounds, inside, False)
        return rename_dims(result, ['loc_' + n for n in self.resolution.names], self.resolution.names)
    return math.any(lies_inside_(self._height, self.grid_bounds, self._bounds, self._fill_below, self._extrapolation), instance(self))

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def shallow_equals(self, other)
Expand source code
def shallow_equals(self, other):
    return self == other

Quick equality check. May return False even if other == self. However, if True is returned, the geometries are guaranteed to be equal.

The shallow_equals() check does not compare all tensor elements but merely checks whether the same tensors are referenced.

class Mesh (vertices: phi.geom._geom.Geometry,
elements: phiml.math._tensors.Tensor,
element_rank: int,
boundaries: Dict[str, Dict[str, slice]],
periodic: Sequence[str],
face_format: str = 'csc',
max_cell_walk: int = None,
variable_attrs: Tuple[str, ...] = ('vertices',),
value_attrs: Tuple[str, ...] = ())
Expand source code
@dataclass(frozen=True)
class Mesh(Geometry):
    """
    Unstructured mesh, consisting of vertices and elements.
    
    Use `phi.geom.mesh()` or `phi.geom.mesh_from_numpy()` to construct a mesh manually or `phi.geom.load_su2()` to load one from a file.
    """

    vertices: Geometry
    """ Vertices are represented by a `Geometry` instance with an instance dim. """
    elements: Tensor
    """ elements: Sparse `Tensor` listing ordered vertex indices per element (solid or surface element, depending on `element_rank`).
    Must have one instance dim listing the elements and the corresponding dual dim to `vertices`.
    The vertex count of an element is equal to the number of elements in that row (i.e. summing the dual dim). """
    element_rank: int
    """The spatial rank of the elements. Solid elements have the same as the ambient space, faces one less."""
    boundaries: Dict[str, Dict[str, slice]]
    """Slices to retrieve boundary face values."""
    periodic: Sequence[str]
    """List of axis names that are periodic. Periodic boundaries must be named as axis- and axis+. For example `['x']` will connect the boundaries x- and x+."""
    face_format: str = 'csc'
    """Sparse matrix format for storing quantities that depend on a pair of neighboring elements, e.g. `face_area`, `face_normal`, `face_center`."""
    max_cell_walk: int = None
    """ Maximum number of steps to walk along the element connectivity in order to find a cell, e.g. for sampling at an arbitrary point."""

    variable_attrs: Tuple[str, ...] = ('vertices',)  # PhiML keyword
    value_attrs: Tuple[str, ...] = ()  # PhiML keyword

    def __post_init__(self):
        if spatial(self.elements):
            assert self.elements.dtype.kind == int, f"elements listing vertices must be integer lists but got dtype {self.elements.dtype}"
        else:
            assert self.elements.dtype.kind == bool, f"element matrices must be of type bool but got {self.elements.dtype}"

    @cached_property
    def shape(self) -> Shape:
        return non_dual(self.elements) & channel(self.vertices) & batch(self.vertices)

    @cached_property
    def cell_count(self):
        return instance(self.elements).size

    @cached_property
    def center(self) -> Tensor:
        if self.element_rank == self.spatial_rank:  # Compute volumetric center from faces
            return sum_(self.face_centers * self.face_areas, dual) / sum_(self.face_areas, dual)
        else:  # approximate center from vertices
            return self._vertex_mean

    @cached_property
    def _vertex_mean(self):
        """Mean vertex location per element."""
        vertex_count = sum_(self.elements, instance(self.vertices).as_dual())
        return (self.elements @ self.vertices.center) / vertex_count

    @cached_property
    def face_centers(self) -> Tensor:
        return self._faces['center']

    @property
    def face_areas(self) -> Tensor:
        return self._faces['area']

    @cached_property
    def face_normals(self) -> Tensor:
        if self.element_rank == self.spatial_rank:  # this cannot depend on element centers because that depends on the normals.
            normals = self._faces['normal']
            face_centers = self._faces['center']
            normals_out = normals.vector * (face_centers - self._vertex_mean).vector > 0
            normals = where(normals_out, normals, -normals)
            return normals
        raise NotImplementedError

    @cached_property
    def _faces(self) -> Dict[str, Any]:
        centers, normals, areas, boundary_slices = build_faces(self.vertices.center, self.elements, self.boundaries, self.element_rank, self.periodic, self._vertex_mean, self.face_format)
        return {
            'center': centers,
            'normal': normals,
            'area': areas,
            'boundary_slices': boundary_slices,
        }

    @property
    def face_shape(self) -> Shape:
        return instance(self.elements) & dual

    @property
    def sets(self):
        return {
            'center': non_batch(self)-'vector',
            'vertex': instance(self.vertices),
            '~vertex': dual(self.elements)
        }

    def get_points(self, set_key: str) -> Tensor:
        if set_key == 'vertex':
            return self.vertices.center
        elif set_key == '~vertex':
            return si2d(self.vertices.center)
        else:
            return Geometry.get_points(self, set_key)

    def get_boundary(self, set_key: str) -> Dict[str, Dict[str, slice]]:
        if set_key in ['vertex', '~vertex']:
            return {}
        return Geometry.get_boundary(self, set_key)

    @property
    def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[str, Dict[str, slice]]:
        return self._faces['boundary_slices']

    @property
    def all_boundary_faces(self) -> Dict[str, slice]:
        return {self.face_shape.dual.name: slice(instance(self).volume, None)}
    
    @property
    def interior_faces(self) -> Dict[str, slice]:
        return {self.face_shape.dual.name: slice(0, instance(self).volume)}

    def pad_boundary(self, value: Tensor, widths: Dict[str, Dict[str, slice]] = None, mode: Extrapolation or Tensor or Number = 0, **kwargs) -> Tensor:
        mode = as_extrapolation(mode)
        if self.face_shape.dual.name not in value.shape:
            value = rename_dims(value, instance, self.face_shape.dual)
        else:
            raise NotImplementedError
        if widths is None:
            widths = self.boundary_faces
        if isinstance(widths, (tuple, list)):
            if len(widths) == 0 or isinstance(widths[0], dict):  # add sliced-off slices
                pass
        dim = next(iter(next(iter(widths.values()))))
        slices = [slice(0, value.shape.get_size(dim))]
        values = [value]
        connectivity = self.connectivity
        for name, b_slice in widths.items():
            if b_slice[dim].stop - b_slice[dim].start > 0:
                slices.append(b_slice[dim])
                values.append(mode.sparse_pad_values(value, connectivity[b_slice], name, mesh=self, **kwargs))
        perm = np.argsort([s.start for s in slices])
        ordered_pieces = [values[i] for i in perm]
        return concat(ordered_pieces, dim, expand_values=True)

    @cached_property
    def cell_connectivity(self) -> Tensor:
        """
        Returns a bool-like matrix whose non-zero entries denote connected elements.
        In meshes or grids, elements are connected if they share a face in 3D, an edge in 2D, or a vertex in 1D.

        Returns:
            `Tensor` of shape (elements, ~elements)
        """
        return self.connectivity[self.interior_faces]

    @cached_property
    def boundary_connectivity(self) -> Tensor:
        return self.connectivity[self.all_boundary_faces]

    @cached_property
    def distance_matrix(self):
        return vec_length(pairwise_distances(self.center, edges=self.cell_connectivity, format='as edges', default=None))

    def faces_to_vertices(self, values: Tensor, reduce=sum):
        v = stored_values(values, invalid='keep')  # ToDo replace this once PhiML has support for dense instance dims and sparse scatter
        i = stored_values(self.face_vertices, invalid='keep')
        i = rename_dims(i, channel, instance)
        out_shape = non_channel(self.vertices) & shape(values).without(self.face_shape)
        return scatter(out_shape, i, v, mode=reduce, outside_handling='undefined')

    @cached_property
    def _cell_deltas(self):
        bounds = bounding_box(self.vertices)
        periodic = {dim[:-len('[::-1]')] if dim.endswith('[::-1]') else dim: dim.endswith('[::-1]') for dim in self.periodic}
        is_periodic = dim_mask(self.vector.item_names, tuple(periodic))
        return pairwise_distances(self.center, format=self.cell_connectivity, periodic=is_periodic, domain=(bounds.lower, bounds.upper))

    @cached_property
    def relative_face_distance(self):
        """|face_center - center| / |neighbor_center - center|"""
        cell_distances = vec_length(self._cell_deltas)
        assert (cell_distances > 0).all, f"All cells must have distance > 0 but found 0 distance at {nonzero(cell_distances == 0)}"
        face_distances = vec_length(self.face_centers[self.interior_faces] - self.center)
        return concat([face_distances / cell_distances, self.boundary_connectivity], self.face_shape.dual)

    @cached_property
    def neighbor_offsets(self):
        """Returns shift vector to neighbor centroids and boundary faces."""
        boundary_deltas = (self.face_centers - self.center)[self.all_boundary_faces]
        assert (vec_length(boundary_deltas) > 0).all, f"All boundary faces must be separated from the cell centers but 0 distance at the following {channel(stored_indices(boundary_deltas)).item_names[0]}:\n{nonzero(vec_length(boundary_deltas) == 0):full}"
        return concat([self._cell_deltas, boundary_deltas], self.face_shape.dual)

    @cached_property
    def neighbor_distances(self):
        return vec_length(self.neighbor_offsets)

    @property
    def faces(self) -> 'Geometry':
        """
        Assembles information about the boundaries of the elements that make up the surface.
        For 2D elements, the faces are edges, for 3D elements, the faces are planar elements.

        Returns:
            center: Center of face connecting a pair of elements. Shape (~elements, elements, vector).
                Returns 0-vectors for unconnected elements.
            area: Area of face connecting a pair of elements. Shape (~elements, elements).
                Returns 0 for unconnected elements.
            normal: Normal vector of face connecting a pair of elements. Shape (~elements, elements, vector).
                Unconnected elements are assigned the vector 0.
                The vector points out of polygon and into ~polygon.
        """
        return Point(self.face_centers)

    @property
    def connectivity(self) -> Tensor:
        return self.element_connectivity

    @cached_property
    def element_connectivity(self) -> Tensor:
        if self.element_rank == self.spatial_rank:
            if is_sparse(self.face_areas):
                return tensor_like(self.face_areas, True)
            else:
                return self.face_areas > 0
        else:  # fallback with no boundaries
            coo = to_format(self.elements, 'coo').numpy()
            connected_elements = coo @ coo.T
            connected_elements.data = np.ones_like(connected_elements.data)
            element_connectivity = wrap(connected_elements, instance(self.elements), instance(self.elements).as_dual())
            return element_connectivity

    @cached_property
    def vertex_connectivity(self) -> Tensor:
        if isinstance(self.vertices, Graph):
            return self.vertices.connectivity
        elif self.element_rank <= 2:
            def single_vertex_connectivity(elements: Tensor):
                indices = stored_indices(elements).index[dual(elements).name]
                idx1 = indices.numpy()
                v_count = sum_(elements, dual).numpy()
                ptr_end = np.cumsum(v_count)
                roll = np.arange(idx1.size) + 1
                roll[ptr_end-1] = ptr_end - v_count
                idx2 = idx1[roll]
                v_conn = coo_matrix((np.ones(idx1.size, dtype=bool), (idx1, idx2)), shape=(dual(elements).size,)*2).tocsr()
                return wrap(v_conn, dual(elements).as_instance(), dual(elements))
            return math.map(single_vertex_connectivity, self.elements, dims=batch)
        raise NotImplementedError

    @cached_property
    def vertex_graph(self) -> Graph:
        return self.vertices if isinstance(self.vertices, Graph) else graph(self.vertices, self.vertex_connectivity)

    def filter_unused_vertices(self) -> 'Mesh':
        coo = to_format(self.elements, 'coo').numpy()
        has_element = np.asarray(coo.sum(0) > 0)[0]
        new_index = np.cumsum(has_element) - 1
        new_index_t = wrap(new_index, dual(self.elements))
        has_element = wrap(has_element, instance(self.vertices))
        has_element_d = si2d(has_element)
        vertices = self.vertices[has_element]
        v_normals = self.vertex_normals[has_element_d]
        vertex_connectivity = None
        # if self._vertex_connectivity is not None:
        #     vertex_connectivity = stored_indices(self._vertex_connectivity).index.as_batch()
        #     vertex_connectivity = new_index_t[{dual: vertex_connectivity}].index.as_channel()
        #     vertex_connectivity = sparse_tensor(vertex_connectivity, stored_values(self._vertex_connectivity), non_batch(self._vertex_connectivity).with_sizes(instance(vertices).size), False)
        if isinstance(self.elements, CompactSparseTensor):
            indices = new_index_t[{dual: self.elements._indices}]
            elements = CompactSparseTensor(indices, self.elements._values, self.elements._compressed_dims.with_size(instance(vertices).volume), self.elements._indices_constant, self.elements._matrix_rank)
        else:
            filtered_coo = coo_matrix((coo.data, (coo.row, new_index)), shape=(instance(self.elements).volume, instance(vertices).volume))  # ToDo keep sparse format
            elements = wrap(filtered_coo, self.elements.shape.without_sizes())
        return replace(self, vertices=vertices, elements=elements)

    @property
    def volume(self) -> Tensor:
        if self.element_rank == 2:
            if instance(self.elements).volume > 0:
                three_vertices = nonzero(self.elements, 3, list_dims=dual)
                v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual)
                cross_area = vec_length(cross(v2-v1, v3-v1))
                vertex_count = math.sum(self.elements, dual)
                fac = where(vertex_count == 3, 0.5, 1)  # tri, quad, ...
                return fac * cross_area
            else:
                return zeros(instance(self.vertices))  # empty mesh
        elif self.element_rank == self.spatial_rank:
            vol_contributions = (self.face_centers.vector @ self.face_normals.vector) * self.face_areas
            return sum_(vol_contributions, dual) / self.spatial_rank
        raise NotImplementedError


    @property
    def normals(self) -> Tensor:
        """Extrinsic element normal space. This is a 0D vector for solid elements and 1D for surface elements."""
        if self.element_rank == 2:
            three_vertices = nonzero(self.elements, 3, list_dims=dual)
            v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual)
            return vec_normalize(cross(v2 - v1, v3 - v1))
        raise NotImplementedError

    @property
    def vertex_normals(self) -> Tensor:
        v_normals = mean(self.elements * self.normals, instance)  # (~vertices,vector)
        return vec_normalize(v_normals)

    @property
    def vertex_positions(self) -> Tensor:
        """Lists the vertex centers along the corresponding dual dim to `self.vertices.center`."""
        return si2d(self.vertices.center)

    def lies_inside(self, location: Tensor) -> Tensor:
        idx = find_closest(self.center, location)
        for i in range(self.max_cell_walk):
            idx, leaves_mesh, is_outside, *_ = self.cell_walk_towards(location, idx, allow_exit=i == self.max_cell_walk - 1)
        return ~(leaves_mesh & is_outside)

    def approximate_signed_distance(self, location: Union[Tensor, tuple]) -> Tensor:
        if self.element_rank == 2 and self.spatial_rank == 3:
            closest_elem = find_closest(self.center, location)
            center = self.center[closest_elem]
            normal = self.normals[closest_elem]
            return plane_sgn_dist(center, normal, location)
        idx = find_closest(self.center, location)
        for i in range(self.max_cell_walk):
            idx, leaves_mesh, is_outside, distances, nb_idx = self.cell_walk_towards(location, idx, allow_exit=False)
        return math.max(distances, dual)

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        if self.element_rank == 2 and self.spatial_rank == 3:
            closest_elem = find_closest(self.center, location)
            center = self.center[closest_elem]
            normal = self.normals[closest_elem]
            face_size = sqrt(self.volume) * 4
            size = face_size[closest_elem]
            sgn_dist = plane_sgn_dist(center, normal, location)
            delta = center - location  # this is not accurate...
            outward = where(abs(sgn_dist) < size, normal, vec_normalize(delta))
            return sgn_dist, delta, outward, None, closest_elem
        # idx = find_closest(self.center, location)
        # for i in range(self.max_cell_walk):
        #     idx, leaves_mesh, is_outside, distances, nb_idx = self.cell_walk_towards(location, idx, allow_exit=False)
        # sgn_dist = max(distances, dual)
        # cell_normals = self.face_normals[idx]
        # normal = cell_normals[{dual: nb_idx}]
        # return sgn_dist, delta, normal, offset, face_index
        raise NotImplementedError

    def cell_walk_towards(self, location: Tensor, start_cell_idx: Tensor, allow_exit=False):
        """
        If `location` is not within the cell at index `from_cell_idx`, moves to a closer neighbor cell.

        Args:
            location: Target location as `Tensor`.
            start_cell_idx: Index of starting cell. Must be a valid cell index.
            allow_exit: If `True`, returns an invalid index for points outside the mesh, otherwise keeps the current index.

        Returns:
            index: Index of the neighbor cell or starting cell.
            leaves_mesh: Whether the walk crossed the mesh boundary. Then `index` is invalid. This is only possible if `allow_exit` is true.
            is_outside: Whether `location` was outside the cell at index `start_cell_idx`.
        """
        closest_normals = self.face_normals[start_cell_idx]
        closest_face_centers = self.face_centers[start_cell_idx]
        offsets = closest_normals.vector @ closest_face_centers.vector  # this dot product could be cashed in the mesh
        distances = closest_normals.vector @ location.vector - offsets
        is_outside = math.any(distances > 0, dual)
        nb_idx = argmax(distances, dual).index[0]  # cell index or boundary face index
        leaves_mesh = nb_idx >= instance(self).volume
        next_idx = where(is_outside & (~leaves_mesh | allow_exit), nb_idx, start_cell_idx)
        return next_idx, leaves_mesh, is_outside, distances, nb_idx

    def sample_uniform(self, *shape: Shape) -> Tensor:
        raise NotImplementedError

    def bounding_radius(self) -> Tensor:
        center = self.elements * self.center
        vert_pos = rename_dims(self.vertices.center, instance, dual)
        dist_to_vert = vec_length(vert_pos - center)
        max_dist = math.max(dist_to_vert, dual)
        return max_dist

    def bounding_half_extent(self) -> Tensor:
        center = self.elements * self.center
        vert_pos = rename_dims(self.vertices.center, instance, dual)
        max_delta = math.max(abs(vert_pos - center), dual)
        return max_delta

    def bounding_box(self) -> 'BaseBox':
        return self.vertices.bounding_box()

    @property
    def bounds(self):
        return Box(math.min(self.vertices.center, instance), math.max(self.vertices.center, instance))

    def at(self, center: Tensor) -> 'Mesh':
        if instance(self.elements) in center.shape:
            raise NotImplementedError("Setting Mesh positions only supported for vertices, not elements")
        if dual(self.elements) in center.shape:
            center = rename_dims(center, dual, instance(self.vertices))
        if instance(self.vertices) in center.shape:
            vertices = self.vertices.at(center)
            return replace(self, vertices=vertices)
        else:
            return self.shifted(center - self.bounds.center)

    def shifted(self, delta: Tensor) -> 'Mesh':
        if instance(self.elements) in delta.shape:
            raise NotImplementedError("Shifting Mesh positions only supported for vertices, not elements")
        if dual(self.elements) in delta.shape:
            delta = rename_dims(delta, dual, instance(self.vertices))
        if instance(self.vertices) in delta.shape:
            vertices = self.vertices.shifted(delta)
            return replace(self, vertices=vertices)
        else:  # shift everything
            # ToDo transfer cached properties
            # copy: center+delta, normals, volume, face_centers+delta, face_areas, face_normals, vertex_normals, vertex_connectivity, element_connectivity
            vertices = self.vertices.shifted(delta)
            return replace(self, vertices=vertices)

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError

    def scaled(self, factor: float | Tensor) -> 'Geometry':
        pivot = self.bounds.center
        vertices = scale(self.vertices, factor, pivot)
        # center = scale(Point(self.center), factor, pivot).center
        # volume = self.volume * factor**self.element_rank if self.volume is not None else None
        return replace(self, vertices=vertices)

    def __getitem__(self, item):
        item: dict = slicing_dict(self, item)
        assert not dual(self.elements).only(tuple(item)), f"Cannot slice vertex lists ('{spatial(self.elements)}') but got slicing dict {item}"
        return getitem(self, item, keepdims=[self.shape.instance.name, 'vector'])

    def __repr__(self):
        return Geometry.__repr__(self)

Unstructured mesh, consisting of vertices and elements.

Use mesh() or mesh_from_numpy() to construct a mesh manually or load_su2() to load one from a file.

Ancestors

  • phi.geom._geom.Geometry

Class variables

var boundaries : Dict[str, Dict[str, slice]]

Slices to retrieve boundary face values.

var element_rank : int

The spatial rank of the elements. Solid elements have the same as the ambient space, faces one less.

var elements : phiml.math._tensors.Tensor

elements: Sparse Tensor listing ordered vertex indices per element (solid or surface element, depending on element_rank). Must have one instance dim listing the elements and the corresponding dual dim to vertices. The vertex count of an element is equal to the number of elements in that row (i.e. summing the dual dim).

var face_format : str

Sparse matrix format for storing quantities that depend on a pair of neighboring elements, e.g. face_area, face_normal, face_center.

var max_cell_walk : int

Maximum number of steps to walk along the element connectivity in order to find a cell, e.g. for sampling at an arbitrary point.

var periodic : Sequence[str]

List of axis names that are periodic. Periodic boundaries must be named as axis- and axis+. For example ['x'] will connect the boundaries x- and x+.

var value_attrs : Tuple[str, ...]
var variable_attrs : Tuple[str, ...]
var vertices : phi.geom._geom.Geometry

Vertices are represented by a Geometry instance with an instance dim.

Instance variables

prop all_boundary_faces : Dict[str, slice]
Expand source code
@property
def all_boundary_faces(self) -> Dict[str, slice]:
    return {self.face_shape.dual.name: slice(instance(self).volume, None)}
var boundary_connectivity : phiml.math._tensors.Tensor
Expand source code
@cached_property
def boundary_connectivity(self) -> Tensor:
    return self.connectivity[self.all_boundary_faces]
prop boundary_elements : Dict[str, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[str, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[str, Dict[str, slice]]:
    return self._faces['boundary_slices']

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounds
Expand source code
@property
def bounds(self):
    return Box(math.min(self.vertices.center, instance), math.max(self.vertices.center, instance))
var cell_connectivity : phiml.math._tensors.Tensor
Expand source code
@cached_property
def cell_connectivity(self) -> Tensor:
    """
    Returns a bool-like matrix whose non-zero entries denote connected elements.
    In meshes or grids, elements are connected if they share a face in 3D, an edge in 2D, or a vertex in 1D.

    Returns:
        `Tensor` of shape (elements, ~elements)
    """
    return self.connectivity[self.interior_faces]

Returns a bool-like matrix whose non-zero entries denote connected elements. In meshes or grids, elements are connected if they share a face in 3D, an edge in 2D, or a vertex in 1D.

Returns

Tensor of shape (elements, ~elements)

var cell_count
Expand source code
@cached_property
def cell_count(self):
    return instance(self.elements).size
var center : phiml.math._tensors.Tensor
Expand source code
@cached_property
def center(self) -> Tensor:
    if self.element_rank == self.spatial_rank:  # Compute volumetric center from faces
        return sum_(self.face_centers * self.face_areas, dual) / sum_(self.face_areas, dual)
    else:  # approximate center from vertices
        return self._vertex_mean
prop connectivity : phiml.math._tensors.Tensor
Expand source code
@property
def connectivity(self) -> Tensor:
    return self.element_connectivity
var distance_matrix
Expand source code
@cached_property
def distance_matrix(self):
    return vec_length(pairwise_distances(self.center, edges=self.cell_connectivity, format='as edges', default=None))
var element_connectivity : phiml.math._tensors.Tensor
Expand source code
@cached_property
def element_connectivity(self) -> Tensor:
    if self.element_rank == self.spatial_rank:
        if is_sparse(self.face_areas):
            return tensor_like(self.face_areas, True)
        else:
            return self.face_areas > 0
    else:  # fallback with no boundaries
        coo = to_format(self.elements, 'coo').numpy()
        connected_elements = coo @ coo.T
        connected_elements.data = np.ones_like(connected_elements.data)
        element_connectivity = wrap(connected_elements, instance(self.elements), instance(self.elements).as_dual())
        return element_connectivity
prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    return self._faces['area']

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

var face_centers : phiml.math._tensors.Tensor
Expand source code
@cached_property
def face_centers(self) -> Tensor:
    return self._faces['center']
var face_normals : phiml.math._tensors.Tensor
Expand source code
@cached_property
def face_normals(self) -> Tensor:
    if self.element_rank == self.spatial_rank:  # this cannot depend on element centers because that depends on the normals.
        normals = self._faces['normal']
        face_centers = self._faces['center']
        normals_out = normals.vector * (face_centers - self._vertex_mean).vector > 0
        normals = where(normals_out, normals, -normals)
        return normals
    raise NotImplementedError
prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return instance(self.elements) & dual

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    """
    Assembles information about the boundaries of the elements that make up the surface.
    For 2D elements, the faces are edges, for 3D elements, the faces are planar elements.

    Returns:
        center: Center of face connecting a pair of elements. Shape (~elements, elements, vector).
            Returns 0-vectors for unconnected elements.
        area: Area of face connecting a pair of elements. Shape (~elements, elements).
            Returns 0 for unconnected elements.
        normal: Normal vector of face connecting a pair of elements. Shape (~elements, elements, vector).
            Unconnected elements are assigned the vector 0.
            The vector points out of polygon and into ~polygon.
    """
    return Point(self.face_centers)

Assembles information about the boundaries of the elements that make up the surface. For 2D elements, the faces are edges, for 3D elements, the faces are planar elements.

Returns

center
Center of face connecting a pair of elements. Shape (~elements, elements, vector). Returns 0-vectors for unconnected elements.
area
Area of face connecting a pair of elements. Shape (~elements, elements). Returns 0 for unconnected elements.
normal
Normal vector of face connecting a pair of elements. Shape (~elements, elements, vector). Unconnected elements are assigned the vector 0. The vector points out of polygon and into ~polygon.
prop interior_faces : Dict[str, slice]
Expand source code
@property
def interior_faces(self) -> Dict[str, slice]:
    return {self.face_shape.dual.name: slice(0, instance(self).volume)}
var neighbor_distances
Expand source code
@cached_property
def neighbor_distances(self):
    return vec_length(self.neighbor_offsets)
var neighbor_offsets
Expand source code
@cached_property
def neighbor_offsets(self):
    """Returns shift vector to neighbor centroids and boundary faces."""
    boundary_deltas = (self.face_centers - self.center)[self.all_boundary_faces]
    assert (vec_length(boundary_deltas) > 0).all, f"All boundary faces must be separated from the cell centers but 0 distance at the following {channel(stored_indices(boundary_deltas)).item_names[0]}:\n{nonzero(vec_length(boundary_deltas) == 0):full}"
    return concat([self._cell_deltas, boundary_deltas], self.face_shape.dual)

Returns shift vector to neighbor centroids and boundary faces.

prop normals : phiml.math._tensors.Tensor
Expand source code
@property
def normals(self) -> Tensor:
    """Extrinsic element normal space. This is a 0D vector for solid elements and 1D for surface elements."""
    if self.element_rank == 2:
        three_vertices = nonzero(self.elements, 3, list_dims=dual)
        v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual)
        return vec_normalize(cross(v2 - v1, v3 - v1))
    raise NotImplementedError

Extrinsic element normal space. This is a 0D vector for solid elements and 1D for surface elements.

var relative_face_distance
Expand source code
@cached_property
def relative_face_distance(self):
    """|face_center - center| / |neighbor_center - center|"""
    cell_distances = vec_length(self._cell_deltas)
    assert (cell_distances > 0).all, f"All cells must have distance > 0 but found 0 distance at {nonzero(cell_distances == 0)}"
    face_distances = vec_length(self.face_centers[self.interior_faces] - self.center)
    return concat([face_distances / cell_distances, self.boundary_connectivity], self.face_shape.dual)

|face_center - center| / |neighbor_center - center|

prop sets
Expand source code
@property
def sets(self):
    return {
        'center': non_batch(self)-'vector',
        'vertex': instance(self.vertices),
        '~vertex': dual(self.elements)
    }
var shape : phiml.math._shape.Shape
Expand source code
@cached_property
def shape(self) -> Shape:
    return non_dual(self.elements) & channel(self.vertices) & batch(self.vertices)
var vertex_connectivity : phiml.math._tensors.Tensor
Expand source code
@cached_property
def vertex_connectivity(self) -> Tensor:
    if isinstance(self.vertices, Graph):
        return self.vertices.connectivity
    elif self.element_rank <= 2:
        def single_vertex_connectivity(elements: Tensor):
            indices = stored_indices(elements).index[dual(elements).name]
            idx1 = indices.numpy()
            v_count = sum_(elements, dual).numpy()
            ptr_end = np.cumsum(v_count)
            roll = np.arange(idx1.size) + 1
            roll[ptr_end-1] = ptr_end - v_count
            idx2 = idx1[roll]
            v_conn = coo_matrix((np.ones(idx1.size, dtype=bool), (idx1, idx2)), shape=(dual(elements).size,)*2).tocsr()
            return wrap(v_conn, dual(elements).as_instance(), dual(elements))
        return math.map(single_vertex_connectivity, self.elements, dims=batch)
    raise NotImplementedError
var vertex_graph : phi.geom._graph.Graph
Expand source code
@cached_property
def vertex_graph(self) -> Graph:
    return self.vertices if isinstance(self.vertices, Graph) else graph(self.vertices, self.vertex_connectivity)
prop vertex_normals : phiml.math._tensors.Tensor
Expand source code
@property
def vertex_normals(self) -> Tensor:
    v_normals = mean(self.elements * self.normals, instance)  # (~vertices,vector)
    return vec_normalize(v_normals)
prop vertex_positions : phiml.math._tensors.Tensor
Expand source code
@property
def vertex_positions(self) -> Tensor:
    """Lists the vertex centers along the corresponding dual dim to `self.vertices.center`."""
    return si2d(self.vertices.center)

Lists the vertex centers along the corresponding dual dim to self.vertices.center.

prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    if self.element_rank == 2:
        if instance(self.elements).volume > 0:
            three_vertices = nonzero(self.elements, 3, list_dims=dual)
            v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual)
            cross_area = vec_length(cross(v2-v1, v3-v1))
            vertex_count = math.sum(self.elements, dual)
            fac = where(vertex_count == 3, 0.5, 1)  # tri, quad, ...
            return fac * cross_area
        else:
            return zeros(instance(self.vertices))  # empty mesh
    elif self.element_rank == self.spatial_rank:
        vol_contributions = (self.face_centers.vector @ self.face_normals.vector) * self.face_areas
        return sum_(vol_contributions, dual) / self.spatial_rank
    raise NotImplementedError

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    if self.element_rank == 2 and self.spatial_rank == 3:
        closest_elem = find_closest(self.center, location)
        center = self.center[closest_elem]
        normal = self.normals[closest_elem]
        face_size = sqrt(self.volume) * 4
        size = face_size[closest_elem]
        sgn_dist = plane_sgn_dist(center, normal, location)
        delta = center - location  # this is not accurate...
        outward = where(abs(sgn_dist) < size, normal, vec_normalize(delta))
        return sgn_dist, delta, outward, None, closest_elem
    # idx = find_closest(self.center, location)
    # for i in range(self.max_cell_walk):
    #     idx, leaves_mesh, is_outside, distances, nb_idx = self.cell_walk_towards(location, idx, allow_exit=False)
    # sgn_dist = max(distances, dual)
    # cell_normals = self.face_normals[idx]
    # normal = cell_normals[{dual: nb_idx}]
    # return sgn_dist, delta, normal, offset, face_index
    raise NotImplementedError

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor | tuple) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Union[Tensor, tuple]) -> Tensor:
    if self.element_rank == 2 and self.spatial_rank == 3:
        closest_elem = find_closest(self.center, location)
        center = self.center[closest_elem]
        normal = self.normals[closest_elem]
        return plane_sgn_dist(center, normal, location)
    idx = find_closest(self.center, location)
    for i in range(self.max_cell_walk):
        idx, leaves_mesh, is_outside, distances, nb_idx = self.cell_walk_towards(location, idx, allow_exit=False)
    return math.max(distances, dual)

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._mesh.Mesh
Expand source code
def at(self, center: Tensor) -> 'Mesh':
    if instance(self.elements) in center.shape:
        raise NotImplementedError("Setting Mesh positions only supported for vertices, not elements")
    if dual(self.elements) in center.shape:
        center = rename_dims(center, dual, instance(self.vertices))
    if instance(self.vertices) in center.shape:
        vertices = self.vertices.at(center)
        return replace(self, vertices=vertices)
    else:
        return self.shifted(center - self.bounds.center)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_box(self) ‑> phi.geom._box.BaseBox
Expand source code
def bounding_box(self) -> 'BaseBox':
    return self.vertices.bounding_box()

Returns the approximately smallest axis-aligned box that contains this Geometry. The center of the box may not be equal to self.center.

Returns

Box or Cuboid that fully contains this Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    center = self.elements * self.center
    vert_pos = rename_dims(self.vertices.center, instance, dual)
    max_delta = math.max(abs(vert_pos - center), dual)
    return max_delta

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    center = self.elements * self.center
    vert_pos = rename_dims(self.vertices.center, instance, dual)
    dist_to_vert = vec_length(vert_pos - center)
    max_dist = math.max(dist_to_vert, dual)
    return max_dist

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def cell_walk_towards(self,
location: phiml.math._tensors.Tensor,
start_cell_idx: phiml.math._tensors.Tensor,
allow_exit=False)
Expand source code
def cell_walk_towards(self, location: Tensor, start_cell_idx: Tensor, allow_exit=False):
    """
    If `location` is not within the cell at index `from_cell_idx`, moves to a closer neighbor cell.

    Args:
        location: Target location as `Tensor`.
        start_cell_idx: Index of starting cell. Must be a valid cell index.
        allow_exit: If `True`, returns an invalid index for points outside the mesh, otherwise keeps the current index.

    Returns:
        index: Index of the neighbor cell or starting cell.
        leaves_mesh: Whether the walk crossed the mesh boundary. Then `index` is invalid. This is only possible if `allow_exit` is true.
        is_outside: Whether `location` was outside the cell at index `start_cell_idx`.
    """
    closest_normals = self.face_normals[start_cell_idx]
    closest_face_centers = self.face_centers[start_cell_idx]
    offsets = closest_normals.vector @ closest_face_centers.vector  # this dot product could be cashed in the mesh
    distances = closest_normals.vector @ location.vector - offsets
    is_outside = math.any(distances > 0, dual)
    nb_idx = argmax(distances, dual).index[0]  # cell index or boundary face index
    leaves_mesh = nb_idx >= instance(self).volume
    next_idx = where(is_outside & (~leaves_mesh | allow_exit), nb_idx, start_cell_idx)
    return next_idx, leaves_mesh, is_outside, distances, nb_idx

If location is not within the cell at index from_cell_idx, moves to a closer neighbor cell.

Args

location
Target location as Tensor.
start_cell_idx
Index of starting cell. Must be a valid cell index.
allow_exit
If True, returns an invalid index for points outside the mesh, otherwise keeps the current index.

Returns

index
Index of the neighbor cell or starting cell.
leaves_mesh
Whether the walk crossed the mesh boundary. Then index is invalid. This is only possible if allow_exit is true.
is_outside
Whether location was outside the cell at index start_cell_idx.
def faces_to_vertices(self, values: phiml.math._tensors.Tensor, reduce=<built-in function sum>)
Expand source code
def faces_to_vertices(self, values: Tensor, reduce=sum):
    v = stored_values(values, invalid='keep')  # ToDo replace this once PhiML has support for dense instance dims and sparse scatter
    i = stored_values(self.face_vertices, invalid='keep')
    i = rename_dims(i, channel, instance)
    out_shape = non_channel(self.vertices) & shape(values).without(self.face_shape)
    return scatter(out_shape, i, v, mode=reduce, outside_handling='undefined')
def filter_unused_vertices(self) ‑> phi.geom._mesh.Mesh
Expand source code
def filter_unused_vertices(self) -> 'Mesh':
    coo = to_format(self.elements, 'coo').numpy()
    has_element = np.asarray(coo.sum(0) > 0)[0]
    new_index = np.cumsum(has_element) - 1
    new_index_t = wrap(new_index, dual(self.elements))
    has_element = wrap(has_element, instance(self.vertices))
    has_element_d = si2d(has_element)
    vertices = self.vertices[has_element]
    v_normals = self.vertex_normals[has_element_d]
    vertex_connectivity = None
    # if self._vertex_connectivity is not None:
    #     vertex_connectivity = stored_indices(self._vertex_connectivity).index.as_batch()
    #     vertex_connectivity = new_index_t[{dual: vertex_connectivity}].index.as_channel()
    #     vertex_connectivity = sparse_tensor(vertex_connectivity, stored_values(self._vertex_connectivity), non_batch(self._vertex_connectivity).with_sizes(instance(vertices).size), False)
    if isinstance(self.elements, CompactSparseTensor):
        indices = new_index_t[{dual: self.elements._indices}]
        elements = CompactSparseTensor(indices, self.elements._values, self.elements._compressed_dims.with_size(instance(vertices).volume), self.elements._indices_constant, self.elements._matrix_rank)
    else:
        filtered_coo = coo_matrix((coo.data, (coo.row, new_index)), shape=(instance(self.elements).volume, instance(vertices).volume))  # ToDo keep sparse format
        elements = wrap(filtered_coo, self.elements.shape.without_sizes())
    return replace(self, vertices=vertices, elements=elements)
def get_boundary(self, set_key: str) ‑> Dict[str, Dict[str, slice]]
Expand source code
def get_boundary(self, set_key: str) -> Dict[str, Dict[str, slice]]:
    if set_key in ['vertex', '~vertex']:
        return {}
    return Geometry.get_boundary(self, set_key)
def get_points(self, set_key: str) ‑> phiml.math._tensors.Tensor
Expand source code
def get_points(self, set_key: str) -> Tensor:
    if set_key == 'vertex':
        return self.vertices.center
    elif set_key == '~vertex':
        return si2d(self.vertices.center)
    else:
        return Geometry.get_points(self, set_key)
def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    idx = find_closest(self.center, location)
    for i in range(self.max_cell_walk):
        idx, leaves_mesh, is_outside, *_ = self.cell_walk_towards(location, idx, allow_exit=i == self.max_cell_walk - 1)
    return ~(leaves_mesh & is_outside)

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def pad_boundary(self,
value: phiml.math._tensors.Tensor,
widths: Dict[str, Dict[str, slice]] = None,
mode: phiml.math.extrapolation.Extrapolation = 0,
**kwargs) ‑> phiml.math._tensors.Tensor
Expand source code
def pad_boundary(self, value: Tensor, widths: Dict[str, Dict[str, slice]] = None, mode: Extrapolation or Tensor or Number = 0, **kwargs) -> Tensor:
    mode = as_extrapolation(mode)
    if self.face_shape.dual.name not in value.shape:
        value = rename_dims(value, instance, self.face_shape.dual)
    else:
        raise NotImplementedError
    if widths is None:
        widths = self.boundary_faces
    if isinstance(widths, (tuple, list)):
        if len(widths) == 0 or isinstance(widths[0], dict):  # add sliced-off slices
            pass
    dim = next(iter(next(iter(widths.values()))))
    slices = [slice(0, value.shape.get_size(dim))]
    values = [value]
    connectivity = self.connectivity
    for name, b_slice in widths.items():
        if b_slice[dim].stop - b_slice[dim].start > 0:
            slices.append(b_slice[dim])
            values.append(mode.sparse_pad_values(value, connectivity[b_slice], name, mesh=self, **kwargs))
    perm = np.argsort([s.start for s in slices])
    ordered_pieces = [values[i] for i in perm]
    return concat(ordered_pieces, dim, expand_values=True)
def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: float | Tensor) -> 'Geometry':
    pivot = self.bounds.center
    vertices = scale(self.vertices, factor, pivot)
    # center = scale(Point(self.center), factor, pivot).center
    # volume = self.volume * factor**self.element_rank if self.volume is not None else None
    return replace(self, vertices=vertices)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.geom._mesh.Mesh
Expand source code
def shifted(self, delta: Tensor) -> 'Mesh':
    if instance(self.elements) in delta.shape:
        raise NotImplementedError("Shifting Mesh positions only supported for vertices, not elements")
    if dual(self.elements) in delta.shape:
        delta = rename_dims(delta, dual, instance(self.vertices))
    if instance(self.vertices) in delta.shape:
        vertices = self.vertices.shifted(delta)
        return replace(self, vertices=vertices)
    else:  # shift everything
        # ToDo transfer cached properties
        # copy: center+delta, normals, volume, face_centers+delta, face_areas, face_normals, vertex_normals, vertex_connectivity, element_connectivity
        vertices = self.vertices.shifted(delta)
        return replace(self, vertices=vertices)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
class Point (location: phiml.math._tensors.Tensor)
Expand source code
class Point(Geometry):
    """
    Points have zero volume and are determined by a single location.
    An instance of `Point` represents a single n-dimensional point or a batch of points.
    """

    def __init__(self, location: math.Tensor):
        assert 'vector' in location.shape, "location must have a vector dimension"
        assert location.shape.get_item_names('vector') is not None, "Vector dimension needs to list spatial dimension as item names."
        self._location = location
        self._shape = self._location.shape

    def __variable_attrs__(self):
        return '_location',

    def __value_attrs__(self):
        return '_location',

    def __with_attrs__(self, **updates):
        if '_location' in updates:
            result = Point.__new__(Point)
            result._location = updates['_location']
            result._shape = result._location.shape if result._location is not None else self._shape
            return result
        else:
            return self

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

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

    @property
    def faces(self) -> 'Geometry':
        return self

    def unstack(self, dimension: str) -> tuple:
        return tuple(Point(loc) for loc in math.unstack(self._location, dimension))

    def lies_inside(self, location: Tensor) -> Tensor:
        return expand(math.wrap(False), shape(location).without('vector'))

    def approximate_signed_distance(self, location: Union[Tensor, tuple]) -> Tensor:
        return math.vec_abs(location - self._location)

    def bounding_radius(self) -> Tensor:
        return math.zeros()

    def bounding_half_extent(self) -> Tensor:
        return tensor_like(self.center, 0)

    def at(self, center: Tensor) -> 'Geometry':
        return Point(center)

    def rotated(self, angle) -> 'Geometry':
        return self

    @property
    def volume(self) -> Tensor:
        return math.wrap(0)

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        raise NotImplementedError

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        return self

    @property
    def face_centers(self) -> Tensor:
        return self._location

    @property
    def face_areas(self) -> Tensor:
        return expand(0, self.face_shape)

    @property
    def face_normals(self) -> Tensor:
        raise AssertionError(f"Points have no normals")

    @property
    def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

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

    @property
    def corners(self):
        return self._location

    def __getitem__(self, item):
        return Point(self._location[_keep_vector(slicing_dict(self, item))])

Points have zero volume and are determined by a single location. An instance of Point represents a single n-dimensional point or a batch of points.

Ancestors

  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._location

Center location in single channel dimension.

prop corners
Expand source code
@property
def corners(self):
    return self._location

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    return expand(0, self.face_shape)

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    return self._location

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    raise AssertionError(f"Points have no normals")

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return self.shape

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    return self
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    return self._shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return math.wrap(0)

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_signed_distance(self, location: phiml.math._tensors.Tensor | tuple) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Union[Tensor, tuple]) -> Tensor:
    return math.vec_abs(location - self._location)

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    return Point(center)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    return tensor_like(self.center, 0)

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    return math.zeros()

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    return expand(math.wrap(False), shape(location).without('vector'))

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle) -> 'Geometry':
    return self

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    return self

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def unstack(self, dimension: str) ‑> tuple
Expand source code
def unstack(self, dimension: str) -> tuple:
    return tuple(Point(loc) for loc in math.unstack(self._location, dimension))

Unstacks this Geometry along the given dimension. The shapes of the returned geometries are reduced by dimension.

Args

dimension
dimension along which to unstack

Returns

geometries
tuple of length equal to geometry.shape.get_size(dimension)
class SDF (sdf: Callable,
out_shape=None,
bounds: phi.geom._box.BaseBox = None,
center: phiml.math._tensors.Tensor = None,
volume: phiml.math._tensors.Tensor = None,
bounding_radius: phiml.math._tensors.Tensor = None,
sdf_and_grad: Callable = None)
Expand source code
class SDF(Geometry):
    """
    Function-based signed distance field.
    Negative values lie inside the geometry, the 0-level represents the surface.
    """
    def __init__(self, sdf: Callable, out_shape=None, bounds: BaseBox = None, center: Tensor = None, volume: Tensor = None, bounding_radius: Tensor = None, sdf_and_grad: Callable = None):
        """
        Args:
            sdf: SDF function. First argument is a `phiml.math.Tensor` with a `vector` channel dim.
            bounds: Grid limits. The bounds fully enclose all virtual cells.
            center: (Optional) Geometry center point. Will be computed otherwise.
            volume: (Optional) Geometry volume. Will be computed otherwise.
            bounding_radius: (Optional) Geometry bounding radius around center. Will be computed otherwise.
        """
        self._sdf = sdf
        if out_shape is not None:
            self._out_shape = out_shape or math.EMPTY_SHAPE
        else:
            dims = channel([bounds, center, bounding_radius])
            assert 'vector' in dims, f"If out_shape is not specified, either bounds, center or bounding_radius must be given."
            self._out_shape = sdf(math.zeros(dims['vector'])).shape
        self._bounds = bounds
        if sdf_and_grad is not None:
            self._grad = sdf_and_grad
        else:
            self._grad = math.gradient(sdf, wrt=0, get_output=True)
        if center is not None:
            self._center = center
        else:
            self._center = bounds.center
        if volume is not None:
            self._volume = volume
        else:
            self._volume = None
        if bounding_radius is not None:
            self._bounding_radius = bounding_radius
        else:
            self._bounding_radius = self._bounds.bounding_radius()

    def __call__(self, location, *aux_args, **aux_kwargs):
        native_loc = not isinstance(location, Tensor)
        if native_loc:
            location = math.wrap(location, instance('points'), self.shape['vector'])
        sdf_val: Tensor = self._sdf(location, *aux_args, **aux_kwargs)
        return sdf_val.native() if native_loc else sdf_val

    def __variable_attrs__(self):
        return '_bounds', '_center', '_volume', '_bounding_radius'

    def __value_attrs__(self):
        return ()

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

    @property
    def size(self):
        return self._bounds.size

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

    @property
    def shape(self) -> Shape:
        return self._out_shape & self._bounds.shape

    @property
    def volume(self) -> Tensor:
        return self._volume

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_centers(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_areas(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_normals(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def face_shape(self) -> Shape:
        return math.EMPTY_SHAPE

    @property
    def corners(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support corners")

    def lies_inside(self, location: Tensor) -> Tensor:
        sdf = self._sdf(location)
        return sdf <= 0

    def approximate_closest_surface(self, location: Tensor, refine_iter=0) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        sgn_dist, outward = self._grad(location)
        closest = location - sgn_dist * outward
        if not refine_iter:
            _, normal = self._grad(closest)
        else:
            for i in range(refine_iter):
                sgn_dist, outward = self._grad(closest)
                closest -= sgn_dist * outward
            normal = outward
        offset = None
        face_index = None
        return sgn_dist, closest - location, normal, offset, face_index

    def sdf_and_gradient(self, location: Tensor, refine_iter=0) -> Tuple[Tensor, Tensor]:
        if not refine_iter:
            sgn_dist, outward = self._grad(location)
        else:
            sgn_dist, delta, *_ = self.approximate_closest_surface(location)
            outward = math.vec_normalize(math.sign(-sgn_dist) * delta)
        return sgn_dist, outward

    def approximate_signed_distance(self, location: Tensor) -> Tensor:
        return self._sdf(location)

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        raise NotImplementedError

    def bounding_radius(self) -> Tensor:
        return self._bounding_radius

    def bounding_half_extent(self) -> Tensor:
        return self._bounds.half_size  # this could be too small if the center is not in the middle of the bounds

    def bounding_box(self) -> 'BaseBox':
        return self._bounds

    def shifted(self, delta: Tensor) -> 'Geometry':
        raise NotImplementedError("SDF does not yet support shifting")

    def at(self, center: Tensor) -> 'Geometry':
        raise NotImplementedError("SDF does not yet support shifting")

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError("SDF does not yet support rotation")

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        off_center = self._center - self._bounds.center
        volume = self._volume * factor ** self.spatial_rank
        bounds = self._bounds.scaled(factor).shifted(off_center * (factor - 1)).corner_representation()
        return SDF(self._sdf, bounds, self._center, volume, self._bounding_radius * factor)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        if not item:
            return self
        raise NotImplementedError

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Geometry':
        from ._geom_ops import GeometryStack
        return GeometryStack(math.layout(values, dim))

Function-based signed distance field. Negative values lie inside the geometry, the 0-level represents the surface.

Args

sdf
SDF function. First argument is a phiml.math.Tensor with a vector channel dim.
bounds
Grid limits. The bounds fully enclose all virtual cells.
center
(Optional) Geometry center point. Will be computed otherwise.
volume
(Optional) Geometry volume. Will be computed otherwise.
bounding_radius
(Optional) Geometry bounding radius around center. Will be computed otherwise.

Ancestors

  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounds : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    return self._bounds
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._center

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support corners")

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return math.EMPTY_SHAPE

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError(f"SDF does not support faces")
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    return self._out_shape & self._bounds.shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size
Expand source code
@property
def size(self):
    return self._bounds.size
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return self._volume

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor, refine_iter=0) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor, refine_iter=0) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    sgn_dist, outward = self._grad(location)
    closest = location - sgn_dist * outward
    if not refine_iter:
        _, normal = self._grad(closest)
    else:
        for i in range(refine_iter):
            sgn_dist, outward = self._grad(closest)
            closest -= sgn_dist * outward
        normal = outward
    offset = None
    face_index = None
    return sgn_dist, closest - location, normal, offset, face_index

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Tensor) -> Tensor:
    return self._sdf(location)

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    raise NotImplementedError("SDF does not yet support shifting")

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_box(self) ‑> phi.geom._box.BaseBox
Expand source code
def bounding_box(self) -> 'BaseBox':
    return self._bounds

Returns the approximately smallest axis-aligned box that contains this Geometry. The center of the box may not be equal to self.center.

Returns

Box or Cuboid that fully contains this Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    return self._bounds.half_size  # this could be too small if the center is not in the middle of the bounds

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    return self._bounding_radius

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    sdf = self._sdf(location)
    return sdf <= 0

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError("SDF does not yet support rotation")

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    off_center = self._center - self._bounds.center
    volume = self._volume * factor ** self.spatial_rank
    bounds = self._bounds.scaled(factor).shifted(off_center * (factor - 1)).corner_representation()
    return SDF(self._sdf, bounds, self._center, volume, self._bounding_radius * factor)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def sdf_and_gradient(self, location: phiml.math._tensors.Tensor, refine_iter=0) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def sdf_and_gradient(self, location: Tensor, refine_iter=0) -> Tuple[Tensor, Tensor]:
    if not refine_iter:
        sgn_dist, outward = self._grad(location)
    else:
        sgn_dist, delta, *_ = self.approximate_closest_surface(location)
        outward = math.vec_normalize(math.sign(-sgn_dist) * delta)
    return sgn_dist, outward
def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def shifted(self, delta: Tensor) -> 'Geometry':
    raise NotImplementedError("SDF does not yet support shifting")

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
class SDFGrid (sdf: phiml.math._tensors.Tensor,
bounds: phi.geom._box.BaseBox,
approximate_outside=True,
gradient: phiml.math._tensors.Tensor = None,
to_surface: phiml.math._tensors.Tensor = None,
surf_normal: phiml.math._tensors.Tensor = None,
surf_index: phiml.math._tensors.Tensor = None,
center: phiml.math._tensors.Tensor = None,
volume: phiml.math._tensors.Tensor = None,
bounding_radius: phiml.math._tensors.Tensor = None)
Expand source code
class SDFGrid(Geometry):
    """
    Grid-based signed distance field.
    """
    def __init__(self,
                 sdf: Tensor,
                 bounds: BaseBox,
                 approximate_outside=True,
                 gradient: Tensor = None,
                 to_surface: Tensor = None, surf_normal: Tensor = None, surf_index: Tensor = None,
                 center: Tensor = None, volume: Tensor = None, bounding_radius: Tensor = None):
        """
        Args:
            sdf: Signed distance values. `Tensor` with spatial dimensions corresponding to the physical space.
                Each value samples the SDF value at the center of a virtual cell.
            bounds: Grid limits. The bounds fully enclose all virtual cells.
            approximate_outside: Whether queries outside the SDF grid should return approximate values. This requires additional computations.
            gradient: (Optional) Pre-computed gradient grid. Will be computed otherwise.
            center: (Optional) Geometry center point. Will be computed otherwise.
            volume: (Optional) Geometry volume. Will be computed otherwise.
            bounding_radius: (Optional) Geometry bounding radius around center. Will be computed otherwise.
        """
        super().__init__()
        self._sdf = sdf
        self._bounds = bounds
        self._approximate_outside = approximate_outside
        dx = bounds.size / spatial(sdf)
        if gradient is True:
            grad = math.spatial_gradient(sdf, dx=dx, difference='forward', padding=math.extrapolation.ZERO_GRADIENT, stack_dim=channel('vector'))
            self._grad = grad[{dim: slice(0, -1) for dim in spatial(sdf).names}]
        else:
            self._grad = gradient
        self._to_surface = to_surface
        self._surf_normal = surf_normal
        self._surf_index = surf_index
        if center is not None:
            self._center = center
        else:
            min_index = math.argmin(self._sdf, spatial, index_dim=channel('vector'))
            self._center = bounds.local_to_global(min_index / spatial(sdf))
        if volume is not None:
            self._volume = volume
        else:
            filled = math.sum(sdf < 0)
            self._volume = filled * math.prod(dx)
        if bounding_radius is not None:
            self._bounding_radius = bounding_radius
        else:
            points = UniformGrid(spatial(sdf), self._bounds).center
            dist = math.vec_length(points - self._center)
            dist = math.where(self._sdf <= 0, dist, 0)
            self._bounding_radius = math.max(dist)

    def __variable_attrs__(self):
        return '_sdf', '_bounds', '_grad', '_to_surface', '_surf_normal', '_surf_index', '_center', '_volume', '_bounding_radius'

    def __value_attrs__(self):
        return '_sdf',

    @property
    def values(self):
        """Signed distance grid."""
        return self._sdf

    def with_values(self, values: Tensor):
        values = expand(values, spatial(self._sdf) - spatial(values))
        return SDFGrid(values, self._bounds, self._approximate_outside, self._grad, self._center, self._volume, self._bounding_radius)

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

    @property
    def size(self) -> Tensor:
        return self._bounds.size

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

    @property
    def dx(self) -> Tensor:
        return self._bounds.size / spatial(self._sdf)

    @property
    def points(self) -> Tensor:
        return UniformGrid(spatial(self._sdf), self._bounds).center

    @property
    def grid(self) -> UniformGrid:
        return UniformGrid(spatial(self._sdf), self._bounds)

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

    @property
    def shape(self) -> Shape:
        return non_spatial(self._sdf) & channel(vector=spatial(self._sdf))

    @property
    def volume(self) -> Tensor:
        return self._volume

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_centers(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_areas(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def face_normals(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support faces")

    @property
    def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def face_shape(self) -> Shape:
        return math.EMPTY_SHAPE

    @property
    def corners(self) -> Tensor:
        raise NotImplementedError(f"SDF does not support corners")

    def lies_inside(self, location: Tensor) -> Tensor:
        float_idx = (location - self._bounds.lower) / self.size * self.resolution
        sdf_val = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
        if self._approximate_outside:
            within_bounds = self._bounds.lies_inside(location)
            return within_bounds & (sdf_val <= 0)
        else:
            return sdf_val <= 0

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        if self._approximate_outside:
            location = self._bounds.push(location, outward=False)
        float_idx = (location - self._bounds.lower) / self.size * self.resolution
        sgn_dist = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
        if self._to_surface is not None:
            to_surf = math.grid_sample(self._to_surface, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
        else:
            sdf_grad = math.grid_sample(self._grad, float_idx - 1, math.extrapolation.ZERO_GRADIENT)
            sdf_grad = math.vec_normalize(sdf_grad)  # theoretically not necessary
            to_surf = sgn_dist * -sdf_grad
        surface_pos = location + to_surf
        if self._surf_normal is not None:
            normal = math.grid_sample(self._surf_normal, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
            int_index = clip(math.to_int32(float_idx), 0, wrap(spatial(self._surf_index), '(x,y,z)')-1)
            face_index = self._surf_index[int_index]
        else:
            surf_float_idx = (surface_pos - self._bounds.lower) / self.size * self.resolution
            normal = math.grid_sample(self._grad, surf_float_idx - 1, math.extrapolation.ZERO_GRADIENT)
            # normal = math.where(self._bounds.lies_inside(surface_pos), normal, sdf_grad)  # use current normal if surface point is outside SDF grid
            normal = math.vec_normalize(normal)
            face_index = None
        offset = normal.vector @ surface_pos.vector
        return sgn_dist, to_surf, normal, offset, face_index

    def approximate_signed_distance(self, location: Tensor) -> Tensor:
        float_idx = (location - self._bounds.lower) / self.size * self.resolution
        sdf_val = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
        if self._approximate_outside:
            within_bounds = self._bounds.lies_inside(location)
            dist_from_center = math.vec_length(location - self._center) - self._bounding_radius
            return math.where(within_bounds, sdf_val, dist_from_center)
        else:
            return sdf_val

    def sample_uniform(self, *shape: math.Shape) -> Tensor:
        raise NotImplementedError

    def bounding_radius(self) -> Tensor:
        return self._bounding_radius

    def bounding_half_extent(self) -> Tensor:
        return self._bounds.half_size  # this could be too small if the center is not in the middle of the bounds

    def shifted(self, delta: Tensor) -> 'Geometry':
        return SDFGrid(self._sdf, self._bounds.shifted(delta), self._approximate_outside, self._grad, self._to_surface, self._surf_normal, self._surf_index, self._center + delta, self._volume, self._bounding_radius)

    def at(self, center: Tensor) -> 'Geometry':
        return self.shifted(center - self._center)

    def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
        raise NotImplementedError("SDF does not yet support rotation")

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        off_center = self._center - self._bounds.center
        volume = self._volume * factor ** self.spatial_rank
        bounds = self._bounds.scaled(factor).shifted(off_center * (factor - 1)).corner_representation()
        return SDFGrid(self._sdf, bounds, self._approximate_outside, self._grad, self._to_surface, self._surf_normal, self._surf_index, self._center, volume, self._bounding_radius * factor)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        if 'vector' in item:
            raise NotImplementedError("SDF projection not yet supported")
        return SDFGrid(self._sdf[item], self._bounds[item], self._approximate_outside, math.slice(self._grad, item), math.slice(self._to_surface, item), math.slice(self._surf_normal, item), math.slice(self._surf_index, item), math.slice(self._center, item), math.slice(self._volume, item), math.slice(self._bounding_radius, item))

    def approximate_occupancy(self):
        assert self._surf_normal is not None
        unit_corners = Cuboid(half_size=.5*self.dx).corners
        surf_dist = self._surf_normal.vector @ self._to_surface.vector
        corner_sdf = unit_corners.vector @ self._surf_normal.vector - surf_dist
        total_dist = math.sum(abs(corner_sdf), dual)
        neg_dist = math.sum(math.minimum(0, corner_sdf), dual)
        occ_near_surf = -neg_dist / total_dist
        occ_away = self._sdf <= 0
        return math.where(abs(self._sdf) < .5*math.vec_length(self.dx), occ_near_surf, occ_away)

    def approximate_fraction_inside(self, other_geometry: 'Geometry', balance: Union[Tensor, Number] = 0.5) -> Tensor:
        if other_geometry == self.grid and math.always_close(balance, .5):
            return self.approximate_occupancy()
        else:
            return Geometry.approximate_fraction_inside(self, other_geometry, balance)

    def downsample2x(self):
        s, g, t, n, i = [math.downsample2x(v) for v in (self._sdf, self._grad, self._to_surface, self._surf_normal, self._surf_index)]
        return SDFGrid(s, self._bounds, self._approximate_outside, g, t, n, i, self._center, self._volume, self._bounding_radius)

Grid-based signed distance field.

Args

sdf
Signed distance values. Tensor with spatial dimensions corresponding to the physical space. Each value samples the SDF value at the center of a virtual cell.
bounds
Grid limits. The bounds fully enclose all virtual cells.
approximate_outside
Whether queries outside the SDF grid should return approximate values. This requires additional computations.
gradient
(Optional) Pre-computed gradient grid. Will be computed otherwise.
center
(Optional) Geometry center point. Will be computed otherwise.
volume
(Optional) Geometry volume. Will be computed otherwise.
bounding_radius
(Optional) Geometry bounding radius around center. Will be computed otherwise.

Ancestors

  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounds : phi.geom._box.BaseBox
Expand source code
@property
def bounds(self) -> BaseBox:
    return self._bounds
prop center : phiml.math._tensors.Tensor
Expand source code
@property
def center(self) -> Tensor:
    return self._center

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support corners")

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop dx : phiml.math._tensors.Tensor
Expand source code
@property
def dx(self) -> Tensor:
    return self._bounds.size / spatial(self._sdf)
prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    raise NotImplementedError(f"SDF does not support faces")

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return math.EMPTY_SHAPE

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError(f"SDF does not support faces")
prop grid : phi.geom._grid.UniformGrid
Expand source code
@property
def grid(self) -> UniformGrid:
    return UniformGrid(spatial(self._sdf), self._bounds)
prop points : phiml.math._tensors.Tensor
Expand source code
@property
def points(self) -> Tensor:
    return UniformGrid(spatial(self._sdf), self._bounds).center
prop resolution : phiml.math._shape.Shape
Expand source code
@property
def resolution(self) -> Shape:
    return spatial(self._sdf)
prop shape : phiml.math._shape.Shape
Expand source code
@property
def shape(self) -> Shape:
    return non_spatial(self._sdf) & channel(vector=spatial(self._sdf))

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size : phiml.math._tensors.Tensor
Expand source code
@property
def size(self) -> Tensor:
    return self._bounds.size
prop values
Expand source code
@property
def values(self):
    """Signed distance grid."""
    return self._sdf

Signed distance grid.

prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> Tensor:
    return self._volume

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    if self._approximate_outside:
        location = self._bounds.push(location, outward=False)
    float_idx = (location - self._bounds.lower) / self.size * self.resolution
    sgn_dist = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
    if self._to_surface is not None:
        to_surf = math.grid_sample(self._to_surface, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
    else:
        sdf_grad = math.grid_sample(self._grad, float_idx - 1, math.extrapolation.ZERO_GRADIENT)
        sdf_grad = math.vec_normalize(sdf_grad)  # theoretically not necessary
        to_surf = sgn_dist * -sdf_grad
    surface_pos = location + to_surf
    if self._surf_normal is not None:
        normal = math.grid_sample(self._surf_normal, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
        int_index = clip(math.to_int32(float_idx), 0, wrap(spatial(self._surf_index), '(x,y,z)')-1)
        face_index = self._surf_index[int_index]
    else:
        surf_float_idx = (surface_pos - self._bounds.lower) / self.size * self.resolution
        normal = math.grid_sample(self._grad, surf_float_idx - 1, math.extrapolation.ZERO_GRADIENT)
        # normal = math.where(self._bounds.lies_inside(surface_pos), normal, sdf_grad)  # use current normal if surface point is outside SDF grid
        normal = math.vec_normalize(normal)
        face_index = None
    offset = normal.vector @ surface_pos.vector
    return sgn_dist, to_surf, normal, offset, face_index

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_fraction_inside(self,
other_geometry: Geometry,
balance: phiml.math._tensors.Tensor | numbers.Number = 0.5) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_fraction_inside(self, other_geometry: 'Geometry', balance: Union[Tensor, Number] = 0.5) -> Tensor:
    if other_geometry == self.grid and math.always_close(balance, .5):
        return self.approximate_occupancy()
    else:
        return Geometry.approximate_fraction_inside(self, other_geometry, balance)

Computes the approximate overlap between the geometry and a small other geometry. Returns 1.0 if other_geometry is fully enclosed in this geometry and 0.0 if there is no overlap. Close to the surface of this geometry, the fraction filled is differentiable w.r.t. the location and size of other_geometry.

To call this method on batches of geometries of same shape, pass a batched Geometry instance. The result tensor will match the batch shape of other_geometry.

The result may only be accurate in special cases. The given geometries may be approximated as spheres or boxes using bounding_radius() and bounding_half_extent().

The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using approximate_signed_distance().

Args

other_geometry
Geometry or geometry batch for which to compute the overlap with self.
balance
Mid-level between 0 and 1, default 0.5. This value is returned when exactly half of other_geometry lies inside self. 0.5 < balance <= 1 makes self seem larger while 0 <= balance < 0.5makes self seem smaller.

Returns

fraction of cell volume lying inside the geometry. float tensor of shape (other_geometry.batch_shape, 1).

def approximate_occupancy(self)
Expand source code
def approximate_occupancy(self):
    assert self._surf_normal is not None
    unit_corners = Cuboid(half_size=.5*self.dx).corners
    surf_dist = self._surf_normal.vector @ self._to_surface.vector
    corner_sdf = unit_corners.vector @ self._surf_normal.vector - surf_dist
    total_dist = math.sum(abs(corner_sdf), dual)
    neg_dist = math.sum(math.minimum(0, corner_sdf), dual)
    occ_near_surf = -neg_dist / total_dist
    occ_away = self._sdf <= 0
    return math.where(abs(self._sdf) < .5*math.vec_length(self.dx), occ_near_surf, occ_away)
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def approximate_signed_distance(self, location: Tensor) -> Tensor:
    float_idx = (location - self._bounds.lower) / self.size * self.resolution
    sdf_val = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
    if self._approximate_outside:
        within_bounds = self._bounds.lies_inside(location)
        dist_from_center = math.vec_length(location - self._center) - self._bounding_radius
        return math.where(within_bounds, sdf_val, dist_from_center)
    else:
        return sdf_val

Computes the approximate distance from location to the surface of the geometry. Locations outside return positive values, inside negative values and zero exactly at the boundary.

The exact distance metric used depends on the geometry. The approximation holds close to the surface and the distance grows to infinity as the location is moved infinitely far from the geometry. The distance metric is differentiable and its gradients are bounded at every point in space.

When dealing with unions or collections of geometries (instance dimensions), the shortest distance to any instance is returned. This also holds for negative distances.

Args

location
Tensor with one channel dim vector matching the geometry's vector dim.

Returns

Float Tensor

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    return self.shifted(center - self._center)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    return self._bounds.half_size  # this could be too small if the center is not in the middle of the bounds

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_radius(self) -> Tensor:
    return self._bounding_radius

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def downsample2x(self)
Expand source code
def downsample2x(self):
    s, g, t, n, i = [math.downsample2x(v) for v in (self._sdf, self._grad, self._to_surface, self._surf_normal, self._surf_index)]
    return SDFGrid(s, self._bounds, self._approximate_outside, g, t, n, i, self._center, self._volume, self._bounding_radius)
def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor
Expand source code
def lies_inside(self, location: Tensor) -> Tensor:
    float_idx = (location - self._bounds.lower) / self.size * self.resolution
    sdf_val = math.grid_sample(self._sdf, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
    if self._approximate_outside:
        within_bounds = self._bounds.lies_inside(location)
        return within_bounds & (sdf_val <= 0)
    else:
        return sdf_val <= 0

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle: Union[float, Tensor]) -> 'Geometry':
    raise NotImplementedError("SDF does not yet support rotation")

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape) ‑> phiml.math._tensors.Tensor
Expand source code
def sample_uniform(self, *shape: math.Shape) -> Tensor:
    raise NotImplementedError

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    off_center = self._center - self._bounds.center
    volume = self._volume * factor ** self.spatial_rank
    bounds = self._bounds.scaled(factor).shifted(off_center * (factor - 1)).corner_representation()
    return SDFGrid(self._sdf, bounds, self._approximate_outside, self._grad, self._to_surface, self._surf_normal, self._surf_index, self._center, volume, self._bounding_radius * factor)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

def shifted(self, delta: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def shifted(self, delta: Tensor) -> 'Geometry':
    return SDFGrid(self._sdf, self._bounds.shifted(delta), self._approximate_outside, self._grad, self._to_surface, self._surf_normal, self._surf_index, self._center + delta, self._volume, self._bounding_radius)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
def with_values(self, values: phiml.math._tensors.Tensor)
Expand source code
def with_values(self, values: Tensor):
    values = expand(values, spatial(self._sdf) - spatial(values))
    return SDFGrid(values, self._bounds, self._approximate_outside, self._grad, self._center, self._volume, self._bounding_radius)
class Sphere (center: phiml.math._tensors.Tensor = None,
radius: phiml.math._tensors.Tensor | float = None,
volume: phiml.math._tensors.Tensor | float = None,
radius_variable=True,
**center_: phiml.math._tensors.Tensor | float)
Expand source code
class Sphere(Geometry):
    """
    N-dimensional sphere.
    Defined through center position and radius.
    """

    def __init__(self,
                 center: Tensor = None,
                 radius: Union[float, Tensor] = None,
                 volume: Union[float, Tensor] = None,
                 radius_variable=True,
                 **center_: Union[float, Tensor]):
        """
        Args:
            center: Sphere center as `Tensor` with `vector` dimension.
                The spatial dimension order should be specified in the `vector` dimension via item names.
                Can be left empty to specify dimensions via kwargs.
            radius: Sphere radius as `float` or `Tensor`
            **center_: Specifies center when the `center` argument is not given. Center position by dimension, e.g. `x=0.5, y=0.2`.
        """
        if center is not None:
            assert isinstance(center, Tensor), f"center must be a Tensor but got {type(center).__name__}"
            assert 'vector' in center.shape, f"Sphere center must have a 'vector' dimension."
            assert center.shape.get_item_names('vector') is not None, f"Vector dimension must list spatial dimensions as item names. Use the syntax Sphere(x=x, y=y) to assign names."
            self._center = center
        else:
            self._center = wrap(tuple(center_.values()), math.channel(vector=tuple(center_.keys())))
        if radius is None:
            assert volume is not None, f"Either radius or volume must be specified but got neither."
            self._radius = Sphere.radius_from_volume(wrap(volume), self._center.vector.size)
        else:
            self._radius = wrap(radius)
        self._radius_variable = radius_variable
        assert 'vector' not in self._radius.shape, f"Sphere radius must not vary along vector but got {radius}"

    def __all_attrs__(self) -> tuple:
        return ('_center', '_radius')

    def __variable_attrs__(self) -> tuple:
        return ('_center', '_radius') if self._radius_variable else ('_center',)

    def __value_attrs__(self) -> tuple:
        return ()

    @property
    def shape(self):
        if self._center is None or self._radius is None:
            return None
        return self._center.shape & self._radius.shape

    @property
    def radius(self):
        return self._radius

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

    @property
    def volume(self) -> math.Tensor:
        return Sphere.volume_from_radius(self._radius, self.spatial_rank)

    @staticmethod
    def volume_from_radius(radius: Union[float, Tensor], spatial_rank: int):
        if spatial_rank == 1:
            return 2 * radius
        elif spatial_rank == 2:
            return PI * radius ** 2
        elif spatial_rank == 3:
            return 4/3 * PI * radius ** 3
        else:
            raise NotImplementedError(f"spatial_rank>3 not supported, got {spatial_rank}")
            # n = self.spatial_rank
            # return math.pi ** (n // 2) / math.faculty(math.ceil(n / 2)) * self._radius ** n

    @staticmethod
    def radius_from_volume(volume: Union[float, Tensor], spatial_rank: int):
        if spatial_rank == 1:
            return volume / 2
        elif spatial_rank == 2:
            return math.sqrt(volume / PI)
        elif spatial_rank == 3:
            return (.75 / PI * volume) ** (1/3)
        else:
            raise NotImplementedError(f"spatial_rank>3 not supported, got {spatial_rank}")

    def lies_inside(self, location):
        distance_squared = math.sum((location - self.center) ** 2, dim='vector')
        return math.any(distance_squared <= self.radius ** 2, self.shape.instance)  # union for instance dimensions

    def approximate_signed_distance(self, location: Union[Tensor, tuple]):
        """
        Computes the exact distance from location to the closest point on the sphere.
        Very close to the sphere center, the distance takes a constant value.

        Args:
          location: float tensor of shape (batch_size, ..., rank)

        Returns:
          float tensor of shape (*location.shape[:-1], 1).

        """
        distance = math.vec_length(location - self._center, eps=1e-3)
        return math.min(distance - self.radius, self.shape.instance)  # union for instance dimensions

    def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
        self_center = self.center
        self_radius = self.radius
        center_delta = location - self_center
        center_dist = math.vec_length(center_delta)
        sgn_dist = center_dist - self_radius
        if instance(self):
            self_center, self_radius, sgn_dist, center_delta, center_dist = math.at_min((self.center, self.radius, sgn_dist, center_delta, center_dist), key=abs(sgn_dist), dim=instance(self))
        normal = math.safe_div(center_delta, center_dist)
        default_normal = wrap([1] + [0] * (self.spatial_rank-1), self.shape['vector'])
        normal = math.where(center_dist == 0, default_normal, normal)
        surface_pos = self_center + self_radius * normal
        delta = surface_pos - location
        face_index = expand(0, non_channel(location))
        offset = normal.vector @ surface_pos.vector
        return sgn_dist, delta, normal, offset, face_index

    def sample_uniform(self, *shape: math.Shape):
        # --- Choose a distance from the center of the sphere, equally weighted by mass ---
        uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape)
        if self.spatial_rank == 1:
            r = self.radius * uniform
        else:
            r = self.radius * (uniform ** (1 / self.spatial_rank))
        # --- Uniformly sample a unit vector for direction over the surface of the sphere (Muller 1959, Marsaglia 1972) ---
        unit_vector = math.random_normal(self.shape.non_singleton.without('vector'), *shape, self.shape['vector'])
        unit_vector /= math.vec_length(unit_vector, vec_dim='vector')
        return self.center + r * unit_vector

    def bounding_radius(self):
        return self.radius

    def bounding_half_extent(self):
        return expand(self.radius, self._center.shape.only('vector'))

    def at(self, center: Tensor) -> 'Geometry':
        return Sphere(center, self._radius, radius_variable=self._radius_variable)

    def rotated(self, angle):
        return self

    def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
        return Sphere(self.center, self.radius * factor, radius_variable=self._radius_variable)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        return Sphere(self._center[_keep_vector(item)], self._radius[item], radius_variable=self._radius_variable)

    @property
    def faces(self) -> 'Geometry':
        raise NotImplementedError(f"Sphere.faces not implemented.")

    @property
    def face_centers(self) -> Tensor:
        return math.zeros(self.shape & dual(shell=0))

    @property
    def face_areas(self) -> Tensor:
        return math.zeros(self.face_shape)

    @property
    def face_normals(self) -> Tensor:
        return math.zeros(self.shape & dual(shell=0))

    @property
    def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
        return {}

    @property
    def face_shape(self) -> Shape:
        return self.shape.without('vector') & dual(shell=0)

    @property
    def corners(self) -> Tensor:
        return math.zeros(self.shape & dual(corners=0))

N-dimensional sphere. Defined through center position and radius.

Args

center
Sphere center as Tensor with vector dimension. The spatial dimension order should be specified in the vector dimension via item names. Can be left empty to specify dimensions via kwargs.
radius
Sphere radius as float or Tensor
**center_
Specifies center when the center argument is not given. Center position by dimension, e.g. x=0.5, y=0.2.

Ancestors

  • phi.geom._geom.Geometry

Static methods

def radius_from_volume(volume: phiml.math._tensors.Tensor | float, spatial_rank: int)
Expand source code
@staticmethod
def radius_from_volume(volume: Union[float, Tensor], spatial_rank: int):
    if spatial_rank == 1:
        return volume / 2
    elif spatial_rank == 2:
        return math.sqrt(volume / PI)
    elif spatial_rank == 3:
        return (.75 / PI * volume) ** (1/3)
    else:
        raise NotImplementedError(f"spatial_rank>3 not supported, got {spatial_rank}")
def volume_from_radius(radius: phiml.math._tensors.Tensor | float, spatial_rank: int)
Expand source code
@staticmethod
def volume_from_radius(radius: Union[float, Tensor], spatial_rank: int):
    if spatial_rank == 1:
        return 2 * radius
    elif spatial_rank == 2:
        return PI * radius ** 2
    elif spatial_rank == 3:
        return 4/3 * PI * radius ** 3
    else:
        raise NotImplementedError(f"spatial_rank>3 not supported, got {spatial_rank}")
        # n = self.spatial_rank
        # return math.pi ** (n // 2) / math.faculty(math.ceil(n / 2)) * self._radius ** n

Instance variables

prop boundary_elements : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_elements(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]
Expand source code
@property
def boundary_faces(self) -> Dict[str, Tuple[Dict[str, slice], Dict[str, slice]]]:
    return {}

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop center
Expand source code
@property
def center(self):
    return self._center

Center location in single channel dimension.

prop corners : phiml.math._tensors.Tensor
Expand source code
@property
def corners(self) -> Tensor:
    return math.zeros(self.shape & dual(corners=0))

Returns

Corner locations as phiml.math.Tensor. Corners belonging to one object or cell are listed along dual dimensions. If the object has no corners, a size-0 tensor with the correct vector and instance dims is returned.

prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    return math.zeros(self.face_shape)

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    return math.zeros(self.shape & dual(shell=0))

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    return math.zeros(self.shape & dual(shell=0))

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return self.shape.without('vector') & dual(shell=0)

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop facesGeometry
Expand source code
@property
def faces(self) -> 'Geometry':
    raise NotImplementedError(f"Sphere.faces not implemented.")
prop radius
Expand source code
@property
def radius(self):
    return self._radius
prop shape
Expand source code
@property
def shape(self):
    if self._center is None or self._radius is None:
        return None
    return self._center.shape & self._radius.shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop volume : phiml.math._tensors.Tensor
Expand source code
@property
def volume(self) -> math.Tensor:
    return Sphere.volume_from_radius(self._radius, self.spatial_rank)

phi.math.Tensor representing the volume of each element. The result retains batch, spatial and instance dimensions.

Methods

def approximate_closest_surface(self, location: phiml.math._tensors.Tensor) ‑> Tuple[phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor, phiml.math._tensors.Tensor]
Expand source code
def approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
    self_center = self.center
    self_radius = self.radius
    center_delta = location - self_center
    center_dist = math.vec_length(center_delta)
    sgn_dist = center_dist - self_radius
    if instance(self):
        self_center, self_radius, sgn_dist, center_delta, center_dist = math.at_min((self.center, self.radius, sgn_dist, center_delta, center_dist), key=abs(sgn_dist), dim=instance(self))
    normal = math.safe_div(center_delta, center_dist)
    default_normal = wrap([1] + [0] * (self.spatial_rank-1), self.shape['vector'])
    normal = math.where(center_dist == 0, default_normal, normal)
    surface_pos = self_center + self_radius * normal
    delta = surface_pos - location
    face_index = expand(0, non_channel(location))
    offset = normal.vector @ surface_pos.vector
    return sgn_dist, delta, normal, offset, face_index

Find the closest surface face of this geometry given a point that can be outside or inside the geometry.

Args

location
Tensor with a single channel dimension called vector. Can have arbitrary other dimensions.

Returns

signed_distance
Scalar signed distance from location to the closest point on the surface. Positive values indicate the point lies outside the geometry, negative values indicate the point lies inside the geometry.
delta
Vector-valued distance vector from location to the closest point on the surface.
normal
Closest surface normal vector.
offset
Min distance of a surface-tangential plane from 0 as a scalar.
face_index
(Optional) An index vector pointing at the closest face.
def approximate_signed_distance(self, location: phiml.math._tensors.Tensor | tuple)
Expand source code
def approximate_signed_distance(self, location: Union[Tensor, tuple]):
    """
    Computes the exact distance from location to the closest point on the sphere.
    Very close to the sphere center, the distance takes a constant value.

    Args:
      location: float tensor of shape (batch_size, ..., rank)

    Returns:
      float tensor of shape (*location.shape[:-1], 1).

    """
    distance = math.vec_length(location - self._center, eps=1e-3)
    return math.min(distance - self.radius, self.shape.instance)  # union for instance dimensions

Computes the exact distance from location to the closest point on the sphere. Very close to the sphere center, the distance takes a constant value.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

float tensor of shape (*location.shape[:-1], 1).

def at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._geom.Geometry
Expand source code
def at(self, center: Tensor) -> 'Geometry':
    return Sphere(center, self._radius, radius_variable=self._radius_variable)

Returns a copy of this Geometry with the center at center. This is equal to calling self @ center.

See Also: Geometry.shifted().

Args

center
New center as Tensor.

Returns

Geometry.

def bounding_half_extent(self)
Expand source code
def bounding_half_extent(self):
    return expand(self.radius, self._center.shape.only('vector'))

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def bounding_radius(self)
Expand source code
def bounding_radius(self):
    return self.radius

Returns the radius of a Sphere object that fully encloses this geometry. The sphere is centered at the center of this geometry.

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def lies_inside(self, location)
Expand source code
def lies_inside(self, location):
    distance_squared = math.sum((location - self.center) ** 2, dim='vector')
    return math.any(distance_squared <= self.radius ** 2, self.shape.instance)  # union for instance dimensions

Tests whether the given location lies inside or outside of the geometry. Locations on the surface count as inside.

When dealing with unions or collections of geometries (instance dimensions), a point lies inside the geometry if it lies inside any instance.

Args

location
float tensor of shape (batch_size, …, rank)

Returns

bool tensor of shape (*location.shape[:-1], 1).

def rotated(self, angle)
Expand source code
def rotated(self, angle):
    return self

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def sample_uniform(self, *shape: phiml.math._shape.Shape)
Expand source code
def sample_uniform(self, *shape: math.Shape):
    # --- Choose a distance from the center of the sphere, equally weighted by mass ---
    uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape)
    if self.spatial_rank == 1:
        r = self.radius * uniform
    else:
        r = self.radius * (uniform ** (1 / self.spatial_rank))
    # --- Uniformly sample a unit vector for direction over the surface of the sphere (Muller 1959, Marsaglia 1972) ---
    unit_vector = math.random_normal(self.shape.non_singleton.without('vector'), *shape, self.shape['vector'])
    unit_vector /= math.vec_length(unit_vector, vec_dim='vector')
    return self.center + r * unit_vector

Samples uniformly distributed random points inside this volume.

Args

*shape
How many points to sample per individual geometry.

Returns

Tensor containing all dimensions from Geometry.shape, shape as well as a channel dimension vector matching the dimensionality of this Geometry.

def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Geometry':
    return Sphere(self.center, self.radius * factor, radius_variable=self._radius_variable)

Scales each individual geometry by factor. The individual center points act as pivots for the operation.

Args

factor: Returns:

class UniformGrid (resolution: phiml.math._shape.Shape = None,
bounds: phi.geom._box.BaseBox = None,
**resolution_)
Expand source code
class UniformGrid(BaseBox):
    """
    An instance of UniformGrid represents all cells of a regular grid as a batch of boxes.
    """

    def __init__(self, resolution: Shape = None, bounds: BaseBox = None, **resolution_):
        assert resolution is None or resolution.is_uniform, f"spatial dimensions must form a uniform grid but got {resolution}"
        resolution = (resolution or EMPTY_SHAPE).spatial & spatial(**resolution_)
        bounds = _get_bounds(bounds, resolution)
        assert set(bounds.vector.item_names) == set(resolution.names)
        self._resolution = resolution.only(bounds.vector.item_names, reorder=True)  # reorder only
        self._bounds = bounds
        self._shape = self._resolution & bounds.shape.non_spatial
        staggered_shapes = [self._shape.spatial.with_dim_size(dim, self._shape.get_size(dim) + 1) for dim in self.vector.item_names]
        self._face_shape = shape_stack(dual(vector=self.vector.item_names), *staggered_shapes)

    @property
    def resolution(self):
        return self._resolution

    @property
    def bounds(self):
        return self._bounds

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

    @property
    def center(self):
        local_coords = math.meshgrid(**{dim.name: math.linspace(0.5 / dim.size, 1 - 0.5 / dim.size, dim) for dim in self.resolution})
        points = self.bounds.local_to_global(local_coords)
        return points

    def position_of(self, voxel_index: Tensor):
        voxel_index = rename_dims(voxel_index, channel, 'vector')
        return self._bounds.lower + (voxel_index+.5) / self.resolution * self._bounds.size

    def voxel_at(self, location: Tensor, clamp=True):
        float_idx = (location - self._bounds.lower) / self._bounds.size * self.resolution
        index = math.to_int32(float_idx)
        if clamp:
            index = math.clip(index, 0, wrap(self.resolution, channel('vector'))-1)
        return index

    @property
    def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
        return {}

    @property
    def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
        result = {}
        for dim in self.vector.item_names:
            result[dim+'-'] = {'~vector': dim, dim: slice(1)}
            result[dim+'+'] = {'~vector': dim, dim: slice(-1, None)}
        return result

    @property
    def face_centers(self) -> Tensor:
        centers = [self.stagger(dim, True, True).center for dim in self.vector.item_names]
        return stack(centers, dual(vector=self.vector.item_names))

    @property
    def faces(self) -> Geometry:
        slices = [self.stagger(d, True, True) for d in self.resolution.names]
        return stack(slices, dual(vector=self.vector.item_names))

    @property
    def face_normals(self) -> Tensor:
        normals = [vec(**{d: float(d == dim) for d in self.vector.item_names}) for dim in self.vector.item_names]
        return stack(normals, dual(vector=self.vector.item_names))

    @property
    def face_areas(self) -> Tensor:
        areas = [math.prod(self.dx.vector[[d for d in self.vector.item_names if d != dim]], 'vector') for dim in self.vector.item_names]
        return stack(areas, dual(vector=self.vector.item_names))

    @property
    def face_shape(self) -> Shape:
        return self._face_shape

    def interior(self) -> 'Geometry':
        raise GeometryException("Regular grid does not have an interior")

    @property
    def grid_size(self):
        return self._bounds.size

    @property
    def size(self):
        return self.bounds.size / math.wrap(self.resolution.sizes)

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

    @property
    def lower(self):
        return self.center - self.half_size

    @property
    def upper(self):
        return self.center + self.half_size

    @property
    def half_size(self):
        return self.bounds.size / self.resolution.sizes / 2

    @property
    def _rot_or_none(self) -> Optional[Tensor]:
        return None

    def with_scaled_resolution(self, scale: float):
        return UniformGrid(self._resolution * scale, self._bounds)

    def __getitem__(self, item):
        item = slicing_dict(self, item)
        resolution = self._resolution.after_gather(item)
        bounds = self._bounds[{d: s for d, s in item.items() if d != 'vector'}]
        if 'vector' in item:
            resolution = resolution.only(item['vector'], reorder=True)
            bounds = bounds.vector[item['vector']]
        bounds = bounds.vector[resolution.name_list]
        dx = self.size
        for dim, selection in item.items():
            if dim in resolution:
                if isinstance(selection, slice):
                    start = selection.start or 0
                    if start < 0:
                        start += self.resolution.get_size(dim)
                    stop = selection.stop or self.resolution.get_size(dim)
                    if stop < 0:
                        stop += self.resolution.get_size(dim)
                    assert selection.step is None or selection.step == 1
                else:  # int slices are not contained in resolution anymore
                    raise ValueError(f"Illegal selection: {item}")
                dim_mask = math.wrap(self.resolution.mask(dim))
                lower = bounds.lower + start * dim_mask * dx
                upper = bounds.upper + (stop - self.resolution.get_size(dim)) * dim_mask * dx
                bounds = Box(lower, upper)
        return UniformGrid(resolution, bounds)

    def __pack_dims__(self, dims: Tuple[str, ...], packed_dim: Shape, pos: Optional[int], **kwargs) -> 'Cuboid':
        return math.pack_dims(self.center_representation(size_variable=False), dims, packed_dim, pos, **kwargs)

    @staticmethod
    def __stack__(values: tuple, dim: Shape, **kwargs) -> 'Geometry':
        from ._geom_ops import GeometryStack
        set_op = kwargs.get('set_op')
        return GeometryStack(math.layout(values, dim), set_op)

    def __replace_dims__(self, dims: Tuple[str, ...], new_dims: Shape, **kwargs) -> 'UniformGrid':
        resolution = math.rename_dims(self._resolution, dims, new_dims).spatial
        bounds = math.rename_dims(self._bounds, dims, new_dims, **kwargs)[resolution.name_list]
        return UniformGrid(resolution, bounds)

    def list_cells(self, dim_name):
        center = math.pack_dims(self.center, self._shape.spatial.names, dim_name)
        return Cuboid(center, self.half_size, size_variable=False)

    def stagger(self, dim: str, lower: bool, upper: bool):
        dim_mask = np.array(self.resolution.mask(dim))
        unit = self.bounds.size / self.resolution * dim_mask
        bounds = Box(self.bounds.lower + unit * (-0.5 if lower else 0.5), self.bounds.upper + unit * (0.5 if upper else -0.5))
        ext_res = self.resolution.sizes + dim_mask * (int(lower) + int(upper) - 1)
        return UniformGrid(self.resolution.with_sizes(ext_res), bounds)

    def staggered_cells(self, boundaries: Extrapolation) -> Dict[str, 'UniformGrid']:
        grids = {}
        for dim in self.vector.item_names:
            grids[dim] = self.stagger(dim, *boundaries.valid_outer_faces(dim))
        return grids

    def padded(self, widths: dict):
        resolution, bounds = self.resolution, self.bounds
        for dim, (lower, upper) in widths.items():
            masked_dx = self.dx * math.dim_mask(self.resolution, dim)
            resolution = resolution.with_dim_size(dim, self.resolution.get_size(dim) + lower + upper)
            bounds = Box(bounds.lower - masked_dx * lower, bounds.upper + masked_dx * upper)
        return UniformGrid(resolution, bounds)

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

    def shifted(self, delta: Tensor, **delta_by_dim) -> BaseBox:
        # delta += math.padded_stack()
        if delta.shape.spatial_rank == 0:
            return UniformGrid(self.resolution, self.bounds.shifted(delta))
        else:
            center = self.center + delta
            return Cuboid(center, self.half_size, size_variable=False)

    def rotated(self, angle) -> Geometry:
        raise NotImplementedError("Grids cannot be rotated. Use center_representation() to convert it to Cuboids first.")

    def shallow_equals(self, other):
        return self == other

    def __repr__(self):
        return f"{self._resolution}, bounds={self._bounds}"

    def __variable_attrs__(self):
        return ()

    def __value_attrs__(self):
        return ()

    def __eq__(self, other):
        if not isinstance(other, UniformGrid):
            return False
        return self._resolution == other._resolution and self._bounds == other._bounds

    def __hash__(self):
        return hash(self._resolution) + hash(self._bounds)

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

    @property
    def _half_size(self):
        return self.half_size

    @property
    def normal(self) -> Tensor:
        raise GeometryException("UniformGrid does not have normals")

    def bounding_half_extent(self) -> Tensor:
        return self.half_size

An instance of UniformGrid represents all cells of a regular grid as a batch of boxes.

Ancestors

  • phi.geom._box.BaseBox
  • phi.geom._geom.Geometry

Instance variables

prop boundary_elements : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_elements(self) -> Dict[Any, Dict[str, slice]]:
    return {}

Slices on the primal dimensions to mark boundary elements. Grids and meshes have no boundary elements and return {}. Dynamic graphs can define boundary elements for obstacles and walls.

Returns

Map from name to slicing dict.

prop boundary_faces : Dict[Any, Dict[str, slice]]
Expand source code
@property
def boundary_faces(self) -> Dict[Any, Dict[str, slice]]:
    result = {}
    for dim in self.vector.item_names:
        result[dim+'-'] = {'~vector': dim, dim: slice(1)}
        result[dim+'+'] = {'~vector': dim, dim: slice(-1, None)}
    return result

Slices on the dual dimensions to mark boundary faces.

Regular grids use the keys (dim, is_upper) to identify boundaries. Unstructured meshes use string identifiers for the boundaries. Dynamic graphs return slices along the dual dimensions.

Returns

Map from name to slicing dict.

prop bounds
Expand source code
@property
def bounds(self):
    return self._bounds
prop center
Expand source code
@property
def center(self):
    local_coords = math.meshgrid(**{dim.name: math.linspace(0.5 / dim.size, 1 - 0.5 / dim.size, dim) for dim in self.resolution})
    points = self.bounds.local_to_global(local_coords)
    return points

Center location in single channel dimension.

prop dx
Expand source code
@property
def dx(self):
    return self.bounds.size / self.resolution
prop face_areas : phiml.math._tensors.Tensor
Expand source code
@property
def face_areas(self) -> Tensor:
    areas = [math.prod(self.dx.vector[[d for d in self.vector.item_names if d != dim]], 'vector') for dim in self.vector.item_names]
    return stack(areas, dual(vector=self.vector.item_names))

Area of face connecting a pair of cells. Shape (elements, ~). Returns 0 for unconnected cells.

prop face_centers : phiml.math._tensors.Tensor
Expand source code
@property
def face_centers(self) -> Tensor:
    centers = [self.stagger(dim, True, True).center for dim in self.vector.item_names]
    return stack(centers, dual(vector=self.vector.item_names))

Center of face connecting a pair of cells. Shape (elements, ~, vector). Here, ~ represents arbitrary internal dual dimensions, such as ~staggered_direction or ~elements. Returns 0-vectors for unconnected cells.

prop face_normals : phiml.math._tensors.Tensor
Expand source code
@property
def face_normals(self) -> Tensor:
    normals = [vec(**{d: float(d == dim) for d in self.vector.item_names}) for dim in self.vector.item_names]
    return stack(normals, dual(vector=self.vector.item_names))

Normal vectors of cell faces, including boundary faces. Shape (elements, ~, vector). For meshes, The vectors point out of the primal cells and into the dual cells.

Instance/spatial dimensions along which the normal does not vary may not be included in the result tensor's shape.

prop face_shape : phiml.math._shape.Shape
Expand source code
@property
def face_shape(self) -> Shape:
    return self._face_shape

Returns

Full Shape to identify each face of this Geometry, including instance/spatial dimensions for the elements and dual dimensions listing the faces per element. If this Geometry has no faces, returns an empty Shape.

prop faces : phi.geom._geom.Geometry
Expand source code
@property
def faces(self) -> Geometry:
    slices = [self.stagger(d, True, True) for d in self.resolution.names]
    return stack(slices, dual(vector=self.vector.item_names))
prop grid_size
Expand source code
@property
def grid_size(self):
    return self._bounds.size
prop half_size
Expand source code
@property
def half_size(self):
    return self.bounds.size / self.resolution.sizes / 2
prop lower
Expand source code
@property
def lower(self):
    return self.center - self.half_size
prop normal : phiml.math._tensors.Tensor
Expand source code
@property
def normal(self) -> Tensor:
    raise GeometryException("UniformGrid does not have normals")
prop resolution
Expand source code
@property
def resolution(self):
    return self._resolution
prop shape
Expand source code
@property
def shape(self):
    return self._shape

The shape of a Geometry consists of the following dimensions:

  • A single channel dimension called 'vector' specifying the physical space
  • Instance dimensions denote that this geometry consists of multiple copies in the same space
  • Spatial dimensions denote a crystal (repeating structure) of this geometric primitive in space
  • Batch dimensions indicate non-interacting versions of this geometry for parallelization only.
prop size
Expand source code
@property
def size(self):
    return self.bounds.size / math.wrap(self.resolution.sizes)
prop spatial_rank : int
Expand source code
@property
def spatial_rank(self) -> int:
    return self._resolution.spatial_rank

Number of spatial dimensions of the geometry, 1 = 1D, 2 = 2D, 3 = 3D, etc.

prop upper
Expand source code
@property
def upper(self):
    return self.center + self.half_size

Methods

def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor
Expand source code
def bounding_half_extent(self) -> Tensor:
    return self.half_size

The bounding half-extent sets a limit on the outer-most point for each coordinate axis. Each component is non-negative.

Let the bounding half-extent have value e in dimension d (extent[...,d] = e). Then, no point of the geometry lies further away from its center point than e along d (in both axis directions).

If this geometry consists of multiple parts listed along instance/spatial dims, these dims are retained, giving the bounds of each part. If these dims are not present on the result, all parts are assumed to have the same bounds.

def interior(self) ‑> phi.geom._geom.Geometry
Expand source code
def interior(self) -> 'Geometry':
    raise GeometryException("Regular grid does not have an interior")
def list_cells(self, dim_name)
Expand source code
def list_cells(self, dim_name):
    center = math.pack_dims(self.center, self._shape.spatial.names, dim_name)
    return Cuboid(center, self.half_size, size_variable=False)
def padded(self, widths: dict)
Expand source code
def padded(self, widths: dict):
    resolution, bounds = self.resolution, self.bounds
    for dim, (lower, upper) in widths.items():
        masked_dx = self.dx * math.dim_mask(self.resolution, dim)
        resolution = resolution.with_dim_size(dim, self.resolution.get_size(dim) + lower + upper)
        bounds = Box(bounds.lower - masked_dx * lower, bounds.upper + masked_dx * upper)
    return UniformGrid(resolution, bounds)
def position_of(self, voxel_index: phiml.math._tensors.Tensor)
Expand source code
def position_of(self, voxel_index: Tensor):
    voxel_index = rename_dims(voxel_index, channel, 'vector')
    return self._bounds.lower + (voxel_index+.5) / self.resolution * self._bounds.size
def rotated(self, angle) ‑> phi.geom._geom.Geometry
Expand source code
def rotated(self, angle) -> Geometry:
    raise NotImplementedError("Grids cannot be rotated. Use center_representation() to convert it to Cuboids first.")

Returns a rotated version of this geometry. The geometry is rotated about its center point.

Args

angle

Delta rotation. Either

  • Angle(s): scalar angle in 2d or euler angles along vector in 3D or higher.
  • Matrix: d⨯d rotation matrix

Returns

Rotated Geometry

def shallow_equals(self, other)
Expand source code
def shallow_equals(self, other):
    return self == other

Quick equality check. May return False even if other == self. However, if True is returned, the geometries are guaranteed to be equal.

The shallow_equals() check does not compare all tensor elements but merely checks whether the same tensors are referenced.

def shifted(self, delta: phiml.math._tensors.Tensor, **delta_by_dim) ‑> phi.geom._box.BaseBox
Expand source code
def shifted(self, delta: Tensor, **delta_by_dim) -> BaseBox:
    # delta += math.padded_stack()
    if delta.shape.spatial_rank == 0:
        return UniformGrid(self.resolution, self.bounds.shifted(delta))
    else:
        center = self.center + delta
        return Cuboid(center, self.half_size, size_variable=False)

Returns a translated version of this geometry.

See Also: Geometry.at().

Args

delta
direction vector
delta
Tensor:

Returns

Geometry
shifted geometry
def stagger(self, dim: str, lower: bool, upper: bool)
Expand source code
def stagger(self, dim: str, lower: bool, upper: bool):
    dim_mask = np.array(self.resolution.mask(dim))
    unit = self.bounds.size / self.resolution * dim_mask
    bounds = Box(self.bounds.lower + unit * (-0.5 if lower else 0.5), self.bounds.upper + unit * (0.5 if upper else -0.5))
    ext_res = self.resolution.sizes + dim_mask * (int(lower) + int(upper) - 1)
    return UniformGrid(self.resolution.with_sizes(ext_res), bounds)
def staggered_cells(self, boundaries: phiml.math.extrapolation.Extrapolation) ‑> Dict[str, phi.geom._grid.UniformGrid]
Expand source code
def staggered_cells(self, boundaries: Extrapolation) -> Dict[str, 'UniformGrid']:
    grids = {}
    for dim in self.vector.item_names:
        grids[dim] = self.stagger(dim, *boundaries.valid_outer_faces(dim))
    return grids
def voxel_at(self, location: phiml.math._tensors.Tensor, clamp=True)
Expand source code
def voxel_at(self, location: Tensor, clamp=True):
    float_idx = (location - self._bounds.lower) / self._bounds.size * self.resolution
    index = math.to_int32(float_idx)
    if clamp:
        index = math.clip(index, 0, wrap(self.resolution, channel('vector'))-1)
    return index
def with_scaled_resolution(self, scale: float)
Expand source code
def with_scaled_resolution(self, scale: float):
    return UniformGrid(self._resolution * scale, self._bounds)