pymomentum.axel

Python bindings for Axel library classes including SignedDistanceField.

class pymomentum.axel.BoundingBox

Bases: pybind11_object

__init__(*args, **kwargs)

Overloaded function.

  1. __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).

  1. __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.

  1. __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).

  1. __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.

  1. 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)
  1. 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")