Transformations and Projections#
Geometric Transformations#
Linear and affine transformations on mesh geometry. Each function
returns a new Mesh with transformed point
coordinates and appropriately invalidated caches. Cached quantities such as
normals and areas are automatically recomputed on next access.
All transformations are also available as methods on
Mesh.
import numpy as np
from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
mesh = sphere_icosahedral.load(subdivisions=3)
# Via Mesh methods
translated = mesh.translate([1.0, 0.0, 0.0])
rotated = mesh.rotate(axis=[0, 0, 1], angle=np.pi / 4)
scaled = mesh.scale(2.0)
scaled_aniso = mesh.scale([2.0, 1.0, 0.5])
# Arbitrary linear transform
import torch
matrix = torch.eye(3) * 2
transformed = mesh.transform(matrix)
Linear and affine transformations for simplicial meshes.
This module implements geometric point transformations with intelligent cache handling. By default, all caches are invalidated; transformations explicitly opt in to preserve or update valid cache fields.
Cached fields handled: - areas: point_data and cell_data - normals: point_data and cell_data - centroids: cell_data only
- physicsnemo.mesh.transformations.geometric.rotate(
- mesh: Mesh,
- angle: float,
- axis: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | Literal['x', 'y', 'z'] | None = None,
- center: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | None = None,
- transform_point_data: bool | TensorDict = False,
- transform_cell_data: bool | TensorDict = False,
- transform_global_data: bool | TensorDict = False,
Rotate the mesh about an axis by a specified angle.
Call it as
rotate(mesh, ...)or asmesh.rotate(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Input mesh to rotate.
angle (float) – Rotation angle in radians (counterclockwise, right-hand rule).
axis (Float[torch.Tensor, " n_spatial_dims"] or Sequence[float] or {"x", "y", "z"} or None) – Rotation axis vector.
Nonefor 2D, shape \((3,)\) for 3D. String literals"x","y","z"are converted to unit vectors(1,0,0),(0,1,0),(0,0,1)respectively.center (Float[torch.Tensor, " n_spatial_dims"] or Sequence[float] or None) – Center point for rotation. If
None, rotates about the origin.transform_point_data (bool or TensorDict) – Controls transformation of
point_datafields. Seetransform()for full semantics.transform_cell_data (bool or TensorDict) – Same semantics as
transform_point_data, forcell_data.transform_global_data (bool or TensorDict) – Same semantics as
transform_point_data, forglobal_data.
- Returns:
New Mesh with rotated geometry.
- Return type:
Notes
Cache Handling:
areas: Unchanged (rotation preserves volumes)
centroids: Rotated
normals: Rotated
- physicsnemo.mesh.transformations.geometric.rotation_matrix(
- angle: float,
- axis: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | Literal['x', 'y', 'z'] | None,
- n_spatial_dims: int,
- device: device,
- dtype: dtype,
Build a rotation matrix from angle and axis.
- Parameters:
angle (float) – Rotation angle in radians (counterclockwise, right-hand rule).
axis (Float[torch.Tensor, " n_spatial_dims"] or Sequence[float] or {"x", "y", "z"} or None) – Rotation axis.
Nonefor 2D, tensor/sequence/string for 3D.n_spatial_dims (int) – Number of spatial dimensions.
device (torch.device) – Target device for the output matrix.
dtype (torch.dtype) – Target dtype for the output matrix.
- Returns:
Rotation matrix, shape \((S, S)\).
- Return type:
Float[torch.Tensor, “n_spatial_dims n_spatial_dims”]
- physicsnemo.mesh.transformations.geometric.scale(
- mesh: Mesh,
- factor: float | Float[Tensor, 'n_spatial_dims'] | Sequence[float],
- center: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | None = None,
- transform_point_data: bool | TensorDict = False,
- transform_cell_data: bool | TensorDict = False,
- transform_global_data: bool | TensorDict = False,
- assume_invertible: bool | None = None,
Scale the mesh by specified factor(s).
Call it as
scale(mesh, ...)or asmesh.scale(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Input mesh to scale.
factor (float or Float[torch.Tensor, " n_spatial_dims"] or Sequence[float]) – Scale factor(s). Scalar for uniform, vector for non-uniform.
center (Float[torch.Tensor, " n_spatial_dims"] or Sequence[float] or None) – Center point for scaling. If
None, scales about the origin.transform_point_data (bool or TensorDict) – Controls transformation of
point_datafields. Seetransform()for full semantics.transform_cell_data (bool or TensorDict) – Same semantics as
transform_point_data, forcell_data.transform_global_data (bool or TensorDict) – Same semantics as
transform_point_data, forglobal_data.assume_invertible (bool or None) –
Controls cache propagation:
True: Assume all factors are non-zero, propagate caches (compile-safe)
False: Assume some factor is zero, skip cache propagation (compile-safe)
None: Check determinant at runtime (may cause graph breaks under torch.compile)
- Returns:
New Mesh with scaled geometry.
- Return type:
Notes
Cache Handling:
- areas: Scaled correctly. For non-isotropic transforms of codimension-1
embedded manifolds, per-element scaling is computed using normals.
centroids: Scaled
normals: Transformed by inverse-transpose (direction adjusted, magnitude normalized)
- physicsnemo.mesh.transformations.geometric.scale_matrix(
- factor: float | Float[Tensor, 'n_spatial_dims'] | Sequence[float],
- n_spatial_dims: int,
- device: device,
- dtype: dtype,
Build a diagonal scale matrix from a factor specification.
- Parameters:
factor (float or Float[torch.Tensor, " n_spatial_dims"] or Sequence[float]) – Scale factor(s). Scalar for uniform, vector for non-uniform.
n_spatial_dims (int) – Number of spatial dimensions.
device (torch.device) – Target device for the output matrix.
dtype (torch.dtype) – Target dtype for the output matrix.
- Returns:
Diagonal scale matrix, shape \((S, S)\).
- Return type:
Float[torch.Tensor, “n_spatial_dims n_spatial_dims”]
- Raises:
ValueError – If
factoris a vector whose length does not matchn_spatial_dims.
- physicsnemo.mesh.transformations.geometric.transform(
- mesh: Mesh,
- matrix: Float[Tensor, 'new_n_spatial_dims n_spatial_dims'],
- transform_point_data: bool | TensorDict = False,
- transform_cell_data: bool | TensorDict = False,
- transform_global_data: bool | TensorDict = False,
- assume_invertible: bool | None = None,
Apply a linear transformation to the mesh.
Call it as
transform(mesh, ...)or asmesh.transform(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Input mesh to transform.
matrix (Float[torch.Tensor, "new_n_spatial_dims n_spatial_dims"]) – Transformation matrix, shape \((S', S)\).
transform_point_data (bool or TensorDict) – Controls transformation of
point_datafields.Truetransforms all compatible fields;Falsetransforms none; aTensorDict(ordict) with scalar bool leaves selectively transforms only the named fields.transform_cell_data (bool or TensorDict) – Same semantics as
transform_point_data, forcell_data.transform_global_data (bool or TensorDict) – Same semantics as
transform_point_data, forglobal_data.assume_invertible (bool or None) –
Controls cache propagation for square matrices:
True: assumematrixis invertible and propagate caches (compile-safe). This is a promise, not a check. Ifmatrixis in fact singular, the inverse-transpose step silently yields non-finite values instead of raising, so the propagatednormalsandareascaches – and anything derived from them, such as the sum ofcell_areas– come back as NaN. UseFalseorNoneunless you know the matrix is non-singular.False: assumematrixis singular and skip cache propagation (compile-safe). Caches are dropped and recomputed lazily on demand, which is always correct, just slower.None(default): testabs(det(matrix)) > 1e-10at runtime and take one of the branches above. Safe for singular input, but the test reads a device scalar back to the host, which synchronizes on CUDA and may cause graph breaks undertorch.compile.
- Returns:
New Mesh with transformed geometry and appropriately updated caches.
- Return type:
Notes
Cache Handling:
areas: For square invertible matrices:
Full-dimensional meshes: scaled by
|det|Codimension-1 manifolds: per-element scaling using
|det| * ||M^{-T} n||Higher codimension: invalidated
centroids: Always transformed
normals: For square invertible matrices, transformed by inverse-transpose
- physicsnemo.mesh.transformations.geometric.translate(
- mesh: Mesh,
- offset: Float[Tensor, 'n_spatial_dims'] | Sequence[float],
Apply a translation to the mesh.
Translation only affects point positions and centroids. Vector/tensor fields are unchanged by translation (they represent directions, not positions).
Call it as
translate(mesh, ...)or asmesh.translate(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Input mesh to translate.
offset (Float[torch.Tensor, " n_spatial_dims"] or Sequence[float]) – Translation vector, shape \((S,)\).
- Returns:
New Mesh with translated geometry.
- Return type:
Notes
Cache Handling:
areas: Unchanged
centroids: Translated
normals: Unchanged
Deformations#
The deform namespace provides four deformation families:
Dense displacement through
displace(), backed bydisplace_points().Compact sparse-control morphing through
morph(), backed bymorph_points().Global radial-basis deformation through
radial_basis_function_deform(), backed byradial_basis_function_deform_points().Lattice free-form deformation through
free_form_deform(), backed byfree_form_deform_points().
Each operation is also available as a method on
Mesh.
Dense displacement accepts a tensor or a point-data key (including a nested
tuple key). The operation returns a new mesh without changing mesh.points.
Assigning a point-data key, as in the second example below, is a separate,
explicit mutation of the source mesh’s attached data.
displacement = torch.zeros_like(mesh.points)
displacement[:, 2] = 0.05
displaced = mesh.displace(0.5 * displacement)
# Point-data fields can drive the same operation.
mesh.point_data["design_displacement"] = displacement
displaced_from_data = mesh.displace("design_displacement")
Sparse controls are useful when only a small set of design handles is known. A control point is a location in world coordinates, and its control displacement is a vector rather than a destination coordinate. Control points do not need to be mesh vertices, although selecting vertices makes their prescribed movement directly visible in the result.
Single-Control Morphing#
Indexing one vertex produces a coordinate vector with shape (3,). The
morph API instead expects (n_controls, n_spatial_dims), so
unsqueeze(0) adds the control dimension and gives shape (1, 3). It is
not a batch dimension.
top_index = mesh.points[:, 2].argmax()
control_points = mesh.points[top_index].unsqueeze(0) # (1, 3)
control_displacements = mesh.points.new_tensor(
[[0.0, 0.0, 0.5]], requires_grad=True
)
single_morph = mesh.morph(
control_points,
control_displacements,
radius=1.0,
)
# Autograd continues through the returned point coordinates.
objective = single_morph.points.square().mean()
objective.backward()
control_displacements is differentiable. An optimizer can learn it from any
differentiable loss computed from single_morph.points. A model can also
predict the displacements.
Without point weights, a mesh vertex exactly at a unique control moves by its
prescribed displacement. point_weights can scale or mask the final movement.
Duplicate controls at the same coordinate contribute their mean displacement.
Multiple-Control Morphing#
Advanced indexing retains the control dimension when several vertices are
selected. Each row of control_points pairs with the same row of
control_displacements and, when supplied, one entry of radius.
bottom_index = mesh.points[:, 2].argmin()
control_indices = torch.stack((top_index, bottom_index))
control_points = mesh.points[control_indices] # (2, 3)
control_displacements = mesh.points.new_tensor(
[[0.0, 0.0, 0.5], [0.0, 0.0, -0.5]]
)
radii = mesh.points.new_tensor([1.0, 1.0])
multiple_morph = mesh.morph(
control_points,
control_displacements,
radius=radii,
)
The radius is a Euclidean support distance in the same coordinate units as the mesh. A control’s influence vanishes smoothly at its support boundary. Where supports overlap, all active controls are evaluated together using a stationary zero-displacement background. The result is not a simple sum or average. Points outside every support remain unchanged. Put simultaneous controls in one call, because applying several morphs sequentially evaluates later fields on already modified coordinates and is therefore order-dependent.
The kernel keyword names the compact radial kernel used by the field.
"wendland_c2" is currently the supported value and the default.
Every tensor-valued radius must remain finite and strictly positive. Its values
are not validated at runtime. When a model learns the radius, use a positive
parameterization such as
torch.nn.functional.softplus(raw_radius) + radius_epsilon rather than
optimizing an unconstrained radius directly. Floating point_weights are
used as supplied and may be signed or greater than one.
Visualization
The panels compare the original sphere with the single-control and multiple-control examples above. Green markers identify the displaced handle locations, while arrows and labels show the prescribed displacement directions and magnitudes.
Global Radial-Basis Deformation#
Radial-basis deformation fits one global displacement field through sparse handles. With zero smoothing and a nonsingular control layout, the fitted field interpolates each prescribed control displacement up to solver precision. Optional point weights are applied after interpolation. Unlike compact Shepard morphing, every control generally influences every point. Fixed controls are therefore useful as anchors.
The standard affine polynomial tail reproduces affine displacement fields. The controls must affinely span the coordinate space, and the augmented system must be nonsingular. This formulation follows the thin-plate-spline interpolant described by Bookstein [1].
[1] F. L. Bookstein, “Principal warps: thin-plate splines and the decomposition of deformations,” IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 11, no. 6, pp. 567-585, 1989. https://doi.org/10.1109/34.24792
Two-Dimensional Example
A mesh with n_spatial_dims=2 requires at least three non-collinear controls
when the affine tail is enabled. This example fixes the four corners of a
triangulated square and moves a fifth handle at the midpoint of its upper edge.
import torch
from physicsnemo.mesh.primitives.planar import unit_square
mesh_2d = unit_square.load(subdivisions=4)
controls_2d = mesh_2d.points.new_tensor(
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 1.0]]
)
displacements_2d = torch.zeros_like(controls_2d)
displacements_2d[-1] = controls_2d.new_tensor([0.15, 0.35])
deformed_2d = mesh_2d.radial_basis_function_deform(
controls_2d,
displacements_2d,
kernel="thin_plate_spline",
polynomial=True,
smoothing=0.0,
)
handle_index = torch.linalg.vector_norm(
mesh_2d.points - controls_2d[-1], dim=1
).argmin()
torch.testing.assert_close(
deformed_2d.points[handle_index],
controls_2d[-1] + displacements_2d[-1],
atol=2.0e-5,
rtol=2.0e-5,
)
The corner controls remain fixed while the upper edge and interior deform smoothly. The hollow green marker in the output panel shows the handle’s source position.
Three-Dimensional Example
In three dimensions, the affine tail requires at least four controls spanning the coordinate space. Practical layouts usually use more. The sphere example uses its six axis-extreme vertices as controls.
control_indices = torch.stack(
(
mesh.points[:, 2].argmax(),
mesh.points[:, 2].argmin(),
mesh.points[:, 0].argmax(),
mesh.points[:, 0].argmin(),
mesh.points[:, 1].argmax(),
mesh.points[:, 1].argmin(),
)
)
rbf_controls = mesh.points[control_indices]
rbf_displacements = torch.zeros_like(rbf_controls)
rbf_displacements[0, 2] = 0.55
exact_rbf = mesh.radial_basis_function_deform(
rbf_controls,
rbf_displacements,
kernel="thin_plate_spline",
polynomial=True,
smoothing=0.0,
)
torch.testing.assert_close(
exact_rbf.points[control_indices],
rbf_controls + rbf_displacements,
atol=2.0e-5,
rtol=2.0e-5,
)
smoothing=0.0 interpolates the controls up to solver precision. A positive
smoothing value adds diagonal regularization. This deliberately relaxes
interpolation accuracy.
Both evaluation backends use PyTorch for the dense coefficient solve. The Warp backend fuses evaluation over the mesh points without materializing its full point/control kernel matrix.
Orange controls are fixed anchors. Green controls mark moved handles, and the arrows show their displacement directions. The labels give the prescribed magnitudes. With zero smoothing, the deformed surfaces interpolate all six control displacements up to solver precision.
Lattice Free-Form Deformation#
free_form_deform() defines a regular array of
control displacements over an axis-aligned evaluation box and deforms every
point inside the box by tensor-product basis interpolation.
Compared with sparse morphing, the design parameters form a structured grid of
fixed size, which suits parametric shape optimization. A lattice of zeros is
exactly the identity, and the same lattice deforms any geometry embedded in
the box.
When origin and extent are omitted, the box spans the mesh bounds. Each
coordinate axis must have positive range. Planar or linear geometry embedded
in a higher-dimensional space therefore needs an explicit positive extent.
Validating an automatically derived extent synchronizes with the device and is
not CUDA Graph capture-safe. For capture, pass both origin and extent
as device tensors.
basis="bernstein" provides classic global-support free-form deformation for
coarse lattices. basis="bspline" provides local four-node-per-axis support
and scales to fine lattices for local sculpting. Its first and last coefficient
planes lie one knot spacing outside the evaluation box. basis="linear",
"cubic_hermite", and "quintic_hermite" instead use the two neighboring
nodes per axis and reproduce every control displacement at its lattice node.
For a local cell coordinate \(t\), their upper-node weights are
\(t\), \(3t^2-2t^3\) (cubic Hermite), and
\(6t^5-15t^4+10t^3\) (quintic Hermite), respectively. The lower-node
weight is one minus the upper-node weight. The resulting fields are C0, C1,
and C2 across cell boundaries, respectively. Perlin introduced the quintic
blend in Improving Noise to
eliminate the cubic blend’s second-derivative discontinuities.
# A 4x4x4 Bernstein lattice spans the mesh bounds.
# Zero displacements start at the identity.
control_displacements = torch.zeros(4, 4, 4, 3, requires_grad=True)
deformed = mesh.free_form_deform(control_displacements)
# Autograd continues through the returned point coordinates.
objective = deformed.points.square().mean()
objective.backward()
Points outside the lattice box are unchanged. The deformation is generally not
continuous across the box boundary. A sufficient condition for a fixed
exterior is to zero the outermost coefficient plane on every Bernstein or
node-interpolating face. For cubic B-splines, zero the first and last three
coefficient planes on every axis. origin and extent are
non-differentiable lattice parameters. Optimize control_displacements
instead.
Visualization
The panels compare the original sphere with two lattice deformations. A coarse Bernstein lattice tapers the whole sphere because every node acts globally, while three independently displaced regions in a finer cubic B-spline lattice produce two local bulges and a local indentation.
Domain Meshes#
morph() evaluates one
world-coordinate control field on the interior and every named boundary. With
point_weights=None, coincident component points receive identical motion.
Domain point weights must instead be a point-data key (or nested tuple key)
present in every component. Raw weight tensors are rejected because component
point counts can differ. Every resolved field must use one common dtype across
the domain: bool for a hard mask, or the same floating dtype as the mesh points.
Coincident points remain coincident under a point-weight key only when their
resolved values also match.
radial_basis_function_deform()
follows the same component and point-weight rules while fitting one global RBF
field. The coefficient system is solved once, and the combined interior and
boundary points are evaluated together before the component meshes are rebuilt.
import torch
from physicsnemo.mesh import DomainMesh, Mesh
interior = Mesh(
points=torch.tensor([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]),
cells=torch.tensor([[0, 1, 2]]),
point_data={"design_weight": torch.tensor([1.0, 0.8, 0.5])},
)
wall = Mesh(
points=interior.points[:2],
cells=torch.tensor([[0, 1]]),
point_data={"design_weight": torch.tensor([1.0, 0.8])},
)
domain = DomainMesh(interior=interior, boundaries={"wall": wall})
domain_controls = interior.points[[0]] # (1, 2)
domain_displacements = interior.points.new_tensor([[0.0, 0.25]])
morphed_domain = domain.morph(
domain_controls,
domain_displacements,
radius=1.25,
point_weights="design_weight",
implementation="torch",
)
# Equal point weights keep the shared wall vertices coincident.
assert torch.allclose(
morphed_domain.interior.points[:2],
morphed_domain.boundaries["wall"].points,
)
free_form_deform() follows the
same pattern for lattice free-form deformation:
The operation evaluates one lattice field over the combined interior and boundary points.
The default box spans the combined component bounds.
The combined bounds must have positive range on every coordinate axis unless an explicit extent is supplied.
Every deformation preserves connectivity and attached point, cell, global, and domain data. These operations treat attached vector and tensor fields as Lagrangian data and do not push them forward. They discard geometry-dependent caches and recompute them lazily. They retain topology caches.
Warning
Deformations do not detect or repair inverted, degenerate, or
self-intersecting output cells. Call
validate() or
validate() explicitly when a
deformation could compromise validity.
For optimization-time geometric penalties on a fixed topology, see Differentiable Deformation Energies.
- physicsnemo.mesh.transformations.deform.displace(
- mesh: Mesh,
- displacement: str | tuple[str, ...] | Tensor,
- *,
- point_weights: str | tuple[str, ...] | Tensor | None = None,
- implementation: Literal['torch'] | None = None,
Displace every mesh point by a dense vector field.
Computes
points + displacementwithout changing connectivity, optionally multiplying the displacement bypoint_weights[..., None].displacementandpoint_weightsmay be raw tensors or keys (including nested tuple keys) inpoint_data.Call it as
displace(mesh, ...)or asmesh.displace(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Mesh whose points are displaced. The source mesh is not modified.
displacement (str, tuple[str, ...], or torch.Tensor) – Dense displacement vectors with shape
(mesh.n_points, mesh.n_spatial_dims), or a point-data key resolving to such a tensor. The tensor andmesh.pointsmust have the same float32 or float64 dtype and device.point_weights (str, tuple[str, ...], torch.Tensor, or None, optional) – Optional bool or floating-point weights with shape
(mesh.n_points,), or a point-data key resolving to those point weights. Floating-point weights may be signed or greater than one. Default isNone.implementation ({"torch"} or None, optional) – Backend override.
Noneselects Torch for dense displacement.
- Returns:
New mesh with displaced points and unchanged connectivity and attached fields.
- Return type:
Notes
Attached fields are treated as Lagrangian data and are not pushed forward. Geometry-dependent caches are invalidated and topology caches are retained. The operation does not detect or repair inverted, degenerate, or self-intersecting cells; call
validate()explicitly when needed.
- physicsnemo.mesh.transformations.deform.free_form_deform(
- mesh: Mesh,
- control_displacements: Float[Tensor, '*lattice_resolution n_spatial_dims'],
- *,
- origin: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | None = None,
- extent: Float[Tensor, 'n_spatial_dims'] | Sequence[float] | None = None,
- basis: Literal['bernstein', 'bspline', 'linear', 'cubic_hermite', 'quintic_hermite'] = 'bernstein',
- point_weights: str | tuple[str, ...] | Bool[Tensor, 'n_points'] | Float[Tensor, 'n_points'] | None = None,
- implementation: Literal['torch', 'warp'] | None = None,
Deform a mesh with a control-point lattice by free-form deformation.
An
n_1 x ... x n_Darray of control displacements defines a field over the axis-aligned box[origin, origin + extent]. Mesh points inside the box move with the tensor-product basis interpolation of those values. Points outside the box are unchanged. A lattice of zero displacements is exactly the identity, and a constant lattice translates every point inside the box.Call it as
free_form_deform(mesh, ...)or asmesh.free_form_deform(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Mesh whose points are deformed. The source mesh is not modified.
control_displacements (torch.Tensor) – Displacement vectors, not destination coordinates, for every lattice node, with shape
(n_1, ..., n_D, mesh.n_spatial_dims)and the same float32 or float64 dtype and device asmesh.points. Each axis needs at least two nodes for"bernstein"and the node-interpolating bases, and four for"bspline".origin (torch.Tensor, sequence of float, or None, optional) – Minimum corner of the lattice box with shape
(mesh.n_spatial_dims,).Noneuses the minimum corner of the mesh bounds. For repeated GPU calls with an explicit box, createoriginandextentonce as device tensors. Reuse them to avoid recreating and transferring sequence values. Default isNone.extent (torch.Tensor, sequence of float, or None, optional) – Edge lengths of the lattice box with the same accepted shapes as
origin. Every value must be finite and strictly positive. The operation does not validate tensor values at runtime.Nonesizes the box fromoriginto the maximum corner of the mesh bounds. Validating a derived extent synchronizes with the device and is not CUDA Graph capture-safe. For capture, pass bothoriginandextentas device tensors. Every point-coordinate axis must have positive range when the extent is derived. Supply an explicit extent for lower-dimensional geometry embedded in a higher-dimensional space. Default isNone.basis ({"bernstein", "bspline", "linear", "cubic_hermite", "quintic_hermite"}, optional) –
Per-axis basis family:
"bernstein"provides classic global-support FFD. Every lattice node influences every point inside the box."bspline"uses a uniform cubic B-spline with local four-node-per-axis support and C2 continuity between knot spans. Coefficient indexicorresponds to local coordinate(i - 1) / (n - 3). The first and last coefficient planes lie outside the evaluation box."linear"uses upper-node weight \(s(t)=t\) within each lattice cell. It interpolates every node. It is continuous (C0) across cell boundaries, but its slope can jump."cubic_hermite"uses the cubic Hermite blend \(s(t)=3t^2-2t^3\). Its first derivative vanishes at both cell endpoints. This gives C1 continuity across cell boundaries."quintic_hermite"uses the quintic Hermite blend \(s(t)=6t^5-15t^4+10t^3\). Its first and second derivatives vanish at both endpoints. This gives C2 continuity across cell boundaries. Perlin introduced this improved interpolant in “Improving Noise” [1].
The node-interpolating bases use only the two neighboring nodes per axis. Here,
tis the local cell coordinate in[0, 1]. The upper-node weight is \(s(t)\), and the lower-node weight is \(1-s(t)\). Default is"bernstein".point_weights (str, tuple[str, ...], torch.Tensor, or None, optional) – Optional bool or floating mesh-point weights with shape
(mesh.n_points,), or apoint_datakey resolving to those point weights. All weights must match the point device. Floating weights must also match the point dtype. Default isNone.implementation ({"torch", "warp"} or None, optional) – Backend override.
Noneselects Torch on CPU. On CUDA, it selects Warp when available and otherwise Torch.
- Returns:
New mesh with deformed points and unchanged connectivity and fields.
- Return type:
- Raises:
TypeError – If tensors, lattice values, or point weights have unsupported types or dtypes.
ValueError – If shapes, devices, lattice parameters, point weights, or
basisare invalid.KeyError – If a point-data key or
implementationname is not found.ImportError – If an explicitly requested backend is unavailable.
Notes
The operation treats attached fields as Lagrangian data and does not push them forward. It invalidates geometry-dependent caches and retains topology caches. The deformation is generally not continuous across the lattice box boundary. To keep the exterior fixed, zero the outermost coefficient plane on every Bernstein or node-interpolating face. For cubic B-splines, zero the first and last three coefficient planes on every axis.
originandextentare non-differentiable lattice parameters. Optimizecontrol_displacementsinstead. The operation does not detect or repair inverted, degenerate, or self-intersecting cells. Callvalidate()explicitly when needed.References
[1] Perlin, K. (2002). “Improving Noise.” ACM Transactions on Graphics, 21(3), 681-682. https://doi.org/10.1145/566654.566636
- physicsnemo.mesh.transformations.deform.morph(
- mesh: Mesh,
- control_points: Tensor,
- control_displacements: Tensor,
- *,
- radius: float | Tensor,
- point_weights: str | tuple[str, ...] | Tensor | None = None,
- kernel: Literal['wendland_c2'] = 'wendland_c2',
- implementation: Literal['torch', 'warp'] | None = None,
Morph a mesh from sparse, compactly supported control displacements.
Control influence uses a Wendland-C2 compact Shepard field with a stationary zero-displacement background. Each control’s influence vanishes smoothly at its support boundary. Where supports overlap, all active controls and the background are blended together; the result is not a simple sum or average. The field is zero outside the union of all supports.
With no point weights, the field at a unique control coordinate is exactly that control’s displacement. Duplicate controls at one coordinate contribute their mean displacement. A control may be anywhere in world coordinates and need not coincide with a mesh point.
Call it as
morph(mesh, ...)or asmesh.morph(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Mesh whose points are morphed. The source mesh is not modified.
control_points (torch.Tensor) – World-coordinate controls with shape
(n_controls, mesh.n_spatial_dims)and the same dtype and device asmesh.points.control_displacements (torch.Tensor) – Displacement vectors, not destination coordinates, with exactly the same shape, dtype, and device as
control_points.radius (float or torch.Tensor) – Support distance in mesh coordinate units. Supply one scalar for every control or a tensor with shape
(n_controls,)that matches the control dtype and device. Every tensor value must remain positive and finite; values are not validated at runtime.point_weights (str, tuple[str, ...], torch.Tensor, or None, optional) – Optional bool or floating mesh-point weights with shape
(mesh.n_points,), or apoint_datakey resolving to those point weights. These are query-point weights, not per-control values. Default isNone.kernel ({"wendland_c2"}, optional) – Compact radial kernel used to blend control displacements. Default is
"wendland_c2".implementation ({"torch", "warp"} or None, optional) – Backend override.
Noneselects Torch on CPU and Warp on CUDA when Warp is available, otherwise Torch.
- Returns:
New mesh with morphed points and unchanged connectivity and attached fields.
- Return type:
Notes
Attached fields are treated as Lagrangian data and are not pushed forward. Geometry-dependent caches are invalidated and topology caches are retained. Parameterize learned radii to remain positive, for example as
torch.nn.functional.softplus(raw_radius) + eps. The operation does not detect or repair inverted, degenerate, or self-intersecting cells; callvalidate()explicitly when needed.
- physicsnemo.mesh.transformations.deform.radial_basis_function_deform(
- mesh: Mesh,
- control_points: Float[Tensor, 'n_controls n_spatial_dims'],
- control_displacements: Float[Tensor, 'n_controls n_spatial_dims'],
- *,
- kernel: Literal['thin_plate_spline'] = 'thin_plate_spline',
- polynomial: bool = True,
- smoothing: float = 0.0,
- point_weights: str | tuple[str, ...] | Bool[Tensor, 'n_points'] | Float[Tensor, 'n_points'] | None = None,
- implementation: Literal['torch', 'warp'] | None = None,
Deform a mesh with a global thin-plate-spline RBF field.
A thin-plate-spline radial field is fitted to the prescribed sparse control displacements and evaluated at every mesh point. With the default affine polynomial tail, zero smoothing, and a nonsingular control layout, the unweighted field interpolates every control displacement up to solver precision.
Call it as
radial_basis_function_deform(mesh, ...)or asmesh.radial_basis_function_deform(...). The bound method suppliesmeshautomatically.- Parameters:
mesh (Mesh) – Mesh whose points are deformed. The source mesh is not modified.
control_points (torch.Tensor) – World-coordinate controls with shape
(n_controls, mesh.n_spatial_dims)and the same dtype and device asmesh.points.control_displacements (torch.Tensor) – Displacement vectors, not destination coordinates, with exactly the same shape, dtype, and device as
control_points.kernel ({"thin_plate_spline"}, optional) – Radial kernel used by the interpolant. Default is
"thin_plate_spline".polynomial (bool, optional) – Add the standard affine polynomial tail and side constraints. This reproduces affine displacement fields. The controls must affinely span the coordinate space, and the augmented system must be nonsingular. Default is
True.smoothing (float, optional) – Nonnegative diagonal regularization added to the radial system. Zero gives exact interpolation for a nonsingular control layout up to solver precision. Positive values relax interpolation accuracy. Default is
0.0.point_weights (str, tuple[str, ...], torch.Tensor, or None, optional) – Optional bool or floating mesh-point weights with shape
(mesh.n_points,), or apoint_datakey resolving to those weights. Values scale or mask the fitted field after interpolation. Bool weights must be on the same device as the mesh points. Floating weights must have the same dtype and device as the mesh points.implementation ({"torch", "warp"} or None, optional) – Evaluation-backend override. Both backends use PyTorch for the dense coefficient solve.
Noneselects Torch on CPU and Warp on CUDA when Warp is available, otherwise Torch.
- Returns:
New mesh with deformed points and unchanged connectivity and attached fields.
- Return type:
- Raises:
TypeError – If control tensors or Python arguments have unsupported types, or if tensor dtypes are unsupported or mismatched.
ValueError – If tensor shapes, devices, control layout, point weights, or RBF options are invalid.
KeyError – If a point-data key is missing or
implementationdoes not name a registered backend.ImportError – If an explicitly requested backend is unavailable.
RuntimeError – If runtime validation or coefficient fitting fails, including for a singular system or during CUDA Graph capture.
Notes
The field has global support. Unlike compact Shepard morphing, every control generally influences every mesh point. Attached fields are treated as Lagrangian data and are not pushed forward. Geometry-dependent caches are invalidated and topology caches are retained. The operation does not detect or repair inverted, degenerate, or self-intersecting cells. Call
validate()explicitly when needed. Coefficient fitting is not supported inside CUDA Graph capture because the singular-system check requires host interaction.
Projections#
Spatial dimension manipulation – changing the embedding dimension of a mesh without altering its manifold dimension.
embed()– add spatial dimensions (non-destructive; for example, 2D mesh to 3D by appending zero coordinates)extrude()– sweep a manifold to create a mesh one dimension higher (for example, a triangle mesh extruded to a prism mesh)project()– reduce spatial dimensions (lossy; drops coordinate axes)
Projection operations for mesh extrusion, embedding, and spatial dimension manipulation.
This module provides functionality for: - Embedding meshes in higher-dimensional spaces (non-destructive) - Projecting meshes to lower-dimensional spaces (lossy) - Extruding manifolds to higher dimensions