Module phi.geom
Differentiable geometry package.
Classes:
See the phi.geom module documentation at https://tum-pbs.github.io/PhiFlow/Geometry.html
Functions
def Cuboid(center: phiml.math._tensors.Tensor = 0,
half_size: phiml.math._tensors.Tensor | float = None,
rotation: phiml.math._tensors.Tensor | None = None,
is_open: phiml.math._tensors.Tensor = False,
variable_attrs=('center', 'size', 'rot'),
**size: phiml.math._tensors.Tensor | float) ‑> phi.geom._box.Box- 
Expand source code
def Cuboid(center: Tensor = 0, half_size: Union[float, Tensor] = None, rotation: Optional[Tensor] = None, is_open: Tensor = wrap(False), variable_attrs=('center', 'size', 'rot'), **size: Union[float, Tensor]) -> Box: """ Args: center: Center position half_size: Half-size of the cuboid as vector or scalar rotation: Rotation angle(s) or rotation matrix. is_open: Specify which faces are open, i.e. have infinite extent. variable_attrs: Which properties of the box are treated as variable. **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." size = wrap(2 * half_size) else: size = wrap(tuple(size.values()), math.channel(vector=tuple(size))) center = wrap(center) if 'vector' not in center.shape or center.shape.get_item_names('vector') is None: center = math.expand(center, channel(size)) rotation = wrap(rotation) result = Box(center, size, rotation, is_open, variable_attrs) if half_size is not None: result.__dict__['half_size'] = half_size return resultArgs
center- Center position
 half_size- Half-size of the cuboid as vector or scalar
 rotation- Rotation angle(s) or rotation matrix.
 is_open- Specify which faces are open, i.e. have infinite extent.
 variable_attrs- Which properties of the box are treated as variable.
 **size- Alternative way of specifying the size. If used, 
half_sizemust not be specified. 
 def as_sdf(geo: phi.geom._geom.Geometry,
bounds=None,
rel_margin=None,
abs_margin=0.0,
separate: str | Sequence | set | phiml.math._shape.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: `SDF` representation of `geo`. """ 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: Box = 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): result = SDF(geo.sdf, bounds, geo.volume, geo.grad_fn) if 'out_shape' in geo.__dict__: result.__dict__['out_shape'] = geo.__dict__['out_shape'] return result 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) 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 result = SDF(sdf_closest_face, bounds, grad_fn=sdf_and_grad) result.__dict__['out_shape'] = math.EMPTY_SHAPE return result 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) 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) result = SDF(geo.approximate_signed_distance, bounds, geo.volume, sdf_and_grad) result.__dict__['out_shape'] = geo.shape.non_instance.without('vector') return resultRepresent existing geometry as a signed distance function.
Args
geoGeometryto represent as a signed distance function. Must implementGeometry.approximate_signed_distance().bounds- Bounds of the SDF. If 
Nonewill be determined from bounds ofgeoandrel_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% ofgeo'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 
geoand return individual SDFs. Once created, SDFs cannot be unstacked. 
Returns
SDFrepresentation ofgeo. 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: Union[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(center - extent, center + extent) return boxes.largest(boxes.shape.only(reduce)-'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ⁱ),
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 
Geometryordictmapping boundary name to correspondingGeometry. method- Meshing algorithm. Only 
quadis 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 
intand specifybounds. Or pass a sequence of floats for each dimension, specifying the vertex positions along each axis. This allows for variable cell stretching. 
Returns
 def clip_length(vec: phiml.math._tensors.Tensor,
min_len=0,
max_len=1,
vec_dim: str | Sequence | set | phiml.math._shape.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
vecTensormin_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 
infgradients for zero-length vectors. 
Returns
Tensorwith same shape asvec. 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 dims and their sizes must be equal, except for `dim`. Batch dims 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 dims 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 dims to all values, not just batch dimensions. This allows tensors with different dims to be concatenated. The resulting tensor will have all dims that are present in `values`. **kwargs: Additional keyword arguments required by specific implementations. Adding spatial dims to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions. Adding batch dims 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_TYPES): 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 # --- Filter 0-length values --- shapes = [shape(v) for v in values] def is_non_zero(s: Shape): if dim not in s: return True size = s.get_size(dim) if isinstance(size, int): return size > 0 return True filtered_values = [v for v, s in zip(values, shapes) if is_non_zero(s)] if not filtered_values: return values[0] values = filtered_values if len(values) == 1: return values[0] shapes = [s for s in shapes if is_non_zero(s)] # --- Add missing dimensions --- if expand_values: all_dims = merge_shapes(*shapes, allow_varying_sizes=True) all_dims = all_dims.with_dim_size(dim, 1, keep_labels=False) values = [expand(v, all_dims - s) for v, s in zip(values, shapes)] else: for v, s in zip(values, shapes): assert dim in s, f"concat dim '{dim}' must be present in the shapes of all values bot got value {type(v).__name__} with shape {s}" 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 dims but got {non_batch(values[0])} and {non_batch(v)}" all_batch_dims = merge_shapes(*[s.batch - dim for s in shapes]) 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, expand_values=expand_values, **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 dims ({dim}={len(unstacked)}). Please implement __concat__()", RuntimeWarning, stacklevel=2) dim = shapes[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.Shapableobjects, e.g.Tensor, along one dimension. All values must have the same spatial, instance and channel dims and their sizes must be equal, except fordim. Batch dims will be added as needed.Args
values- Tuple or list of 
phiml.math.magic.Shapable, such asphiml.math.Tensor dim- Concatenation dimension, must be present in all 
values. The size alongdimis determined fromvaluesand can be set to undefined (None). Alternatively, astrof the form't->name:t'can be specified, wheretis on ofb d i s cdenoting the dimension type. This first packs all dims 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 dims to all values, not just batch dimensions. This allows tensors with different dims to be concatenated. The resulting tensor will have all dims that are present invalues. **kwargs- Additional keyword arguments required by specific implementations.
Adding spatial dims to fields requires the 
bounds: Boxargument specifying the physical extent of the new dimensions. Adding batch dims must always work without keyword arguments. 
Returns
Concatenated
TensorExamples
>>> 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 'vector' in vec2.shape if 'vector' in vec1.shape: 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 'vector' in vec1.shape and 'vector' in vec2.shape, 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
vec1Tensorwith a single channel dimension called'vector'vec2Tensorwith 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: Union[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 
Tensorwithvectordimension. The spatial dimension order should be specified in thevectordimension via item names. Can be left empty to specify dimensions via kwargs. radius- Cylinder radius as 
floatorTensor. depth- Cylinder length as 
floatorTensor. 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 
centerargument 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, Box): 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
geometryare ignored.Args
geometryGeometryprojected_dims- Additional dimensions
 
Returns
Geometrywith spatial rankgeometry.spatial_rank + projected_dims.rank. def enclosing_grid(*geometries: phi.geom._geom.Geometry | phiml.math._tensors.Tensor,
voxel_count: int,
rel_margin=0.0,
abs_margin=0.0,
margin_cells=0) ‑> phi.geom._grid.UniformGrid- 
Expand source code
def enclosing_grid(*geometries: Union[Geometry, Tensor], voxel_count: int, rel_margin=0., abs_margin=0., margin_cells=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 `Tensor` of points 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. margin_cells: Number of cell layers to fit outside the bounding box around `geometries`. This is cumulative with `rel_margin` and `abs_margin`. Returns: `UniformGrid` """ bounds = stack([g.bounding_box() if isinstance(g, Geometry) else bounding_box(g) for g in geometries], batch('_geometries')) bounds = bounds.largest(shape).scaled(1+rel_margin) bounds = Box(bounds.lower - abs_margin, bounds.upper + abs_margin) if not margin_cells: voxel_vol = bounds.volume / voxel_count voxel_size = voxel_vol ** (1/bounds.spatial_rank) resolution = math.to_int32(math.round(safe_div(bounds.size, voxel_size))) resolution = spatial(**resolution.vector) else: inner_res, outer_res = solve_resolution_with_margin_cells(*bounds.size.vector, voxel_count, margin_cells) dx = safe_div(bounds.size, inner_res) bounds = Box(bounds.lower - dx*margin_cells, bounds.upper + dx*margin_cells) resolution = spatial(**{d: r for d, r in zip(bounds.size.vector.labels, outer_res)}) return UniformGrid(resolution, bounds)Constructs a
UniformGridwhich fully encloses thegeometries. The grid voxels are chosen to have approximately the same size along each axis.Args
*geometriesGeometryobjectsTensorof points 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.
 margin_cells- Number of cell layers to fit outside the bounding box around 
geometries. This is cumulative withrel_marginandabs_margin. 
Returns
 def farthest_points(points: phiml.math._tensors.Tensor,
list_dim: phiml.math._shape.Shape,
must_contain: phiml.math._tensors.Tensor = None)- 
Expand source code
def farthest_points(points: Tensor, list_dim: Shape, must_contain: Tensor = None): """ Perform farthest point sampling (FPS) on a set of 3D points. Parameters: points (numpy.ndarray): (N, 3) array of 3D points. list_dim: Number of points to sample and dimension along which to list the sampled points. must_contain: (Optional) Indices of points that must be contained in the sample. Duplicate indices will only be sampled once. Returns: numpy.ndarray: (M, 3) array of sampled points. """ points_np = points.numpy([..., 'vector']) sampled_indices = np.zeros(list_dim.volume, dtype=int) if must_contain is not None: must_contain_np = must_contain.numpy([shape]) must_contain_np = np.unique(must_contain_np) sampled_indices[:must_contain_np.size] = must_contain_np distances = np.min(np.linalg.norm(points_np - points_np[must_contain_np, None, :], axis=-1), 0) i0 = must_contain_np.size else: distances = np.full(points_np.shape[0], np.inf) # Initialize distances as infinity sampled_indices[0] = np.random.randint(points_np.shape[0]) # Start with a random point i0 = 1 for i in range(i0, list_dim.volume): last_selected = points_np[sampled_indices[i - 1]] # Compute distances from the last selected point to all points distances = np.minimum(distances, np.linalg.norm(points_np - last_selected, axis=1)) # Update the minimum distance to the set of selected points sampled_indices[i] = np.argmax(distances) # Select the farthest point sampled_indices = expand(wrap(sampled_indices, list_dim), channel(index=(points.shape-'vector').name_list)) return sampled_indices, points[sampled_indices]Perform farthest point sampling (FPS) on a set of 3D points.
Parameters
points (numpy.ndarray): (N, 3) array of 3D points. list_dim: Number of points to sample and dimension along which to list the sampled points. must_contain: (Optional) Indices of points that must be contained in the sample. Duplicate indices will only be sampled once.
Returns
numpy.ndarray- (M, 3) array of sampled points.
 
 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, # remaining for legacy reasons. Now evaluated on demand. 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. 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 return Graph(nodes, edges, boundary) 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-dimensionalSphereinn+1dimensions.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_dimas keyword arguments. 
Returns
 def intersection(*geometries: phi.geom._geom.Geometry, dim=(intersectionⁱ)) ‑> 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` """ assert dim.rank == 1 and dim.instance, f"intersection dimension must be a single instance dimension but got {dim}" geometries = geometries[0] if len(geometries) == 1 and isinstance(geometries[0], (tuple, list)) else geometries if len(geometries) == 0: warnings.warn("Empty intersection cannot infer dimensionality. Returning 0-dimensional empty.", RuntimeWarning, stacklevel=2) return NoGeometry(channel(vector=0)) elif len(geometries) == 1: return geometries[0] return Intersection(layout(geometries, 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 def join_meshes(meshes: Sequence[phi.geom._mesh.Mesh]) ‑> phi.geom._mesh.Mesh- 
Expand source code
def join_meshes(meshes: Sequence[Mesh]) -> Mesh: """ Creates a mesh that contains the vertices and elements of all `meshes`. Vertex and elements are concatenated, thereby increasing the indices as needed. Args: meshes: Collection of `Mesh` instances. Returns: `Mesh` """ same_vertices = all(mesh.vertices is meshes[0].vertices for mesh in meshes) e_name = instance(meshes[0]).name v_name = dual(meshes[0].elements).name v_offset = 0 e_offset = 0 vertices = [] indices = [] compact_sizes = [dual(m.elements._indices).size if math.get_format(m.elements) == 'compact-cols' else -1 for m in meshes] compatible_compact = compact_sizes[0] > 0 and all(s == compact_sizes[0] for s in compact_sizes) for mesh in meshes: vertices.append(mesh.vertices) if compatible_compact: indices.append(mesh.elements._indices + v_offset) else: idx = math.stored_indices(mesh.elements)# + (e_offset, v_offset) # ToDo order idx += vec('index', **{e_name: e_offset, v_name: v_offset})[channel(idx).item_names[0]] indices.append(idx) if not same_vertices: v_offset += instance(mesh.vertices).size e_offset += instance(mesh).size vertices = icat(vertices) indices = icat(indices) e_dim = instance(meshes[0]).with_size(e_offset) v_dim = instance(vertices).as_dual() if compatible_compact: elements = CompactSparseTensor(indices, wrap(True), v_dim, True, -1) else: elements = sparse_tensor(indices, True, e_dim + v_dim) return replace(meshes[0], vertices=vertices, elements=elements) def length(obj: phi.geom._geom.Geometry | phiml.math._tensors.Tensor, epsilon=None) ‑> phiml.math._tensors.Tensor- 
Expand source code
def length(obj: Union[Geometry, Tensor], epsilon=None) -> Tensor: """ Returns the length of a vector `Tensor` or geometric object with a length-like property. Args: obj: `Tensor` with 'vector' dim or `Geometry` with a length-like property. 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.norm(obj, 'vector', epsilon) elif isinstance(obj, Cylinder): return obj.depth raise ValueError(obj)Returns the length of a vector
Tensoror geometric object with a length-like property.Args
objTensorwith 'vector' dim orGeometrywith a length-like property.epsilon- Minimum valid vector length. Use to avoid 
infgradients for zero-length vectors. Lengths shorter thanepsare 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_indexTrace a line until it hits the surface of
geo. The surface can be hit either from the outside or the inside.Args
geoGeometrythat implementsapproximate_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 
1if the signed distance values ofgeoare 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ⁱ),
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
.mshfile.This requires the package
meshioto be installed.Args
file- Path to 
.su2file. 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
 def load_stl(file: str, face_dim=(facesⁱ)) ‑> 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 trimesh mesh = trimesh.load(file) if isinstance(mesh, trimesh.Scene): # STL contains multiple parts -> merge vertices = [] v_count = 0 faces = [] for geometry in mesh.geometry.values(): assert isinstance(geometry, trimesh.Trimesh) vertices.append(geometry.vertices) faces.append(geometry.faces + v_count) v_count += geometry.vertices.shape[0] vertices = np.concatenate(vertices) faces = np.concatenate(faces) else: assert isinstance(mesh, trimesh.Trimesh), f"Unexpected content of STL: {mesh}" vertices, faces = mesh.vertices, mesh.faces return mesh_from_numpy(vertices, faces, None, 2, None, face_dim) # import stl # this only loads the first part of multi-part STL files # model = stl.mesh.Mesh.from_file(file, calculate_normals=False, ) # 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 def load_su2(file_or_mesh: str, cell_dim=(cellsⁱ), 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
.su2file.This requires the package
ezmeshto be installed.Args
file_or_mesh- Path to 
.su2file or ezmeshMeshinstance. cell_dim- Dimension along which to list the cells. This should be an instance dimension.
 face_format- Sparse storage format for cell connectivity.
 
Returns
 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: Union[Geometry, Tensor], elements: Tensor, boundaries: Union[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)} as dual dim 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
verticesTensorwith one instance and one channel dimensionvector.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 
strto assign one name to all boundary faces. For multiple boundaries, pass adictmapping group namesstrto lists of faces, defined by their vertices. The last entry can beNoneto group all boundary faces not explicitly listed before. Theboundariesdictmaps 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
 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ⁱ),
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: Union[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: Either list of elements where 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. Or sparse connectivity matrix of shape (num_elements, num_vertices) with boolean values. 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` """ points = np.asarray(points) xyz = tuple(axes[:points.shape[-1]]) vertices = wrap(points, instance('vertices'), channel(vector=xyz)) if issparse(polygons): cell_dim = cell_dim.with_size(polygons.shape[0]) assert points.shape[0] == polygons.shape[-1], f"Number of points {points.shape[0]} must match number of vertices in polygons {polygons.shape[-1]} but got polygons={polygons.shape}" elements = wrap(polygons, cell_dim, instance(vertices).as_dual()) else: cell_dim = cell_dim.with_size(len(polygons)) if len(polygons) == 0: elements = math.ones(cell_dim, instance(vertices).as_dual(), dtype=bool) return mesh(vertices, elements, boundaries, element_rank, periodic, face_format) 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- Either list of elements where 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. Or sparse connectivity matrix of shape (num_elements, num_vertices) with boolean values. boundaries- An unstructured mesh can have multiple boundaries, each defined by a name 
strand a list of faces, defined by their vertices. Theboundariesdictmaps 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
 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
slopelisting all dimensions ofspacein 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
Tensoralong the 'vector' dim.Args
objTensorwith '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 
NaNas 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 
epsiloninstead of a unit vector. 
Returns
Tensorof the same shape asobj. def numpy_sdf(sdf: Callable, bounds: phi.geom._box.Box) ‑> phi.geom._sdf.SDF- 
Expand source code
def numpy_sdf(sdf: Callable, bounds: Box) -> 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. 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']) result = SDF(native_sdf_function, bounds) result.__dict__['out_shape'] = math.EMPTY_SHAPE return result def pack_dims(value,
dims: str | Sequence | set | phiml.math._shape.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 dims into a single dimension by concatenating the elements. Elements along the new dims 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 dims to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions. Adding batch dims 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 if DEBUG_CHECKS: 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, 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 dims ({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 dims into a single dimension by concatenating the elements. Elements along the new dims are laid out according to the order of
dims. If the order ofdimsdiffers from the current dimension order, the tensor is transposed accordingly. This function replaces the traditionalreshapefor these cases.The type of the new dimension will be equal to the types of
dims. Ifdimshave varying types, the new dimension will be a batch dimension.If none of
dimsexist onvalue,packed_dimwill be added only if it is given with a definite size andvalueis not a primitive type.See Also:
unpack_dim()Args
valuephiml.math.magic.Shapable, such asphiml.math.Tensor.dims- Dimensions to be compressed in the specified order.
 packed_dim- Single-dimension 
Shape. pos- Index of new dimension. 
Nonefor automatic,-1for last,0for first. **kwargs- Additional keyword arguments required by specific implementations.
Adding spatial dims to fields requires the 
bounds: Boxargument specifying the physical extent of the new dimensions. Adding batch dims 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: Union[float, Tensor, None], invert=False, pivot: Union[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(rot, Tensor) and rot.dtype.kind == object: return math.map(rotate, obj, rot, invert=invert, pivot=pivot, dims=rot.shape) if isinstance(obj, Geometry): if pivot is None: pivot = obj.bounding_box().center from ._mesh import Mesh if isinstance(obj, Mesh): vertices = rotate(obj.vertex_positions, rot, invert=invert, pivot=pivot) return obj.at(vertices) center = pivot + rotate(obj.center - pivot, rot, invert=invert) if invert: raise NotImplementedError 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
Geometryabout thepivot.Args
obj- n-dimensional vector 
TensororGeometry. rot- Euler angle(s) or rotation matrix.
Noneis 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 ifpivotis not given as aTensor. 
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_to_unit=False) ‑> phiml.math._tensors.Tensor | None- 
Expand source code
def rotation_matrix(x: Union[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 
anglechannel 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_dimin primal and dual form as well as all non-channel dimensions ofx. 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: Union[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 NotImplementedErrorComputes a rotation matrix that rotates by
anglearoundaxis.Args
axis- 3D vector. 
Tensorwith channel dim called 'vector'. angle- Rotation angle.
 is_axis_normalized- Whether 
axishas length 1. epsilon- Minimum axis length. For shorter axes, the unit matrix is returned.
 
Returns
Rotation matrix as
Tensorwith '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 NotImplementedErrorComputes a rotation matrix A, such that
target_dir = A @ source_dirArgs
source_dir- Two or three-dimensional vector. 
Tensorwith channel dim called 'vector'. target_dir- Two or three-dimensional vector. 
Tensorwith channel dim called 'vector'. 
Returns
Rotation matrix as
Tensorwith '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 valuesCalls
f, passing either theelementsdirectly or the relevant sample points as aTensor, depending on the signature off.Args
f- Function taking a 
Geometryor locationTensor´ and returning aTensor`. AGeometrywill be passed if the first argument offis calledgeometryorgeoor ends with_geo. elementsGeometryon which to samplef.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.Box | 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: Union[Box, 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: Box = 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 = _create_sdf_grid(sdf, bounds, approximate_outside, center, volume, bounding_radius, delta, normal, idx) else: sdf = geometry.approximate_signed_distance(points) approximate = _create_sdf_grid(sdf, bounds, approximate_outside, center, volume, 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 _create_sdf_grid(sdf, bounds, approximate_outside, center, volume, bounding_radius)Build a grid of signed distance values for a given
Geometryobject.Args
geometryGeometryto 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. IfNone(default), SDF values are queried fromgeometry.'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: Union[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) def squared_length(obj: phi.geom._geom.Geometry | phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor- 
Expand source code
def squared_length(obj: Union[Geometry, Tensor]) -> Tensor: """ Returns the squared length of a vector `Tensor` or geometric object with a length-like property. Args: obj: `Tensor` with 'vector' dim or `Geometry` with a length-like property. Returns: Squared length as `Tensor` """ if isinstance(obj, Tensor): assert 'vector' in obj.shape, f"squared_length() requires 'vector' dim but got {type(obj)} with shape {shape(obj)}." return math.squared_norm(obj, 'vector') elif isinstance(obj, Cylinder): return obj.depth ** 2 raise ValueError(obj)Returns the squared length of a vector
Tensoror geometric object with a length-like property.Args
objTensorwith 'vector' dim orGeometrywith a length-like property.
Returns
Squared length as
Tensor 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 dims 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 labels along `dim`. dim: `Shape` with a least one dimension. None of these dims 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 dims to all values, not just batch dimensions. This allows tensors with different dims to be stacked. The resulting tensor will have all dims 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 dims to fields requires the `bounds: Box` argument specifying the physical extent of the new dimensions. Adding batch dims 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_TYPES): 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) 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 dims --- 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_labels = tuple([k.name if isinstance(k, SHAPE_TYPES) else k for k in values.keys()]) assert all(isinstance(k, str) for k in dim_labels), f"dict keys must be of type str but got {dim_labels}" values = tuple(values.values()) dim = dim.with_size(dim_labels) # --- First try __stack__ --- for v in values: if hasattr(v, '__stack__'): result = v.__stack__(values, dim, **kwargs) if result is not NotImplemented: if DEBUG_CHECKS: assert isinstance(result, SHAPE_TYPES) if isinstance(v, SHAPE_TYPES) 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 try: return dc_stack(values, dim, expand_values=expand_values, simplify=simplify, layout_non_matching=layout_non_matching, **kwargs) except NotCompatible as err: if layout_non_matching: from ._tensors import layout return layout(values, dim) raise err 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 any(isinstance(v, (tuple, list, dict)) for v in values_): from ._tensors import wrap, layout if all(np.asarray(v).dtype != object for v in values_): tensors = [wrap(v) for v in values_] return stack(tensors, dim) else: assert len(dim) == 1, f"Cannot stack values with nested tuples, lists or dicts along multiple dimensions {dim}" return layout(values_, dim) 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.labels[0] is None else dim.labels[0][i]), **kwargs) for i, v in enumerate(values)]) if len(expanded_values) > 8: warnings.warn(f"stack() default implementation is slow on large dims ({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
valuesalong the new dimensiondim. All values must have the same spatial, instance and channel dimensions. If the dimension sizes vary, the resulting tensor will be non-uniform. Batch dims 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 asphiml.math.TensorIf adict, keys must be of typestrand are used as labels alongdim. dimShapewith a least one dimension. None of these dims can be present with any of thevalues. Ifdimis a single-dimension shape, its size is determined fromlen(values)and can be left undefined (None). Ifdimis a multi-dimension shape, its volume must be equal tolen(values).expand_values- If 
True, will first add missing dims to all values, not just batch dimensions. This allows tensors with different dims to be stacked. The resulting tensor will have all dims that are present invalues. IfFalse, this may return a non-numeric object instead. simplify- If 
Trueand 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 dims to fields requires the 
bounds: Boxargument specifying the physical extent of the new dimensions. Adding batch dims must always work without keyword arguments. 
Returns
Tensorcontainingvaluesstacked alongdim.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" # --- Check special cases --- if method == 'auto' and isinstance(geo, Box): 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) if rel_dx is None and abs_dx is None: rel_dx = 1 / 128 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) if method == 'auto' and isinstance(geo, Sphere): pass # ToDo analytic solution elif method == 'auto' and isinstance(geo, BSplineSheet): vol_per_cp = geo.bounding_box().volume / math.prod(geo.res, 'vector').sum avg_cp_dist = vol_per_cp ** (1/geo.spatial_rank) res = max(round(avg_cp_dist / dx), 3) return geo.build_mesh(res, res) # --- 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
Meshfrom a Geometry.Args
geoGeometryto convert. Must implementapproximate_signed_distance.rel_dx- Relative mesh resolution as fraction of bounding box size.
 abs_dx- Absolute mesh resolution. If both 
rel_dxandabs_dxare provided, the lower value is used. method- 'auto' to select based on the type of 
geo. 'lewiner' or 'lorensen' for marching cubes. 
Returns
Meshif there is any geometry def union(*geometries, dim=(unionⁱ))- 
Expand source code
def union(*geometries, dim=instance('union')): """ 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` """ assert dim.rank == 1 and dim.instance, f"union dimension must be a single instance dimension but got {dim}" geometries = geometries[0] if len(geometries) == 1 and isinstance(geometries[0], (tuple, list)) else geometries if len(geometries) == 0: warnings.warn("Empty union cannot infer dimensionality. Returning 0-dimensional empty.", RuntimeWarning, stacklevel=2) return NoGeometry(channel(vector=0)) elif len(geometries) == 1: return geometries[0] elif all(type(g) == type(geometries[0]) and isinstance(g, PhiTreeNode) for g in geometries): return stack(tuple(geometries), dim, simplify=True) else: return GeometryStack(layout(geometries, 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 Box (center: phiml.math._tensors.Tensor,
size: phiml.math._tensors.Tensor,
rot: phiml.math._tensors.Tensor,
is_open: phiml.math._tensors.Tensor,
variable_attrs: Tuple[str, ...] = ('center', 'size', 'rot'))- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) class Box(Geometry, 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)`. """ center: Tensor size: Tensor rot: Tensor # can be Layout(None) for no rotation is_open: Tensor # Infinite extent per face, (~side, vector) or fewer variable_attrs: Tuple[str, ...] = ('center', 'size', 'rot') def __post_init__(self): assert isinstance(self.center, Tensor) and 'vector' in channel(self.center) assert isinstance(self.size, Tensor) assert isinstance(self.rot, Tensor) @property def half_size(self): return self.size * 0.5 @cached_property def lower(self): return math.where(self.is_open.side['lower'], -math.INF, self.center - self.half_size) @cached_property def upper(self): return math.where(self.is_open.side['upper'], math.INF, self.center + self.half_size) @cached_property def is_finite(self): return not self.is_open.any def __repr__(self): if self.rot is not None: return f"Cuboid(center={self.center}, size={self.size})" if self.center is None or self.size 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}]' @cached_property def shape(self): return self.center.shape & self.size.shape & (shape(self.rot) - '~vector') @property def pos(self): return self.center @property def volume(self) -> Tensor: return math.prod(self.size, 'vector') @property def is_axis_aligned(self): return self.rot == None @property def rotation_matrix(self) -> Tensor: return rotation_matrix(self.rot, self.shape['vector'], none_to_unit=True) def at(self, center: Tensor) -> 'Box': return replace(self, center=center) def rotated(self, angle) -> 'Box': rot = wrap(angle) if self.is_axis_aligned.all else self.rotation_matrix @ rotation_matrix(angle) return replace(self, rot=rot) def scaled(self, factor: Union[float, Tensor]) -> 'Box': return replace(self, size=self.size * factor) 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, 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) + origin_loc def __mul__(self, other): if not isinstance(other, Box): return NotImplemented assert self.is_axis_aligned.all and other.is_axis_aligned.all, f"Box * Box only supported for axis-aligned boxes (rot=None)." pos = concat([self.center, other.center], 'vector') size = concat([self.size, other.size], 'vector') return replace(self, center=pos, size=size) def bounding_half_extent(self) -> Tensor: if self.rot 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: union_dims = instance(self) - instance(location) location = self.global_to_local(location, scale=False, origin='center') # scale can only be performed for finite sizes if not self.is_open.any: bool_inside = abs(location) <= self.half_size else: above_lower = (location > self.lower) | self.is_open.side.dual['lower'] below_upper = (location < self.upper) | self.is_open.side.dual['upper'] bool_inside = above_lower & below_upper bool_inside = math.all(bool_inside, 'vector') bool_inside = math.any(bool_inside, union_dims) return bool_inside def largest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.largest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.min(self.lower, dim), math.max(self.upper, dim)) def smallest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.smallest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.max(self.lower, dim), math.min(self.upper, dim)) def without(self, dims: Tuple[str, ...]): assert self.is_axis_aligned.all, f"Box.without() is only supported for axis-aligned boxes (rot=None)" remaining = list(self.shape.get_item_names('vector')) for dim in dims: if dim in remaining: remaining.remove(dim) return self.vector[remaining] def bounding_radius(self): return vec_length(self.half_size) 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 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). """ assert not self.is_open.any, f"approximate_signed_distance not supported for open boxes" # 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 approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: assert not self.is_open.any, f"approximate_closest_surface not supported for open boxes" 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) 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 = vec_length(delta) * math.sign(max_sgn_dist) return sgn_surf_dist, delta, normal, offset, face_index def sample_uniform(self, *shape: Shape) -> Tensor: assert not self.is_open.any, f"sample_uniform not supported for open boxes" uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector']) return self.lower + uniform * self.size def contains(self, other: 'Box'): """ Tests if the other box lies fully inside this box. """ assert not self.is_open.any and not other.is_open.any, f"contains not supported for open boxes" assert self.is_axis_aligned.all and other.rot is None, f"contains() is only supported for axis-aligned boxes (rot=None)." return np.all(other.lower >= self.lower) and np.all(other.upper <= self.upper) 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=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 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 @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, 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 = to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict))) vectors = rotate(unit_vectors, self.rot) 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 @property def is_size_variable(self): warnings.warn("Box.is_size_variable is deprecated. Check Box.variable_attrs instead.", DeprecationWarning, stacklevel=2) return 'size' in self.variable_attrs def corner_representation(self) -> 'Box': assert self.is_axis_aligned.all, f"corner_representation does not support rotations" return self box = corner_representation def center_representation(self) -> 'Cuboid': return self cuboid = center_representationSimple 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 askwargs.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)`.Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. var center : phiml.math._tensors.Tensorprop 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_cornerReturns
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')) # ~vectorArea 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_sizeCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~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 = to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict))) vectors = rotate(unit_vectors, self.rot) 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) prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': return Cuboid(self.face_centers, self.half_size, self.rot, size_variable=False) prop half_size- 
Expand source code
@property def half_size(self): return self.size * 0.5 prop is_axis_aligned- 
Expand source code
@property def is_axis_aligned(self): return self.rot == None var is_finite- 
Expand source code
@cached_property def is_finite(self): return not self.is_open.any var is_open : phiml.math._tensors.Tensorprop is_size_variable- 
Expand source code
@property def is_size_variable(self): warnings.warn("Box.is_size_variable is deprecated. Check Box.variable_attrs instead.", DeprecationWarning, stacklevel=2) return 'size' in self.variable_attrs var lower- 
Expand source code
@cached_property def lower(self): return math.where(self.is_open.side['lower'], -math.INF, self.center - self.half_size) prop pos- 
Expand source code
@property def pos(self): return self.center var rot : phiml.math._tensors.Tensorprop rotation_matrix : phiml.math._tensors.Tensor- 
Expand source code
@property def rotation_matrix(self) -> Tensor: return rotation_matrix(self.rot, self.shape['vector'], none_to_unit=True) var shape- 
Expand source code
@cached_property def shape(self): return self.center.shape & self.size.shape & (shape(self.rot) - '~vector') var size : phiml.math._tensors.Tensorvar upper- 
Expand source code
@cached_property def upper(self): return math.where(self.is_open.side['upper'], math.INF, self.center + self.half_size) var variable_attrs : Tuple[str, ...]prop volume : phiml.math._tensors.Tensor- 
Expand source code
@property def volume(self) -> Tensor: return math.prod(self.size, 'vector')phi.math.Tensorrepresenting 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]: assert not self.is_open.any, f"approximate_closest_surface not supported for open boxes" 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) 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 = vec_length(delta) * math.sign(max_sgn_dist) return sgn_surf_dist, delta, normal, offset, face_indexFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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). """ assert not self.is_open.any, f"approximate_signed_distance not supported for open boxes" # 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 distanceComputes the signed L-infinity norm (manhattan distance) from the location to the nearest side of the box. For an outside location
lwith the closest surface points, the distance ismax(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 at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._box.Box- 
Expand source code
def at(self, center: Tensor) -> 'Box': return replace(self, center=center)Returns a copy of this
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor- 
Expand source code
def bounding_half_extent(self) -> Tensor: if self.rot 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_sizeThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 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.is_axis_aligned.all, f"corner_representation does not support rotations" return self def center_representation(self) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self) -> 'Cuboid': return self def contains(self, other: Box)- 
Expand source code
def contains(self, other: 'Box'): """ Tests if the other box lies fully inside this box. """ assert not self.is_open.any and not other.is_open.any, f"contains not supported for open boxes" assert self.is_axis_aligned.all and other.rot is None, f"contains() is only supported for axis-aligned boxes (rot=None)." 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.is_axis_aligned.all, f"corner_representation does not support rotations" return self def cuboid(self) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self) -> 'Cuboid': return self 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, invert=True) if scale: pos /= (self.half_size if origin == 'center' else self.size) return posTransform 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'ororigin='center', respectively. origin- 'lower' or 'center'
 
Returns
Box-space coordinate
Tensor def largest(self, dim: str | Sequence | set | phiml.math._shape.Shape | Callable | None) ‑> phi.geom._box.Box- 
Expand source code
def largest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.largest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.min(self.lower, dim), math.max(self.upper, dim)) def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor- 
Expand source code
def lies_inside(self, location: Tensor) -> Tensor: union_dims = instance(self) - instance(location) location = self.global_to_local(location, scale=False, origin='center') # scale can only be performed for finite sizes if not self.is_open.any: bool_inside = abs(location) <= self.half_size else: above_lower = (location > self.lower) | self.is_open.side.dual['lower'] below_upper = (location < self.upper) | self.is_open.side.dual['upper'] bool_inside = above_lower & below_upper bool_inside = math.all(bool_inside, 'vector') bool_inside = math.any(bool_inside, union_dims) return bool_insideTests 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) + 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=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) * shiftShifts 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) ‑> phi.geom._box.Box- 
Expand source code
def rotated(self, angle) -> 'Box': rot = wrap(angle) if self.is_axis_aligned.all else self.rotation_matrix @ rotation_matrix(angle) return replace(self, rot=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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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: assert not self.is_open.any, f"sample_uniform not supported for open boxes" uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector']) return self.lower + uniform * self.sizeSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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.Box- 
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Box': return replace(self, size=self.size * factor)Scales each individual geometry by
factor. The individualcenterpoints act as pivots for the operation.Args
factor: Returns:
 def smallest(self, dim: str | Sequence | set | phiml.math._shape.Shape | Callable | None) ‑> phi.geom._box.Box- 
Expand source code
def smallest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.smallest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.max(self.lower, dim), math.min(self.upper, dim)) def without(self, dims: Tuple[str, ...])- 
Expand source code
def without(self, dims: Tuple[str, ...]): assert self.is_axis_aligned.all, f"Box.without() is only supported for axis-aligned boxes (rot=None)" remaining = list(self.shape.get_item_names('vector')) for dim in dims: if dim in remaining: remaining.remove(dim) return self.vector[remaining] 
 class BaseBox (center: phiml.math._tensors.Tensor,
size: phiml.math._tensors.Tensor,
rot: phiml.math._tensors.Tensor,
is_open: phiml.math._tensors.Tensor,
variable_attrs: Tuple[str, ...] = ('center', 'size', 'rot'))- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) class Box(Geometry, 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)`. """ center: Tensor size: Tensor rot: Tensor # can be Layout(None) for no rotation is_open: Tensor # Infinite extent per face, (~side, vector) or fewer variable_attrs: Tuple[str, ...] = ('center', 'size', 'rot') def __post_init__(self): assert isinstance(self.center, Tensor) and 'vector' in channel(self.center) assert isinstance(self.size, Tensor) assert isinstance(self.rot, Tensor) @property def half_size(self): return self.size * 0.5 @cached_property def lower(self): return math.where(self.is_open.side['lower'], -math.INF, self.center - self.half_size) @cached_property def upper(self): return math.where(self.is_open.side['upper'], math.INF, self.center + self.half_size) @cached_property def is_finite(self): return not self.is_open.any def __repr__(self): if self.rot is not None: return f"Cuboid(center={self.center}, size={self.size})" if self.center is None or self.size 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}]' @cached_property def shape(self): return self.center.shape & self.size.shape & (shape(self.rot) - '~vector') @property def pos(self): return self.center @property def volume(self) -> Tensor: return math.prod(self.size, 'vector') @property def is_axis_aligned(self): return self.rot == None @property def rotation_matrix(self) -> Tensor: return rotation_matrix(self.rot, self.shape['vector'], none_to_unit=True) def at(self, center: Tensor) -> 'Box': return replace(self, center=center) def rotated(self, angle) -> 'Box': rot = wrap(angle) if self.is_axis_aligned.all else self.rotation_matrix @ rotation_matrix(angle) return replace(self, rot=rot) def scaled(self, factor: Union[float, Tensor]) -> 'Box': return replace(self, size=self.size * factor) 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, 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) + origin_loc def __mul__(self, other): if not isinstance(other, Box): return NotImplemented assert self.is_axis_aligned.all and other.is_axis_aligned.all, f"Box * Box only supported for axis-aligned boxes (rot=None)." pos = concat([self.center, other.center], 'vector') size = concat([self.size, other.size], 'vector') return replace(self, center=pos, size=size) def bounding_half_extent(self) -> Tensor: if self.rot 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: union_dims = instance(self) - instance(location) location = self.global_to_local(location, scale=False, origin='center') # scale can only be performed for finite sizes if not self.is_open.any: bool_inside = abs(location) <= self.half_size else: above_lower = (location > self.lower) | self.is_open.side.dual['lower'] below_upper = (location < self.upper) | self.is_open.side.dual['upper'] bool_inside = above_lower & below_upper bool_inside = math.all(bool_inside, 'vector') bool_inside = math.any(bool_inside, union_dims) return bool_inside def largest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.largest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.min(self.lower, dim), math.max(self.upper, dim)) def smallest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.smallest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.max(self.lower, dim), math.min(self.upper, dim)) def without(self, dims: Tuple[str, ...]): assert self.is_axis_aligned.all, f"Box.without() is only supported for axis-aligned boxes (rot=None)" remaining = list(self.shape.get_item_names('vector')) for dim in dims: if dim in remaining: remaining.remove(dim) return self.vector[remaining] def bounding_radius(self): return vec_length(self.half_size) 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 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). """ assert not self.is_open.any, f"approximate_signed_distance not supported for open boxes" # 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 approximate_closest_surface(self, location: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: assert not self.is_open.any, f"approximate_closest_surface not supported for open boxes" 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) 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 = vec_length(delta) * math.sign(max_sgn_dist) return sgn_surf_dist, delta, normal, offset, face_index def sample_uniform(self, *shape: Shape) -> Tensor: assert not self.is_open.any, f"sample_uniform not supported for open boxes" uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector']) return self.lower + uniform * self.size def contains(self, other: 'Box'): """ Tests if the other box lies fully inside this box. """ assert not self.is_open.any and not other.is_open.any, f"contains not supported for open boxes" assert self.is_axis_aligned.all and other.rot is None, f"contains() is only supported for axis-aligned boxes (rot=None)." return np.all(other.lower >= self.lower) and np.all(other.upper <= self.upper) 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=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 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 @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, 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 = to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict))) vectors = rotate(unit_vectors, self.rot) 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 @property def is_size_variable(self): warnings.warn("Box.is_size_variable is deprecated. Check Box.variable_attrs instead.", DeprecationWarning, stacklevel=2) return 'size' in self.variable_attrs def corner_representation(self) -> 'Box': assert self.is_axis_aligned.all, f"corner_representation does not support rotations" return self box = corner_representation def center_representation(self) -> 'Cuboid': return self cuboid = center_representationSimple 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 askwargs.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)`.Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. var center : phiml.math._tensors.Tensorprop 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_cornerReturns
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')) # ~vectorArea 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_sizeCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~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 = to_float(math.range(self.shape['vector']) == math.range(dual(**self.shape['vector'].untyped_dict))) vectors = rotate(unit_vectors, self.rot) 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) prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': return Cuboid(self.face_centers, self.half_size, self.rot, size_variable=False) prop half_size- 
Expand source code
@property def half_size(self): return self.size * 0.5 prop is_axis_aligned- 
Expand source code
@property def is_axis_aligned(self): return self.rot == None var is_finite- 
Expand source code
@cached_property def is_finite(self): return not self.is_open.any var is_open : phiml.math._tensors.Tensorprop is_size_variable- 
Expand source code
@property def is_size_variable(self): warnings.warn("Box.is_size_variable is deprecated. Check Box.variable_attrs instead.", DeprecationWarning, stacklevel=2) return 'size' in self.variable_attrs var lower- 
Expand source code
@cached_property def lower(self): return math.where(self.is_open.side['lower'], -math.INF, self.center - self.half_size) prop pos- 
Expand source code
@property def pos(self): return self.center var rot : phiml.math._tensors.Tensorprop rotation_matrix : phiml.math._tensors.Tensor- 
Expand source code
@property def rotation_matrix(self) -> Tensor: return rotation_matrix(self.rot, self.shape['vector'], none_to_unit=True) var shape- 
Expand source code
@cached_property def shape(self): return self.center.shape & self.size.shape & (shape(self.rot) - '~vector') var size : phiml.math._tensors.Tensorvar upper- 
Expand source code
@cached_property def upper(self): return math.where(self.is_open.side['upper'], math.INF, self.center + self.half_size) var variable_attrs : Tuple[str, ...]prop volume : phiml.math._tensors.Tensor- 
Expand source code
@property def volume(self) -> Tensor: return math.prod(self.size, 'vector')phi.math.Tensorrepresenting 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]: assert not self.is_open.any, f"approximate_closest_surface not supported for open boxes" 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) 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 = vec_length(delta) * math.sign(max_sgn_dist) return sgn_surf_dist, delta, normal, offset, face_indexFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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). """ assert not self.is_open.any, f"approximate_signed_distance not supported for open boxes" # 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 distanceComputes the signed L-infinity norm (manhattan distance) from the location to the nearest side of the box. For an outside location
lwith the closest surface points, the distance ismax(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 at(self, center: phiml.math._tensors.Tensor) ‑> phi.geom._box.Box- 
Expand source code
def at(self, center: Tensor) -> 'Box': return replace(self, center=center)Returns a copy of this
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor- 
Expand source code
def bounding_half_extent(self) -> Tensor: if self.rot 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_sizeThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 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.is_axis_aligned.all, f"corner_representation does not support rotations" return self def center_representation(self) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self) -> 'Cuboid': return self def contains(self, other: Box)- 
Expand source code
def contains(self, other: 'Box'): """ Tests if the other box lies fully inside this box. """ assert not self.is_open.any and not other.is_open.any, f"contains not supported for open boxes" assert self.is_axis_aligned.all and other.rot is None, f"contains() is only supported for axis-aligned boxes (rot=None)." 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.is_axis_aligned.all, f"corner_representation does not support rotations" return self def cuboid(self) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self) -> 'Cuboid': return self 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, invert=True) if scale: pos /= (self.half_size if origin == 'center' else self.size) return posTransform 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'ororigin='center', respectively. origin- 'lower' or 'center'
 
Returns
Box-space coordinate
Tensor def largest(self, dim: str | Sequence | set | phiml.math._shape.Shape | Callable | None) ‑> phi.geom._box.Box- 
Expand source code
def largest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.largest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.min(self.lower, dim), math.max(self.upper, dim)) def lies_inside(self, location: phiml.math._tensors.Tensor) ‑> phiml.math._tensors.Tensor- 
Expand source code
def lies_inside(self, location: Tensor) -> Tensor: union_dims = instance(self) - instance(location) location = self.global_to_local(location, scale=False, origin='center') # scale can only be performed for finite sizes if not self.is_open.any: bool_inside = abs(location) <= self.half_size else: above_lower = (location > self.lower) | self.is_open.side.dual['lower'] below_upper = (location < self.upper) | self.is_open.side.dual['upper'] bool_inside = above_lower & below_upper bool_inside = math.all(bool_inside, 'vector') bool_inside = math.any(bool_inside, union_dims) return bool_insideTests 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) + 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=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) * shiftShifts 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) ‑> phi.geom._box.Box- 
Expand source code
def rotated(self, angle) -> 'Box': rot = wrap(angle) if self.is_axis_aligned.all else self.rotation_matrix @ rotation_matrix(angle) return replace(self, rot=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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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: assert not self.is_open.any, f"sample_uniform not supported for open boxes" uniform = math.random_uniform(self.shape.non_singleton.without('vector'), *shape, self.shape['vector']) return self.lower + uniform * self.sizeSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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.Box- 
Expand source code
def scaled(self, factor: Union[float, Tensor]) -> 'Box': return replace(self, size=self.size * factor)Scales each individual geometry by
factor. The individualcenterpoints act as pivots for the operation.Args
factor: Returns:
 def smallest(self, dim: str | Sequence | set | phiml.math._shape.Shape | Callable | None) ‑> phi.geom._box.Box- 
Expand source code
def smallest(self, dim: DimFilter) -> 'Box': assert self.is_axis_aligned.all, f"Box.smallest() is only supported for axis-aligned boxes (rot=None)" dim = self.shape.without('vector').only(dim) if not dim: return self return box_from_limits(math.max(self.lower, dim), math.min(self.upper, dim)) def without(self, dims: Tuple[str, ...])- 
Expand source code
def without(self, dims: Tuple[str, ...]): assert self.is_axis_aligned.all, f"Box.without() is only supported for axis-aligned boxes (rot=None)" remaining = list(self.shape.get_item_names('vector')) for dim in dims: if dim in remaining: remaining.remove(dim) return self.vector[remaining] 
 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, ...] = () @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 NotImplementedErrorN-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
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 var axis : strprop 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
nameto slicingdict. 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
nameto slicingdict. var center : phiml.math._tensors.Tensorprop 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. var depth : phiml.math._tensors.Tensorprop 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.upCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~elements. Returns 0-vectors for unconnected cells. prop face_normals : phiml.math._tensors.Tensor- 
Expand source code
@property def face_normals(self) -> Tensor: raise NotImplementedErrorNormal 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') prop faces : Geometry- 
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 radius : phiml.math._tensors.Tensorvar rotation : phiml.math._tensors.Tensorvar 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 value_attrs : Tuple[str, ...]var variable_attrs : Tuple[str, ...]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, idxFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 dimensionsTests 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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 individualcenterpoints 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
@dataclass(frozen=True, eq=False) class Geometry: """ Abstract base class for N-dimensional shapes. Main implementing classes: * `Sphere` * `Box` * `Cylinder` * `Graph` * `Mesh` * `Heightmap` * `SDFGrid` * `SDF` * `SplineSheet` 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. """ if TYPE_CHECKING: # allow center to be implemented as a field as well @property @abstractmethod def center(self) -> Tensor: """ Center location in single channel dimension. """ raise NotImplementedError(self.__class__) @property @abstractmethod 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. """ @property @abstractmethod def volume(self) -> Tensor: """ `phi.math.Tensor` representing the volume of each element. The result retains batch, spatial and instance dimensions. """ @property @abstractmethod def faces(self) -> 'Geometry': raise NotImplementedError(self.__class__) @property @abstractmethod 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. """ @property @abstractmethod def face_areas(self) -> Tensor: """ Area of face connecting a pair of cells. Shape `(elements, ~)`. Returns 0 for unconnected cells. """ @property @abstractmethod 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. """ @property @abstractmethod 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`. """ @property @abstractmethod 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`. """ @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 math.EMPTY_SHAPE @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 @abstractmethod 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. """ 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 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): """ 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_from_limits return box_from_limits(min_vec, max_vec) def bounding_sphere(self): from ._functions import vec_length from ._sphere import Sphere center = self.bounding_box().center dist = vec_length(self.center - center) + self.bounding_radius() max_dist = math.max(dist, dim=self.shape.non_batch - 'vector') return Sphere(center, max_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=all_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, attr_type=all_attributes) 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 return GeometryStack(math.layout(values, dim)) 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}"Abstract base class for N-dimensional shapes.
Main implementing classes:
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 asshape), save for getters, must not depend on any variables marked as variable via__variable_attrs__()as these may beNoneduring tracing. Equality checks must also take this into account.Subclasses
- phi.geom._box.Box
 - 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._geom_ops.Intersection
 - phi.geom._graph.Graph
 - phi.geom._grid.UniformGrid
 - phi.geom._heightmap.Heightmap
 - phi.geom._mesh.Mesh
 - phi.geom._sdf.SDF
 - phi.geom._sdf_grid.SDFGrid
 - phi.geom._sphere.Sphere
 - phi.geom._spline_sheet.BSplineSheet
 
Instance variables
prop boundary_elements : Dict[str, Dict[str, slice]]- 
Expand source code
@property @abstractmethod 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`. """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
nameto slicingdict. prop boundary_faces : Dict[str, Dict[str, slice]]- 
Expand source code
@property @abstractmethod 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`. """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
nameto slicingdict. prop corners : phiml.math._tensors.Tensor- 
Expand source code
@property @abstractmethod 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. """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 @abstractmethod def face_areas(self) -> Tensor: """ Area of face connecting a pair of cells. Shape `(elements, ~)`. Returns 0 for unconnected cells. """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 @abstractmethod 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. """Center of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~elements. Returns 0-vectors for unconnected cells. prop face_normals : phiml.math._tensors.Tensor- 
Expand source code
@property @abstractmethod 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. """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 math.EMPTY_SHAPE prop faces : Geometry- 
Expand source code
@property @abstractmethod 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 @abstractmethod 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. """The
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 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 @abstractmethod def volume(self) -> Tensor: """ `phi.math.Tensor` representing the volume of each element. The result retains batch, spatial and instance dimensions. """phi.math.Tensorrepresenting 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
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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_fractionComputes the approximate overlap between the geometry and a small other geometry. Returns 1.0 if
other_geometryis 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 ofother_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()andbounding_half_extent().The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using
approximate_signed_distance().Args
other_geometryGeometryor geometry batch for which to compute the overlap withself.balance- Mid-level between 0 and 1, default 0.5.
This value is returned when exactly half of 
other_geometrylies insideself.0.5 < balance <= 1makesselfseem larger while0 <= balance < 0.5makesselfseem 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_box(self)- 
Expand source code
def bounding_box(self): """ 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_from_limits return box_from_limits(min_vec, max_vec) 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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): from ._functions import vec_length from ._sphere import Sphere center = self.bounding_box().center dist = vec_length(self.center - center) + self.bounding_radius() max_dist = math.max(dist, dim=self.shape.non_batch - 'vector') return Sphere(center, max_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 resultMultiplies `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
Tensorof 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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 individualcenterpoints 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, attr_type=all_attributes) return not differencesQuick equality check. May return
Falseeven ifother == self. However, ifTrueis 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:Ancestors
- builtins.BaseException
 
 class Graph (nodes: phi.geom._geom.Geometry,
edges: phiml.math._tensors.Tensor,
boundary: Dict[str, Dict[str, slice]])- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) 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. """ nodes: Geometry edges: Tensor boundary: Dict[str, Dict[str, slice]] variable_attrs = ('nodes', 'edges') def __post_init__(self): assert isinstance(self.nodes, Geometry), f"nodes must be a Geometry but got {self.nodes}" node_dims = non_batch(self.nodes).non_channel assert node_dims in self.edges.shape and self.edges.shape.dual.rank == node_dims.rank, f"edges must contain all node dims {node_dims} as primal and dual but got {self.edges.shape}" @cached_property def connectivity(self) -> Tensor: return math.tensor_like(self.edges, 1) if math.is_sparse(self.edges) else (self.edges != 0) & ~math.is_nan(self.edges) def as_points(self): return replace(self, nodes=Point(self.nodes.center)) @cached_property def deltas(self): return math.pairwise_distances(self.nodes.center, format=self.edges) @cached_property def unit_deltas(self): return math.safe_div(self.deltas, self.distances) @cached_property def distances(self): return vec_length(self.deltas) @cached_property def bounding_distance(self) -> Optional[Tensor]: return math.max(self.distances) @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): # shift varies between elements raise NotImplementedError("Shifting the node positions of a Graph is not supported as it would invalidate distances.") return replace(self, nodes=self.nodes.shifted(delta)) def rotated(self, angle: Union[float, Tensor]) -> 'Geometry': return replace(self, nodes=self.nodes.rotated(angle)) def scaled(self, factor: Union[float, Tensor]) -> 'Geometry': return replace(self, nodes=self.nodes.scaled(factor)) 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 return Graph(self.nodes[item], self.edges[edge_sel], self.boundary)A graph consists of multiple geometry nodes and corresponding edge information.
Edges are stored as a Tensor with the same axes ad
geometryplus their dual counterparts. Additional dimensions can be added toedgesto store vector-valued connectivity weights.Ancestors
- phi.geom._geom.Geometry
 
Class variables
var variable_attrs
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 var boundary : Dict[str, Dict[str, slice]]prop boundary_elements : Dict[str, Dict[str, slice]]- 
Expand source code
@property def boundary_elements(self) -> Dict[str, Dict[str, slice]]: return self.boundarySlices 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
nameto slicingdict. 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 elementsSlices 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
nameto slicingdict. var bounding_distance : phiml.math._tensors.Tensor | None- 
Expand source code
@cached_property def bounding_distance(self) -> Optional[Tensor]: return math.max(self.distances) prop center : phiml.math._tensors.Tensor- 
Expand source code
@property def center(self) -> Tensor: return self.nodes.center var connectivity : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def connectivity(self) -> Tensor: return math.tensor_like(self.edges, 1) if math.is_sparse(self.edges) else (self.edges != 0) & ~math.is_nan(self.edges) var deltas- 
Expand source code
@cached_property def deltas(self): return math.pairwise_distances(self.nodes.center, format=self.edges) var distances- 
Expand source code
@cached_property def distances(self): return vec_length(self.deltas) var edges : phiml.math._tensors.Tensorprop face_areas : phiml.math._tensors.Tensor- 
Expand source code
@property def face_areas(self) -> Tensor: raise NotImplementedErrorArea 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 NotImplementedErrorCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~elements. Returns 0-vectors for unconnected cells. prop face_normals : phiml.math._tensors.Tensor- 
Expand source code
@property def face_normals(self) -> Tensor: raise NotImplementedErrorNormal 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 prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': raise NotImplementedError var nodes : phi.geom._geom.Geometryprop shape : phiml.math._shape.Shape- 
Expand source code
@property def shape(self) -> Shape: return self.nodes.shapeThe
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 var unit_deltas- 
Expand source code
@cached_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.volumephi.math.Tensorrepresenting 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 NotImplementedErrorFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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 NotImplementedErrorComputes 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
Returns
Float
Tensor def as_points(self)- 
Expand source code
def as_points(self): return replace(self, nodes=Point(self.nodes.center)) 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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 NotImplementedErrorTests 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': return replace(self, nodes=self.nodes.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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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, nodes=self.nodes.scaled(factor))Scales each individual geometry by
factor. The individualcenterpoints 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): # shift varies between elements raise NotImplementedError("Shifting the node positions of a Graph is not supported as it would invalidate distances.") return replace(self, nodes=self.nodes.shifted(delta))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,
fill_below: phiml.math._tensors.Tensor[bool] = <factory>,
extrapolation: phiml.math.extrapolation.Extrapolation = none,
variable_attrs: Tuple[str, ...] = ('height', 'bounds', 'extrapolation'),
value_attrs: Tuple[str, ...] = ())- 
Expand source code
@sliceable @dataclass(frozen=True, eq=False) class Heightmap(Geometry): height: Tensor """Heightmap `Tensor` of absolute (world-space) height values. Scalar height values on a d-1 dimensional grid.""" bounds: Box """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: Tensor """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: Tensor[bool] = field(default_factory=lambda: wrap(True)) """Whether the inside is below or above the height values.""" extrapolation: Extrapolation = NONE """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.""" variable_attrs: Tuple[str, ...] = 'height', 'bounds', 'extrapolation' value_attrs: Tuple[str, ...] = () def __post_init__(self): assert channel(self.height).is_empty, f"height must be a scalar quantity but got {self.height.shape}" assert spatial(self.height), f"height field must have at least one spatial dim but got {self.height}" assert self.bounds.vector.size == spatial(self.height).rank + 1, f"bounds must include the spatial grid dimensions {spatial(self.height)} and the height dimension but got {self.bounds}" if math.all_available(self.height, self.bounds.lower, self.bounds.upper): assert self.bounds[self.hdim].lies_inside(self.height).all, f"All height values should be within the {self.hdim}-range given by bounds but got height={self.height}" @property def shape(self) -> Shape: return self.resolution & channel(self.bounds) & non_spatial(self.height) @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) @cached_property def hdim(self): return spatial(*self.bounds.vector.item_names).without(self.height.shape).name @cached_property def face_cache(self): 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) faces: Face = stack([proj_faces, *unstack(secondary_faces, 'side')], batch(consider='self,outside,inside'), expand_values=True) return cached(faces) # otherwise, this may get expanded during tracing @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.face_cache) 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 bounding_half_extent(self) -> Tensor: h_min, h_max = self.face_cache.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.face_cache.center.consider[0] @property def volume(self) -> Tensor: return math.prod(self.bounding_half_extent() * 2, channel) @property def face_centers(self) -> Tensor: return self.face_cache.center @property def face_areas(self) -> Tensor: raise NotImplementedError @property def face_normals(self) -> Tensor: return self.face_cache.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.face_cache.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 def __getattr__(self, item): if item in self.shape: return BoundDim(self, item) raise AttributeError(f"{self.__class__.__name__} has no attribute '{item}'")Heightmap(height: phiml.math._tensors.Tensor, bounds: phi.geom._box.Box, max_dist: phiml.math._tensors.Tensor, fill_below: phiml.math._tensors.Tensor[bool] =
, extrapolation: phiml.math.extrapolation.Extrapolation = none, variable_attrs: Tuple[str, …] = ('height', 'bounds', 'extrapolation'), value_attrs: Tuple[str, …] = ()) Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. var bounds : phi.geom._box.Box- 
Locations outside
bounds' can never lie inside this geometry ifextrapolation is None`. Otherwise, only the height dimension is checked. The grid dimensions ofboundsmust be finite but the height dimension may be infinite to count all values above/belowheightas inside. prop center : phiml.math._tensors.Tensor- 
Expand source code
@property def center(self) -> Tensor: return self.face_cache.center.consider[0] prop dx- 
Expand source code
@property def dx(self): return self.bounds.size[self.resolution.name_list] / spatial(self.resolution) var extrapolation : phiml.math.extrapolation.Extrapolation- 
Surface height outside
bounds´. Can be any validphiml.math.Extrapolation`, such as a constant. If notNone, values outsideboundswill be checked against the extrapolatedheightvalues. Otherwise, values outsideboundsalways lie on the outside. prop face_areas : phiml.math._tensors.Tensor- 
Expand source code
@property def face_areas(self) -> Tensor: raise NotImplementedErrorArea of face connecting a pair of cells. Shape
(elements, ~). Returns 0 for unconnected cells. var face_cache- 
Expand source code
@cached_property def face_cache(self): 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) faces: Face = stack([proj_faces, *unstack(secondary_faces, 'side')], batch(consider='self,outside,inside'), expand_values=True) return cached(faces) # otherwise, this may get expanded during tracing prop face_centers : phiml.math._tensors.Tensor- 
Expand source code
@property def face_centers(self) -> Tensor: return self.face_cache.centerCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~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.face_cache.normalNormal 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.face_cache.center) var fill_below : phiml.math._tensors.Tensor[bool]- 
Whether the inside is below or above the height values.
 prop grid_bounds- 
Expand source code
@property def grid_bounds(self): return self.bounds[self.resolution.name_list] var hdim- 
Expand source code
@cached_property def hdim(self): return spatial(*self.bounds.vector.item_names).without(self.height.shape).name var height : phiml.math._tensors.Tensor- 
Heightmap
Tensorof absolute (world-space) height values. Scalar height values on a d-1 dimensional grid. var max_dist : phiml.math._tensors.Tensor- 
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.
 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.resolution & channel(self.bounds) & non_spatial(self.height)The
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 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) var value_attrs : Tuple[str, ...]var variable_attrs : Tuple[str, ...]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.Tensorrepresenting 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.face_cache) 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
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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 NotImplementedErrorReturns a copy of this
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor- 
Expand source code
def bounding_half_extent(self) -> Tensor: h_min, h_max = self.face_cache.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) * .5The 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 NotImplementedErrorReturns 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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 NotImplementedErrorScales each individual geometry by
factor. The individualcenterpoints act as pivots for the operation.Args
factor: Returns:
 def shallow_equals(self, other)- 
Expand source code
def shallow_equals(self, other): return self == otherQuick equality check. May return
Falseeven ifother == self. However, ifTrueis 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
@sliceable # __getitem__ implemented manually to reject dual dim slicing @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, } def _build_faces(self): return build_faces(self.vertices.center, self.elements, self.boundaries, self.element_rank, self.periodic, self._vertex_mean, self.face_format) @property def face_shape(self) -> Shape: if not self.boundary_faces: return instance(self) & dual dual_len = max([next(iter(sl.values())).stop for sl in self.boundary_faces.values()]) dim = instance(self) return dim.as_dual().with_size(dual_len) + dim @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]]: if self.boundaries is None: return {} return self._faces['boundary_slices'] @property def all_boundary_faces(self) -> Dict[str, slice]: if self.face_shape.dual.size == self.elements.shape.instance.size: return {} 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.without_sizes()) else: raise NotImplementedError if widths is None: widths = self.boundary_faces if isinstance(widths, dict) and len(widths) == 0: return value 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: if not self.all_boundary_faces: dual_dim = instance(self).as_dual().with_size(0) return zeros(instance(self) & dual_dim) 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.""" if not self.all_boundary_faces: return self._cell_deltas 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: """Neighbor element connectivity, excluding diagonal.""" 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.setdiag(0) connected_elements.eliminate_zeros() 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] # 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[coo.col])), 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) @cached_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) @cached_property def _v_kdtree_i(self): return math.find_closest(self.vertices.center, method='kd') def closest_vertex(self, location: Tensor): idx = self._v_kdtree_i(location) v_pos = self.vertices.center[idx] return idx, v_pos - location 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]: def acs_single_mesh(self: Mesh, location: Tensor): if self.element_rank == 2 and self.spatial_rank == 3: closest_elem = find_closest(self.center, location) normal = self.normals[closest_elem] if math.get_format(self.elements) == 'compact-cols' and dual(self.elements._indices).size == 3: # triangle mesh three_vertices = self.elements[closest_elem]._indices v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual(self.elements)) surf_pos = closest_on_triangle(v1, v2, v3, location, exact_edges=True) delta = surf_pos - location sgn_dist = vec_length(delta) * -sign(delta.vector @ normal) return sgn_dist, delta, normal, None, closest_elem center = self.center[closest_elem] face_size = sqrt(self.volume) * 2 size = face_size[closest_elem] sgn_dist = plane_sgn_dist(center, normal, location) delta_far = center - location # this is not accurate... delta_near = normal * -sgn_dist far_fac = minimum(1, abs(sgn_dist) / size) delta = far_fac * delta_far + (1 - far_fac) * delta_near return sgn_dist, delta, normal, 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 return math.map(acs_single_mesh, self, location, dims=batch(self.elements), map_name="Mesh.approximate_closest_surface") 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) -> 'Box': 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: Union[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 - instance(self.vertices)).name, 'vector']) def __repr__(self): return Geometry.__repr__(self)Unstructured mesh, consisting of vertices and elements.
Use
mesh()ormesh_from_numpy()to construct a mesh manually orload_su2()to load one from a file.Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop all_boundary_faces : Dict[str, slice]- 
Expand source code
@property def all_boundary_faces(self) -> Dict[str, slice]: if self.face_shape.dual.size == self.elements.shape.instance.size: return {} return {self.face_shape.dual.name: slice(instance(self).volume, None)} var boundaries : Dict[str, Dict[str, slice]]- 
Slices to retrieve boundary face values.
 var boundary_connectivity : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def boundary_connectivity(self) -> Tensor: if not self.all_boundary_faces: dual_dim = instance(self).as_dual().with_size(0) return zeros(instance(self) & dual_dim) 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
nameto slicingdict. prop boundary_faces : Dict[str, Dict[str, slice]]- 
Expand source code
@property def boundary_faces(self) -> Dict[str, Dict[str, slice]]: if self.boundaries is None: return {} 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
nameto slicingdict. 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
Tensorof 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: """Neighbor element connectivity, excluding diagonal.""" 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.setdiag(0) connected_elements.eliminate_zeros() connected_elements.data = np.ones_like(connected_elements.data) element_connectivity = wrap(connected_elements, instance(self.elements), instance(self.elements).as_dual()) return element_connectivityNeighbor element connectivity, excluding diagonal.
 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
Tensorlisting ordered vertex indices per element (solid or surface element, depending onelement_rank). Must have one instance dim listing the elements and the corresponding dual dim tovertices. The vertex count of an element is equal to the number of elements in that row (i.e. summing the dual dim). 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_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 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: if not self.boundary_faces: return instance(self) & dual dual_len = max([next(iter(sl.values())).stop for sl in self.boundary_faces.values()]) dim = instance(self) return dim.as_dual().with_size(dual_len) + dim prop faces : Geometry- 
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 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 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.""" if not self.all_boundary_faces: return self._cell_deltas 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 NotImplementedErrorExtrinsic element normal space. This is a 0D vector for solid elements and 1D for surface elements.
 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 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 value_attrs : Tuple[str, ...]var variable_attrs : Tuple[str, ...]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. var vertices : phi.geom._geom.Geometry- 
Vertices are represented by a
Geometryinstance with an instance dim. var volume : phiml.math._tensors.Tensor- 
Expand source code
@cached_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 
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]: def acs_single_mesh(self: Mesh, location: Tensor): if self.element_rank == 2 and self.spatial_rank == 3: closest_elem = find_closest(self.center, location) normal = self.normals[closest_elem] if math.get_format(self.elements) == 'compact-cols' and dual(self.elements._indices).size == 3: # triangle mesh three_vertices = self.elements[closest_elem]._indices v1, v2, v3 = unstack(self.vertices.center[{instance: three_vertices}], dual(self.elements)) surf_pos = closest_on_triangle(v1, v2, v3, location, exact_edges=True) delta = surf_pos - location sgn_dist = vec_length(delta) * -sign(delta.vector @ normal) return sgn_dist, delta, normal, None, closest_elem center = self.center[closest_elem] face_size = sqrt(self.volume) * 2 size = face_size[closest_elem] sgn_dist = plane_sgn_dist(center, normal, location) delta_far = center - location # this is not accurate... delta_near = normal * -sgn_dist far_fac = minimum(1, abs(sgn_dist) / size) delta = far_fac * delta_far + (1 - far_fac) * delta_near return sgn_dist, delta, normal, 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 return math.map(acs_single_mesh, self, location, dims=batch(self.elements), map_name="Mesh.approximate_closest_surface")Find the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_box(self) ‑> phi.geom._box.Box- 
Expand source code
def bounding_box(self) -> 'Box': return self.vertices.bounding_box() 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_deltaThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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_distReturns 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_idxIf
locationis not within the cell at indexfrom_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 
indexis invalid. This is only possible ifallow_exitis true. is_outside- Whether 
locationwas outside the cell at indexstart_cell_idx. 
 def closest_vertex(self, location: phiml.math._tensors.Tensor)- 
Expand source code
def closest_vertex(self, location: Tensor): idx = self._v_kdtree_i(location) v_pos = self.vertices.center[idx] return idx, v_pos - location 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] # 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[coo.col])), 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.without_sizes()) else: raise NotImplementedError if widths is None: widths = self.boundary_faces if isinstance(widths, dict) and len(widths) == 0: return value 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 NotImplementedErrorReturns 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. def scaled(self, factor: phiml.math._tensors.Tensor | float) ‑> phi.geom._geom.Geometry- 
Expand source code
def scaled(self, factor: Union[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 individualcenterpoints 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
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) 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. """ location: Tensor variable_attrs = ('location',) value_attrs = ('location',) def __post_init__(self): assert 'vector' in self.location.shape, "location must have a vector dimension" assert self.location.shape.get_item_names('vector') is not None, "Vector dimension needs to list spatial dimension as item names." @property def center(self) -> Tensor: return self.location @property def shape(self) -> Shape: return self.location.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.norm(location - self.location) def bounding_radius(self) -> Tensor: return math.zeros() def bounding_half_extent(self) -> Tensor: return wrap(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
Pointrepresents a single n-dimensional point or a batch of points.Ancestors
- phi.geom._geom.Geometry
 
Class variables
var value_attrsvar variable_attrs
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. prop center : phiml.math._tensors.Tensor- 
Expand source code
@property def center(self) -> Tensor: return self.location prop corners- 
Expand source code
@property def corners(self): return self.locationReturns
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.locationCenter of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~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 prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': return self var location : phiml.math._tensors.Tensorprop shape : phiml.math._shape.Shape- 
Expand source code
@property def shape(self) -> Shape: return self.location.shapeThe
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 prop volume : phiml.math._tensors.Tensor- 
Expand source code
@property def volume(self) -> Tensor: return math.wrap(0)phi.math.Tensorrepresenting 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.norm(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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor- 
Expand source code
def bounding_half_extent(self) -> Tensor: return wrap(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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 selfReturns 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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 selfScales each individual geometry by
factor. The individualcenterpoints 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,
bounds: phi.geom._box.Box,
vol: phiml.math._tensors.Tensor = None,
grad_fn: Callable | None = None,
variable_attrs: Tuple[str, ...] = ('bounds',),
value_attrs: Tuple[str, ...] = ())- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) class SDF(Geometry): """ Function-based signed distance field. Negative values lie inside the geometry, the 0-level represents the surface. """ sdf: Callable # SDF function. First argument is a `phiml.math.Tensor` with a `vector` channel dim. bounds: Box # Grid limits. The bounds fully enclose all virtual cells. vol: Tensor = None grad_fn: Optional[Callable] = None # Returns (sdf, grad) when called with location variable_attrs: Tuple[str, ...] = 'bounds', value_attrs: Tuple[str, ...] = () def __call__(self, location, *aux_args, **aux_kwargs): native_loc = not isinstance(location, Tensor) if native_loc: location = 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 @cached_property def out_shape(self): dims = channel(self.bounds) assert 'vector' in dims, f"If out_shape is not specified, either bounds, center or bounding_radius must be given." return self.sdf(math.zeros(dims['vector'])).shape @cached_property def grad(self) -> Callable: return math.gradient(self.sdf, wrt=0, get_output=True) if self.grad_fn is None else self.grad_fn @property def center(self): return self.bounds.center @property def volume(self): return self.vol @property def size(self): return self.bounds.size @property def shape(self) -> Shape: return self.out_shape & self.bounds.shape @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.bounds.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) -> 'Box': 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': raise NotImplementedError("SDF does not yet support scaling") def __getitem__(self, item): item = slicing_dict(self, item) if not item: return self raise NotImplementedError("SDF cannot be sliced.") @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.
Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. var bounds : phi.geom._box.Boxprop center- 
Expand source code
@property def center(self): return self.bounds.center 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_directionor~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 prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': raise NotImplementedError(f"SDF does not support faces") var grad : Callable- 
Expand source code
@cached_property def grad(self) -> Callable: return math.gradient(self.sdf, wrt=0, get_output=True) if self.grad_fn is None else self.grad_fn var grad_fn : Callable | Nonevar out_shape- 
Expand source code
@cached_property def out_shape(self): dims = channel(self.bounds) assert 'vector' in dims, f"If out_shape is not specified, either bounds, center or bounding_radius must be given." return self.sdf(math.zeros(dims['vector'])).shape var sdf : Callableprop shape : phiml.math._shape.Shape- 
Expand source code
@property def shape(self) -> Shape: return self.out_shape & self.bounds.shapeThe
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 prop size- 
Expand source code
@property def size(self): return self.bounds.size var value_attrs : Tuple[str, ...]var variable_attrs : Tuple[str, ...]var vol : phiml.math._tensors.Tensorprop volume- 
Expand source code
@property def volume(self): return self.volphi.math.Tensorrepresenting 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_indexFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 def bounding_box(self) ‑> phi.geom._box.Box- 
Expand source code
def bounding_box(self) -> 'Box': return self.bounds 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 boundsThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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: sdf = self.sdf(location) return sdf <= 0Tests 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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("SDF does not yet support scaling")Scales each individual geometry by
factor. The individualcenterpoints 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 (values: phiml.math._tensors.Tensor,
bounds: phi.geom._box.Box,
approximate_outside: bool = True,
to_surface: phiml.math._tensors.Tensor | None = None,
surf_normal: phiml.math._tensors.Tensor | None = None,
surf_index: phiml.math._tensors.Tensor | None = None,
value_attrs: Tuple[str, ...] = ('sdf',),
variable_attrs: Tuple[str, ...] = ('sdf', 'bounds', 'to_surface', 'surf_normal', 'surf_index'))- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True) class SDFGrid(Geometry): """ Grid-based signed distance field. """ values: Tensor # 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: Box # Grid limits. The bounds fully enclose all virtual cells. approximate_outside: bool = True # Whether queries outside the SDF grid should return approximate values. This requires additional computations. to_surface: Optional[Tensor] = None # Pre-computed vector field from grid points to closest surface point. surf_normal: Optional[Tensor] = None # Pre-computed surface normal at closest surface point. surf_index: Optional[Tensor] = None # Pre-computed surface face index at closest surface point. value_attrs: Tuple[str, ...] = ('sdf',) variable_attrs: Tuple[str, ...] = ('sdf', 'bounds', 'to_surface', 'surf_normal', 'surf_index') @cached_property def grad(self): grad = math.spatial_gradient(self.values, dx=self.dx, difference='forward', padding=math.extrapolation.ZERO_GRADIENT, stack_dim=channel('vector')) return grad[{dim: slice(0, -1) for dim in self.resolution.names}] def with_values(self, values: Tensor): values = expand(values, spatial(self.values) - spatial(values)) return replace(self, values=values) @property def size(self) -> Tensor: return self.bounds.size @property def resolution(self) -> Shape: return spatial(self.values) @cached_property def dx(self) -> Tensor: return self.bounds.size / spatial(self.values) @property def points(self) -> Tensor: return UniformGrid(spatial(self.values), self.bounds).center @cached_property def grid(self) -> UniformGrid: return UniformGrid(spatial(self.values), self.bounds) @cached_property def center(self) -> Tensor: min_index = math.argmin(self.values, spatial, index_dim=channel('vector')) return self.bounds.local_to_global(min_index / self.resolution) @property def shape(self) -> Shape: return non_spatial(self.values) & channel(vector=spatial(self.values)) @cached_property def volume(self) -> Tensor: filled = math.sum(self.values < 0) return filled * cprod(self.dx) @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.values, 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.values, 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.values, float_idx - .5, math.extrapolation.ZERO_GRADIENT) if self.approximate_outside: within_bounds = self.bounds.lies_inside(location) dist_from_center = vec_length(location - self.center) - self.cached_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.cached_bounding_radius @cached_property def cached_bounding_radius(self) -> Tensor: points = self.grid.center dist = vec_length(points - self.center) dist = math.where(self.values <= 0, dist, 0) return math.max(dist) 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': result = replace(self, bounds=self.bounds.shifted(delta)) if 'center' in self.__dict__: result.__dict__['center'] = self.center + delta return result 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 replace(self, bounds=bounds) 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.values <= 0 return math.where(abs(self.values) < .5*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, t, n, i = [math.downsample2x(v) for v in (self.values, self.to_surface, self.surf_normal, self.surf_index)] return SDFGrid(s, self.bounds, self.approximate_outside, t, n, i)Grid-based signed distance field.
Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 var approximate_outside : boolprop 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
nameto slicingdict. 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
nameto slicingdict. var bounds : phi.geom._box.Boxvar cached_bounding_radius : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def cached_bounding_radius(self) -> Tensor: points = self.grid.center dist = vec_length(points - self.center) dist = math.where(self.values <= 0, dist, 0) return math.max(dist) var center : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def center(self) -> Tensor: min_index = math.argmin(self.values, spatial, index_dim=channel('vector')) return self.bounds.local_to_global(min_index / self.resolution) 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. var dx : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def dx(self) -> Tensor: return self.bounds.size / spatial(self.values) 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_directionor~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 prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': raise NotImplementedError(f"SDF does not support faces") var grad- 
Expand source code
@cached_property def grad(self): grad = math.spatial_gradient(self.values, dx=self.dx, difference='forward', padding=math.extrapolation.ZERO_GRADIENT, stack_dim=channel('vector')) return grad[{dim: slice(0, -1) for dim in self.resolution.names}] var grid : phi.geom._grid.UniformGrid- 
Expand source code
@cached_property def grid(self) -> UniformGrid: return UniformGrid(spatial(self.values), self.bounds) prop points : phiml.math._tensors.Tensor- 
Expand source code
@property def points(self) -> Tensor: return UniformGrid(spatial(self.values), self.bounds).center prop resolution : phiml.math._shape.Shape- 
Expand source code
@property def resolution(self) -> Shape: return spatial(self.values) prop shape : phiml.math._shape.Shape- 
Expand source code
@property def shape(self) -> Shape: return non_spatial(self.values) & channel(vector=spatial(self.values))The
shapeof aGeometryconsists 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.
 
 - A single channel dimension called 
 prop size : phiml.math._tensors.Tensor- 
Expand source code
@property def size(self) -> Tensor: return self.bounds.size var surf_index : phiml.math._tensors.Tensor | Nonevar surf_normal : phiml.math._tensors.Tensor | Nonevar to_surface : phiml.math._tensors.Tensor | Nonevar value_attrs : Tuple[str, ...]var values : phiml.math._tensors.Tensorvar variable_attrs : Tuple[str, ...]var volume : phiml.math._tensors.Tensor- 
Expand source code
@cached_property def volume(self) -> Tensor: filled = math.sum(self.values < 0) return filled * cprod(self.dx) 
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.values, 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_indexFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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_geometryis 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 ofother_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()andbounding_half_extent().The default implementation of this method approximates other_geometry as a Sphere and computes the fraction using
approximate_signed_distance().Args
other_geometryGeometryor geometry batch for which to compute the overlap withself.balance- Mid-level between 0 and 1, default 0.5.
This value is returned when exactly half of 
other_geometrylies insideself.0.5 < balance <= 1makesselfseem larger while0 <= balance < 0.5makesselfseem 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.values <= 0 return math.where(abs(self.values) < .5*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.values, float_idx - .5, math.extrapolation.ZERO_GRADIENT) if self.approximate_outside: within_bounds = self.bounds.lies_inside(location) dist_from_center = vec_length(location - self.center) - self.cached_bounding_radius return math.where(within_bounds, sdf_val, dist_from_center) else: return sdf_valComputes 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
locationTensorwith one channel dimvectormatching the geometry'svectordim.
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
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 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 boundsThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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.cached_bounding_radiusReturns 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, t, n, i = [math.downsample2x(v) for v in (self.values, self.to_surface, self.surf_normal, self.surf_index)] return SDFGrid(s, self.bounds, self.approximate_outside, t, n, i) 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.values, 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 <= 0Tests 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 NotImplementedErrorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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 replace(self, bounds=bounds)Scales each individual geometry by
factor. The individualcenterpoints 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': result = replace(self, bounds=self.bounds.shifted(delta)) if 'center' in self.__dict__: result.__dict__['center'] = self.center + delta return resultReturns 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.values) - spatial(values)) return replace(self, values=values) 
 class Sphere (center: phiml.math._tensors.Tensor,
radius: phiml.math._tensors.Tensor,
variable_attrs: Tuple[str, ...] = ('center', 'radius'))- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) class Sphere(Geometry, metaclass=SphereType): """ N-dimensional sphere. Defined through center position and radius. """ center: Tensor radius: Tensor variable_attrs: Tuple[str, ...] = ('center', 'radius') def __post_init__(self): assert 'vector' in self.center.shape assert 'vector' not in self.radius.shape, f"Sphere radius must not vary along vector but got {self.radius}" @cached_property def shape(self): return self.center.shape & self.radius.shape @property def pos(self): return self.center @cached_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}") @staticmethod def area_from_radius(radius: Union[float, Tensor], spatial_rank: int): if spatial_rank == 1: return 0 elif spatial_rank == 2: return 2*PI * radius elif spatial_rank == 3: return 4*PI * radius**2 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 = 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 = 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 /= vec_length(unit_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 replace(self, center=center) def rotated(self, angle): return self def scaled(self, factor: Union[float, Tensor]) -> 'Geometry': return replace(self, radius=self.radius * factor) @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 expand(Sphere.area_from_radius(self.radius, self.spatial_rank), instance(self) + dual(shell=1)) @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=1) @property def corners(self) -> Tensor: return math.zeros(self.shape & dual(corners=0))N-dimensional sphere. Defined through center position and radius.
Ancestors
- phi.geom._geom.Geometry
 
Static methods
def area_from_radius(radius: phiml.math._tensors.Tensor | float, spatial_rank: int)- 
Expand source code
@staticmethod def area_from_radius(radius: Union[float, Tensor], spatial_rank: int): if spatial_rank == 1: return 0 elif spatial_rank == 2: return 2*PI * radius elif spatial_rank == 3: return 4*PI * radius**2 else: raise NotImplementedError(f"spatial_rank>3 not supported, got {spatial_rank}") 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 Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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
nameto slicingdict. var center : phiml.math._tensors.Tensorprop 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 expand(Sphere.area_from_radius(self.radius, self.spatial_rank), instance(self) + dual(shell=1))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_directionor~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=1) prop faces : Geometry- 
Expand source code
@property def faces(self) -> 'Geometry': raise NotImplementedError(f"Sphere.faces not implemented.") prop pos- 
Expand source code
@property def pos(self): return self.center var radius : phiml.math._tensors.Tensorvar shape- 
Expand source code
@cached_property def shape(self): return self.center.shape & self.radius.shape var variable_attrs : Tuple[str, ...]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) 
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 = 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_indexFind the closest surface face of this geometry given a point that can be outside or inside the geometry.
Args
locationTensorwith a single channel dimension called vector. Can have arbitrary other dimensions.
Returns
signed_distance- Scalar signed distance from 
locationto 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 
locationto 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 = vec_length(location - self.center, eps=1e-3) return math.min(distance - self.radius, self.shape.instance) # union for instance dimensionsComputes 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 replace(self, center=center)Returns a copy of this
Geometrywith the center atcenter. This is equal to callingself @ center.See Also:
Geometry.shifted().Args
center- New center as 
Tensor. 
Returns
 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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.radiusReturns 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 dimensionsTests 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 selfReturns 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
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 /= vec_length(unit_vector) return self.center + r * unit_vectorSamples uniformly distributed random points inside this volume.
Args
*shape- How many points to sample per individual geometry.
 
Returns
Tensorcontaining all dimensions fromGeometry.shape,shapeas well as achanneldimensionvectormatching the dimensionality of thisGeometry. 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)Scales each individual geometry by
factor. The individualcenterpoints act as pivots for the operation.Args
factor: Returns:
 
 class UniformGrid (resolution: phiml.math._shape.Shape, bounds: phi.geom._box.Box)- 
Expand source code
@sliceable(keepdims='vector') @dataclass(frozen=True, eq=False) class UniformGrid(Geometry, metaclass=UniformGridType): """ An instance of UniformGrid represents all cells of a regular grid as a batch of boxes. """ resolution: Shape bounds: Box def __post_init__(self): assert set(self.bounds.vector.labels) == set(self.resolution.names) @property def spatial_rank(self) -> int: return self.resolution.spatial_rank @cached_property def shape(self): return self.resolution & non_spatial(self.bounds) @cached_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.labels: 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.labels] return stack(centers, dual(vector=self.vector.labels)) @property def faces(self) -> Geometry: slices = [self.stagger(d, True, True) for d in self.resolution.names] return stack(slices, dual(vector=self.vector.labels)) @property def face_normals(self) -> Tensor: normals = [vec(**{d: float(d == dim) for d in self.vector.labels}) for dim in self.vector.labels] return stack(normals, dual(vector=self.vector.labels)) @property def face_areas(self) -> Tensor: areas = [math.prod(self.dx.vector[[d for d in self.vector.labels if d != dim]], 'vector') for dim in self.vector.labels] return stack(areas, dual(vector=self.vector.labels)) @cached_property def face_shape(self) -> Shape: staggered_shapes = [self.shape.spatial.with_dim_size(dim, self.shape.get_size(dim) + 1) for dim in self.vector.labels] return shape_stack(dual(vector=self.vector.labels), *staggered_shapes) def interior(self) -> 'Geometry': raise GeometryException("Regular grid does not have an interior") @property def grid_size(self): return self.bounds.size @cached_property def dx(self): return self.bounds.size / self.resolution @property def size(self): return self.dx @property def volume(self) -> Tensor: return math.prod(self.dx, 'vector') @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 corner_representation(self) -> 'Box': 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 with_scaled_resolution(self, scale: float): return UniformGrid(self.resolution.with_sizes([s*scale for s in self.resolution.sizes]), 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] 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 * self.dx upper = bounds.upper + (stop - self.resolution.get_size(dim)) * dim_mask * self.dx bounds = Box(lower, upper) return UniformGrid(resolution, bounds) def __pack_dims__(self, dims: Tuple[str, ...], packed_dim: Shape, pos: Optional[int], **kwargs) -> 'Box': 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 return GeometryStack(math.layout(values, dim)) 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.labels: 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) def shifted(self, delta: Tensor, **delta_by_dim): # 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 __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 def bounding_radius(self) -> Tensor: return vec_length(self.half_size)An instance of UniformGrid represents all cells of a regular grid as a batch of boxes.
Ancestors
- phi.geom._geom.Geometry
 
Instance variables
prop Tc- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ti- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 prop Ts- 
partial(func, args, *keywords) - new function with partial application of the given arguments and keywords.
 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
nameto slicingdict. 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.labels: result[dim+'-'] = {'~vector': dim, dim: slice(1)} result[dim+'+'] = {'~vector': dim, dim: slice(-1, None)} return resultSlices 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
nameto slicingdict. var bounds : phi.geom._box.Boxvar center- 
Expand source code
@cached_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 var dx- 
Expand source code
@cached_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.labels if d != dim]], 'vector') for dim in self.vector.labels] return stack(areas, dual(vector=self.vector.labels))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.labels] return stack(centers, dual(vector=self.vector.labels))Center of face connecting a pair of cells. Shape
(elements, ~, vector). Here,~represents arbitrary internal dual dimensions, such as~staggered_directionor~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.labels}) for dim in self.vector.labels] return stack(normals, dual(vector=self.vector.labels))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.
 var face_shape : phiml.math._shape.Shape- 
Expand source code
@cached_property def face_shape(self) -> Shape: staggered_shapes = [self.shape.spatial.with_dim_size(dim, self.shape.get_size(dim) + 1) for dim in self.vector.labels] return shape_stack(dual(vector=self.vector.labels), *staggered_shapes) 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.labels)) 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") var resolution : phiml.math._shape.Shapevar shape- 
Expand source code
@cached_property def shape(self): return self.resolution & non_spatial(self.bounds) prop size- 
Expand source code
@property def size(self): return self.dx prop spatial_rank : int- 
Expand source code
@property def spatial_rank(self) -> int: return self.resolution.spatial_rankNumber 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 prop volume : phiml.math._tensors.Tensor- 
Expand source code
@property def volume(self) -> Tensor: return math.prod(self.dx, 'vector')phi.math.Tensorrepresenting the volume of each element. The result retains batch, spatial and instance dimensions. 
Methods
def bounding_half_extent(self) ‑> phiml.math._tensors.Tensor- 
Expand source code
def bounding_half_extent(self) -> Tensor: return self.half_sizeThe 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
ein dimensiond(extent[...,d] = e). Then, no point of the geometry lies further away from its center point thanealongd(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 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': return Box(self.lower, self.upper) def center_representation(self, size_variable=True) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self, size_variable=True) -> 'Cuboid': return Cuboid(self.center, self.half_size, size_variable=size_variable) def corner_representation(self) ‑> phi.geom._box.Box- 
Expand source code
def corner_representation(self) -> 'Box': return Box(self.lower, self.upper) def cuboid(self, size_variable=True) ‑>Cuboid() at 0x7fae3c3f2d40> - 
Expand source code
def center_representation(self, size_variable=True) -> 'Cuboid': return Cuboid(self.center, self.half_size, size_variable=size_variable) 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 
vectorin 3D or higher. - Matrix: d⨯d rotation matrix
 
 - Angle(s): scalar angle in 2d or euler angles along 
 
Returns
Rotated
Geometry def shallow_equals(self, other)- 
Expand source code
def shallow_equals(self, other): return self == otherQuick equality check. May return
Falseeven ifother == self. However, ifTrueis 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)- 
Expand source code
def shifted(self, delta: Tensor, **delta_by_dim): # 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.labels: 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.with_sizes([s*scale for s in self.resolution.sizes]), self.bounds)