pymomentum.axel
Python bindings for Axel library classes including SignedDistanceField.
- class pymomentum.axel.BoundingBox
Bases:
pybind11_object
- __init__(*args, **kwargs)
Overloaded function.
__init__(self: pymomentum.axel.BoundingBox, min_corner: numpy.ndarray[numpy.float32[3, 1]], max_corner: numpy.ndarray[numpy.float32[3, 1]], id: int = 0) -> None
Create a bounding box from minimum and maximum corners.
- Parameters:
min_corner – Minimum corner of the bounding box (x, y, z).
max_corner – Maximum corner of the bounding box (x, y, z).
id – Optional ID for the bounding box (default: 0).
__init__(self: pymomentum.axel.BoundingBox, center: numpy.ndarray[numpy.float32[3, 1]], thickness: float = 0.0) -> None
Create a bounding box centered at a point with given thickness.
- Parameters:
center – Center point of the bounding box (x, y, z).
thickness – Half-width in each dimension (default: 0.0).
- property center
Get the center of the bounding box.
- contains(self: pymomentum.axel.BoundingBox, point: numpy.ndarray[numpy.float32[3, 1]]) bool
Check if a point is contained within the bounding box.
- Parameters:
point – Point to test (x, y, z).
- Returns:
True if the point is inside the bounding box.
- extend(self: pymomentum.axel.BoundingBox, point: numpy.ndarray[numpy.float32[3, 1]]) None
Extend the bounding box to include a point.
- Parameters:
point – Point to include (x, y, z).
- property max
Get the maximum corner of the bounding box.
- property min
Get the minimum corner of the bounding box.
- class pymomentum.axel.MeshToSdfConfig
Bases:
pybind11_object
- __init__(self: pymomentum.axel.MeshToSdfConfig) None
Create MeshToSdfConfig with default parameters.
- property max_distance
Maximum distance to compute (distances beyond this are clamped). Set to 0 to disable clamping. Default: 0
- property narrow_band_width
Narrow band width around triangles (in voxel units). Default: 1.5
- property tolerance
Numerical tolerance for computations. Default: machine epsilon * 1000
- class pymomentum.axel.SignedDistanceField
Bases:
pybind11_object
- __init__(*args, **kwargs)
Overloaded function.
__init__(self: pymomentum.axel.SignedDistanceField, bounds: pymomentum.axel.BoundingBox, resolution: numpy.ndarray[numpy.int32[3, 1]], initial_value: float = 3.4028234663852886e+38) -> None
Create a signed distance field with given bounds and resolution.
- Parameters:
bounds – 3D bounding box defining the spatial extent of the SDF.
resolution – Grid resolution in each dimension (nx, ny, nz).
initial_value – Initial distance value for all voxels (default: very far distance).
__init__(self: pymomentum.axel.SignedDistanceField, bounds: pymomentum.axel.BoundingBox, resolution: numpy.ndarray[numpy.int32[3, 1]], data: list[float]) -> None
Create a signed distance field with given bounds, resolution, and initial data.
- Parameters:
bounds – 3D bounding box defining the spatial extent of the SDF.
resolution – Grid resolution in each dimension (nx, ny, nz).
data – Initial distance values. Must have size nx * ny * nz.
- property bounds
Get the bounding box of the SDF.
- fill(self: pymomentum.axel.SignedDistanceField, value: float) None
Fill the entire SDF with a constant value.
- Parameters:
value – The value to fill with.
- gradient(self: pymomentum.axel.SignedDistanceField, positions: numpy.ndarray[numpy.float32]) numpy.ndarray[numpy.float32]
Sample the SDF gradient at continuous 3D positions.
Supports both single position and batch operations: - Single position: Pass 1D array of shape (3,) to get a 1D array of shape (3,) - Batch positions: Pass 2D array of shape (N, 3) to get 2D array of shape (N, 3)
The gradient points in the direction of increasing distance.
- Parameters:
positions – Position(s) to query. Either (3,) for single position or (N, 3) for batch of positions.
- Returns:
Gradient vector(s) at the given position(s). Shape (3,) for single position, (N, 3) for batch.
- grid_to_world(self: pymomentum.axel.SignedDistanceField, grid_pos: numpy.ndarray[numpy.float32[3, 1]]) numpy.ndarray[numpy.float32[3, 1]]
Convert continuous grid coordinates to 3D world-space position.
- Parameters:
grid_pos – Continuous grid coordinates.
- Returns:
3D world-space position (x, y, z).
- is_valid_index(self: pymomentum.axel.SignedDistanceField, i: int, j: int, k: int) bool
Check if the given grid coordinates are within bounds.
- Parameters:
i – Grid index in x dimension.
j – Grid index in y dimension.
k – Grid index in z dimension.
- Returns:
True if indices are within valid range.
- property resolution
Get the grid resolution as (nx, ny, nz).
- sample(self: pymomentum.axel.SignedDistanceField, positions: numpy.ndarray[numpy.float32]) numpy.ndarray[numpy.float32]
Sample the SDF at continuous 3D positions using trilinear interpolation.
Supports both single position and batch operations: - Single position: Pass 1D array of shape (3,) to get a scalar result - Batch positions: Pass 2D array of shape (N, 3) to get 1D array of N results
- Parameters:
positions – Position(s) to query. Either (3,) for single position or (N, 3) for batch of positions.
- Returns:
Interpolated signed distance value(s). Scalar for single position, 1D array for batch.
- sample_with_gradient(self: pymomentum.axel.SignedDistanceField, positions: numpy.ndarray[numpy.float32]) tuple
Sample both the SDF value and gradient at continuous 3D positions.
Supports both single position and batch operations: - Single position: Pass 1D array of shape (3,) to get tuple of (scalar, 1D array of shape (3,)) - Batch positions: Pass 2D array of shape (N, 3) to get tuple of (1D array of N values, 2D array of shape (N, 3))
More efficient than calling sample() and gradient() separately.
- Parameters:
positions – Position(s) to query. Either (3,) for single position or (N, 3) for batch of positions.
- Returns:
Tuple of (value(s), gradient(s)) at the given position(s).
- property total_voxels
Get the total number of voxels in the SDF.
- property voxel_size
Get the voxel size in each dimension as (dx, dy, dz).
- world_to_grid(self: pymomentum.axel.SignedDistanceField, position: numpy.ndarray[numpy.float32[3, 1]]) numpy.ndarray[numpy.float32[3, 1]]
Convert a 3D world-space position to continuous grid coordinates.
- Parameters:
position – 3D world-space position (x, y, z).
- Returns:
Continuous grid coordinates (may be fractional).
- pymomentum.axel.dual_contouring(sdf: pymomentum.axel.SignedDistanceField, isovalue: float = 0.0, triangulate: bool = False) tuple
Extract an isosurface from a signed distance field using dual contouring.
Dual contouring places vertices inside grid cells and generates quad faces between adjacent cells that both contain vertices. This naturally produces quads rather than triangles, which better preserves surface topology and reduces mesh artifacts.
The algorithm works by: 1. Finding all cells that intersect the isosurface (sign changes across cell corners) 2. Placing one vertex at each intersecting cell, positioned on the surface using gradient descent 3. Generating quads for each edge crossing that connects 4 adjacent cells
- Parameters:
sdf – The
SignedDistanceField
to extract the isosurface from.isovalue – The isovalue to extract (typically 0.0 for zero level set). Default: 0.0
triangulate – Whether to triangulate the quads (default: False).
- Returns:
Tuple of (vertices, normals, quads) where: - vertices: 2D array of shape (N, 3) with vertex positions - normals: 2D array of shape (N, 3) with vertex normals (computed from SDF gradients) - quads: Quad indices of shape (M, 4) connecting the vertices
Example usage:
import numpy as np import pymomentum.axel as axel # Create a sphere SDF bounds = axel.BoundingBox( min_corner=np.array([-2.0, -2.0, -2.0]), max_corner=np.array([2.0, 2.0, 2.0]) ) resolution = np.array([32, 32, 32]) sdf = axel.SignedDistanceField(bounds, resolution) # Fill with sphere distance values for k in range(resolution[2]): for j in range(resolution[1]): for i in range(resolution[0]): grid_pos = np.array([i, j, k], dtype=np.float32) world_pos = sdf.grid_to_world(grid_pos) distance = np.linalg.norm(world_pos) - 1.0 # Unit sphere sdf.set(i, j, k, distance) # Extract mesh (always returns quads) vertices, normals, quads = axel.dual_contouring(sdf, isovalue=0.0) print(f"Extracted {len(vertices)} vertices, {len(quads)} quads") print(f"Vertex normals have shape: {normals.shape}")
- pymomentum.axel.fill_holes(vertices: numpy.ndarray[numpy.float32], triangles: numpy.ndarray[numpy.int32]) tuple
Fill holes in a triangle mesh to create a watertight surface.
This function identifies holes in the mesh and fills them with new triangles using an advancing front method. The result is a complete mesh suitable for operations that require watertight surfaces, such as SDF generation.
For small holes (≤6 vertices), a centroid-based fan triangulation is used. For larger holes, an ear clipping algorithm is applied.
- Parameters:
vertices – Vertex positions as 2D array of shape (N, 3) where N is number of vertices.
triangles – Triangle indices as 2D array of shape (M, 3) where M is number of triangles. Indices must be valid within the vertices array.
config – Configuration parameters as
MeshHoleFillingConfig
(optional).
- Returns:
Tuple of (filled_vertices, filled_triangles) where: - filled_vertices: 2D array of shape (N’, 3) with original + new vertices - filled_triangles: 2D array of shape (M’, 3) with original + new triangles
Example usage:
import numpy as np import pymomentum.axel as axel # Create a cube mesh with a missing face (hole) vertices = np.array([ [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], # bottom face [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1] # top face ], dtype=np.float32) # Missing top face triangles to create a hole triangles = np.array([ [0, 1, 2], [0, 2, 3], # bottom face # [4, 7, 6], [4, 6, 5], # top face (missing - creates hole) [0, 4, 5], [0, 5, 1], # front face [2, 6, 7], [2, 7, 3], # back face [0, 3, 7], [0, 7, 4], # left face [1, 5, 6], [1, 6, 2] # right face ], dtype=np.int32) config = axel.MeshHoleFillingConfig() config.max_edge_length_ratio = 2.0 config.smoothing_iterations = 3 filled_vertices, filled_triangles = axel.fill_holes(vertices, triangles, config) print(f"Original mesh: {len(vertices)} vertices, {len(triangles)} triangles") print(f"Filled mesh: {len(filled_vertices)} vertices, {len(filled_triangles)} triangles")
- pymomentum.axel.mesh_to_sdf(*args, **kwargs)
Overloaded function.
mesh_to_sdf(vertices: numpy.ndarray[numpy.float32], triangles: numpy.ndarray[numpy.int32], bounds: pymomentum.axel.BoundingBox, resolution: numpy.ndarray[numpy.int32], config: pymomentum.axel.MeshToSdfConfig = MeshToSdfConfig(narrow_band_width=1.500, max_distance=0.000, tolerance=1.192093e-04)) -> pymomentum.axel.SignedDistanceField
Convert a triangle mesh to a signed distance field using modern 3-step approach.
This function creates a high-quality signed distance field from a triangle mesh using: 1. Narrow band initialization with exact triangle distances 2. Fast marching propagation using Eikonal equation 3. Sign determination using ray casting
- Parameters:
vertices – Vertex positions as 2D array of shape (N, 3) where N is number of vertices.
triangles – Triangle indices as 2D array of shape (M, 3) where M is number of triangles. Indices must be valid within the vertices array.
bounds – Spatial bounds for the SDF as a
BoundingBox
.resolution – Grid resolution as 1D array of shape (3,) containing (nx, ny, nz).
config – Configuration parameters as
MeshToSdfConfig
(optional).
- Returns:
Generated
SignedDistanceField
.
Example usage:
import numpy as np import pymomentum.axel as axel # Create a simple cube mesh vertices = np.array([ [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], # bottom face [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1] # top face ], dtype=np.float32) triangles = np.array([ [0, 1, 2], [0, 2, 3], # bottom face [4, 7, 6], [4, 6, 5], # top face [0, 4, 5], [0, 5, 1], # front face [2, 6, 7], [2, 7, 3], # back face [0, 3, 7], [0, 7, 4], # left face [1, 5, 6], [1, 6, 2] # right face ], dtype=np.int32) bounds = axel.BoundingBox( min_corner=np.array([-1.5, -1.5, -1.5]), max_corner=np.array([1.5, 1.5, 1.5]) ) resolution = np.array([32, 32, 32]) config = axel.MeshToSdfConfig() config.narrow_band_width = 3.0 sdf = axel.mesh_to_sdf(vertices, triangles, bounds, resolution, config)
mesh_to_sdf(vertices: numpy.ndarray[numpy.float32], triangles: numpy.ndarray[numpy.int32], resolution: numpy.ndarray[numpy.int32], padding: float = 0.10000000149011612, config: pymomentum.axel.MeshToSdfConfig = MeshToSdfConfig(narrow_band_width=1.500, max_distance=0.000, tolerance=1.192093e-04)) -> pymomentum.axel.SignedDistanceField
Convert a triangle mesh to a signed distance field with automatic bounds computation.
This convenience function automatically computes the bounding box from the mesh vertices, adds padding, and creates a signed distance field.
- Parameters:
vertices – Vertex positions as 2D array of shape (N, 3) where N is number of vertices.
triangles – Triangle indices as 2D array of shape (M, 3) where M is number of triangles.
resolution – Grid resolution as 1D array of shape (3,) containing (nx, ny, nz).
padding – Extra space around mesh bounds as fraction of bounding box size (default: 0.1).
config – Configuration parameters as
MeshToSdfConfig
(optional).
- Returns:
Generated
SignedDistanceField
.
Example usage:
import numpy as np import pymomentum.axel as axel # Create a simple tetrahedron mesh vertices = np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [0.5, 0.5, 1.0] ], dtype=np.float32) triangles = np.array([ [0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2] ], dtype=np.int32) resolution = np.array([32, 32, 32]) # Automatically compute bounds with 20% padding sdf = axel.mesh_to_sdf(vertices, triangles, resolution, padding=0.2)
- pymomentum.axel.smooth_mesh_laplacian(vertices: numpy.ndarray[numpy.float32], faces: numpy.ndarray[numpy.int32], vertex_mask: numpy.ndarray[bool] = array([], dtype=bool), iterations: int = 1, step: float = 0.5) numpy.ndarray[numpy.float32]
Smooth a triangle or quad mesh using Laplacian smoothing with optional vertex masking.
This function applies Laplacian smoothing to mesh vertices, where each vertex is moved toward the average position of its neighboring vertices. Optionally, you can specify which vertices to smooth using a boolean mask. Both triangle and quad meshes are supported automatically based on the shape of the faces array.
The smoothing is applied iteratively using the formula: new_position = (1 - step) * old_position + step * average_neighbor_position
- Parameters:
vertices – Vertex positions as 2D array of shape (N, 3) where N is number of vertices.
faces – Face indices as 2D array of shape (M, 3) for triangles or (M, 4) for quads. Indices must be valid within the vertices array.
vertex_mask – Optional boolean mask of shape (N,) indicating which vertices to smooth. If empty array or not provided, all vertices will be smoothed.
iterations – Number of smoothing iterations to apply (default: 1).
step – Smoothing step size between 0.0 and 1.0 (default: 0.5). Smaller values preserve original shape better, larger values smooth more aggressively.
- Returns:
Smoothed vertex positions as 2D array of shape (N, 3).
Example usage:
import numpy as np import pymomentum.axel as axel # Example 1: Triangle mesh tri_vertices = np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [0.5, 0.0, 1.0] ], dtype=np.float32) tri_faces = np.array([ [0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2] ], dtype=np.int32) # Smooth all vertices of triangle mesh smoothed_tri = axel.smooth_mesh_laplacian( tri_vertices, tri_faces, np.array([]), iterations=5, step=0.3) # Example 2: Quad mesh quad_vertices = np.array([ [0.0, 0.0, 0.0], # 0 [1.0, 0.0, 0.0], # 1 [1.0, 1.0, 0.0], # 2 [0.0, 1.0, 0.0], # 3 [0.0, 0.0, 1.0], # 4 [1.0, 0.0, 1.0], # 5 [1.0, 1.0, 1.0], # 6 [0.0, 1.0, 1.0] # 7 ], dtype=np.float32) quad_faces = np.array([ [0, 1, 2, 3], # Bottom face [4, 7, 6, 5], # Top face [0, 4, 5, 1], # Front face [2, 6, 7, 3], # Back face [0, 3, 7, 4], # Left face [1, 5, 6, 2] # Right face ], dtype=np.int32) # Smooth only internal vertices (exclude corners) vertex_mask = np.array([False, True, True, False, False, True, True, False]) smoothed_quad = axel.smooth_mesh_laplacian( quad_vertices, quad_faces, vertex_mask, iterations=3, step=0.5)
- pymomentum.axel.triangulate_quads(quads: numpy.ndarray[numpy.int32]) numpy.ndarray[numpy.int32]
Triangulate a quad mesh into triangles.
Each quad is split into two triangles using the diagonal (0,2). This converts a quad mesh (as produced by dual contouring) into a triangle mesh suitable for rendering or processing with triangle-based algorithms.
- Parameters:
quads – Quad indices as 2D array of shape (M, 4) where M is number of quads. Each row contains 4 vertex indices defining a quad.
- Returns:
Triangle indices as 2D array of shape (2M, 3) where each quad produces 2 triangles.
Example usage:
import numpy as np import pymomentum.axel as axel # Get quads from dual contouring vertices, normals, quads = axel.dual_contouring(sdf, config) # Convert to triangles if needed for rendering triangles = axel.triangulate_quads(quads) print(f"Converted {len(quads)} quads to {len(triangles)} triangles")