Skip to content

Lagrangian Motion

This page documents the Lagrangian mesh motion functions in the mmgpy.lagrangian module and the dataset.mmg.move(...) accessor method.

Overview

Lagrangian remeshing handles moving meshes by:

  1. Applying a displacement field to the mesh
  2. Remeshing to maintain quality
  3. Preserving boundary conditions

This is useful for:

  • Moving mesh simulations
  • Shape optimization
  • Fluid-structure interaction
  • Morphing between shapes

dataset.mmg.move() applies the displacement and remeshes for every mesh kind (TET, 2D, surface) with no external dependency. For interior propagation it ships two solvers: a pure-Python Laplacian smoother (default) and an optional fedoo-backed linear elasticity solver, selectable via propagation_method.

import numpy as np
import pyvista as pv
import mmgpy  # noqa: F401  -- registers reader/writer + accessor

sphere = pv.Sphere(theta_resolution=10, phi_resolution=10)
displacement = np.zeros((sphere.n_points, 3))
displacement[:, 0] = 0.1  # Move in x direction

moved = sphere.mmg.move(displacement, hausd=0.01)

Functions

mmgpy.move_mesh

move_mesh(
    mesh: MmgMesh2D | MmgMesh3D | MmgMeshS | Mesh,
    displacement: NDArray[float64],
    *,
    boundary_mask: NDArray[bool_] | None = None,
    propagate: bool = True,
    propagation_method: str = "laplacian",
    n_steps: int = 1,
    **remesh_options: float | bool | None,
) -> None

Move mesh vertices by displacement and remesh to maintain quality.

For large displacements, consider using multiple steps (n_steps > 1) to avoid mesh inversion.

Parameters

mesh : Mesh or MmgMesh2D or MmgMesh3D or MmgMeshS The mesh to deform in place. displacement : ndarray Nxdim array of displacement vectors for each vertex. If boundary_mask is provided and propagate=True, only boundary values need to be correct; interior values will be computed. boundary_mask : ndarray of bool, optional Boolean array indicating which vertices have prescribed displacement. If None, all vertices are treated as having prescribed displacement (no propagation needed). propagate : bool If True and boundary_mask is provided, propagate boundary displacement to interior using the chosen propagation_method. propagation_method : str Method for propagating boundary displacements to the interior. Options:

- ``"laplacian"`` (default): Solves the Laplace equation. Fast,
  no extra dependencies.
- ``"elasticity"``: Solves a linear elasticity problem using
  `fedoo <https://github.com/3MAH/fedoo>`_. Produces physically
  meaningful displacements, better for large deformations and
  complex geometries. Requires ``pip install fedoo``.

n_steps : int Number of incremental steps to apply the displacement. Use more steps for large displacements to avoid mesh inversion. **remesh_options Options passed to mesh.remesh() (hmax, hmin, etc.).

Raises

ValueError If displacement dimensions don't match the mesh, or propagation_method is not recognized.

options: show_root_heading: true

mmgpy.propagate_displacement

propagate_displacement(
    vertices: NDArray[float64],
    elements: NDArray[int32],
    boundary_mask: NDArray[bool_],
    boundary_displacement: NDArray[float64],
) -> NDArray[np.float64]

Propagate displacement from boundary to interior using Laplacian smoothing.

Solves the Laplace equation nabla^2 u = 0 with Dirichlet boundary conditions u = boundary_displacement on the boundary. This produces a smooth displacement field that transitions from boundary values to interior.

The complexity is O(n) for building the matrix and typically O(n^1.5) for solving due to the sparse structure.

Parameters

vertices : ndarray Nx2 or Nx3 array of vertex coordinates. elements : ndarray Mx(nodes_per_element) array of element connectivity. boundary_mask : ndarray of bool N boolean array, True for vertices with prescribed displacement. boundary_displacement : ndarray Nxdim array of displacement vectors. Only values at boundary vertices (where boundary_mask is True) are used.

Returns

ndarray Nxdim array of displacement for all vertices.

Raises

ValueError If array dimensions don't match.

options: show_root_heading: true

mmgpy.lagrangian.propagate_displacement_elasticity

propagate_displacement_elasticity(
    vertices: NDArray[float64],
    elements: NDArray[int32],
    boundary_mask: NDArray[bool_],
    boundary_displacement: NDArray[float64],
    *,
    youngs_modulus: float = 1000000.0,
    nu: float = 0.3,
) -> NDArray[np.float64]

Propagate displacement from boundary to interior using linear elasticity.

Solves a fictitious linear elasticity problem with prescribed displacements on the vertices flagged in boundary_mask; vertices outside the mask are free DOFs whose displacement is computed by the elasticity solve. This produces a physically meaningful smooth field, superior to Laplacian smoothing for large deformations and complex geometries.

Requires the fedoo package (optional dependency).

Parameters

vertices : ndarray Nx2 or Nx3 array of vertex coordinates. elements : ndarray Mx(nodes_per_element) array of element connectivity. boundary_mask : ndarray of bool N boolean array, True for vertices with prescribed displacement. boundary_displacement : ndarray Nxdim array of displacement vectors. Only values at boundary vertices (where boundary_mask is True) are used. youngs_modulus : float Young's modulus for the fictitious elastic material. With only Dirichlet BCs (no body forces, no tractions) the displacement field is independent of this value; only nu affects the result. Default 1e6. nu : float Poisson's ratio. Default is 0.3.

Returns

ndarray Nxdim array of displacement for all vertices.

Raises

ValueError If array dimensions don't match or the element type is unsupported.

options: show_root_heading: true

mmgpy.detect_boundary_vertices

detect_boundary_vertices(
    mesh: MmgMesh2D | MmgMesh3D | MmgMeshS | Mesh,
) -> NDArray[np.bool_]

Detect boundary vertices in a mesh.

Boundary vertices are those that lie on the exterior surface of the mesh. For 3D meshes, these are vertices on surface triangles. For 2D/surface meshes, these are vertices on boundary edges.

Parameters

mesh : Mesh or MmgMesh2D or MmgMesh3D or MmgMeshS The mesh whose boundary vertices to detect.

Returns

ndarray of bool Boolean array of length n_vertices, True for boundary vertices.

options: show_root_heading: true

propagate_displacement_elasticity requires the optional fedoo backend (pip install "mmgpy[fem]"). On a cantilever bracket the Laplacian solver pivots the geometry rigidly while the elasticity solver captures the bending kinematics:

Laplacian vs elasticity on a 2D L-bracket Laplacian vs elasticity on a 3D L-bracket

See the Elasticity Propagation tutorial for a full walkthrough.

fedoo on conda-forge

fedoo is published on PyPI but not on conda-forge. If you installed mmgpy from conda-forge, install fedoo separately with pip install fedoo (or conda install -c set3MAH fedoo) inside the same environment to enable elasticity propagation.

Deprecated: remesh_lagrangian

Mesh.remesh_lagrangian(), dataset.mmg.remesh_lagrangian() and mmgpy.progress.remesh_mesh_lagrangian() are kept as deprecation shims that forward to mmgpy.move_mesh / dataset.mmg.move. They emit a DeprecationWarning and will be removed in a future release; new code should call the move/move_mesh API directly.

Accessor Method

import numpy as np
import pyvista as pv
import mmgpy  # noqa: F401

mesh = pv.read("input.mesh")

# Define displacement field (3D vector at each vertex)
displacement = np.zeros((mesh.n_points, 3))
displacement[:, 0] = 0.1

moved = mesh.mmg.move(displacement)

Usage Examples

Basic Lagrangian Remeshing

import numpy as np
import pyvista as pv
import mmgpy  # noqa: F401

mesh = pv.read("input.mesh")
vertices = np.asarray(mesh.points)

# Create displacement: radial expansion
center = vertices.mean(axis=0)
directions = vertices - center
distances = np.linalg.norm(directions, axis=1, keepdims=True)
directions = directions / (distances + 1e-10)
displacement = directions * 0.1 * distances

remeshed = mesh.mmg.move(displacement)
print(f"Cells: {mesh.n_cells} -> {remeshed.n_cells}")

Boundary-Only Displacement

Move only boundary vertices and let move() propagate the displacement into the interior. PyVista's extract_surface gives the boundary vertex set for any mesh kind:

import numpy as np
import pyvista as pv
import mmgpy  # noqa: F401

mesh = pv.read("input.mesh")
surface = mesh.extract_surface()
boundary_mask = np.zeros(mesh.n_points, dtype=bool)
boundary_mask[surface.point_data["vtkOriginalPointIds"]] = True

displacement = np.zeros((mesh.n_points, 3))
displacement[boundary_mask, 2] = 0.05

moved = mesh.mmg.move(
    displacement,
    boundary_mask=boundary_mask,
    propagate=True,
    hmax=0.1,
)

Iterative Motion

For large deformations, use multiple sub-steps via the n_steps argument:

moved = mesh.mmg.move(total_displacement, n_steps=10, hmax=0.1, verbose=-1)

With Quality Control

Combine with remeshing parameters:

remeshed = mesh.mmg.move(
    displacement,
    hmin=0.01,
    hmax=0.1,
    hausd=0.001,
    verbose=1,
)

Complete Example

Deform a sphere into an ellipsoid:

import numpy as np
import pyvista as pv
import mmgpy  # noqa: F401

mesh = pv.read("sphere.mesh")
vertices = np.asarray(mesh.points)

# Compute displacement: stretch in z, compress in x and y
center = vertices.mean(axis=0)
relative = vertices - center
scale = np.array([0.7, 0.7, 1.5])  # Compress x,y, stretch z
displacement = (center + relative * scale) - vertices

remeshed = mesh.mmg.move(displacement, hmax=0.1, verbose=1)

q = remeshed.mmg.element_qualities()
print(f"Vertices: {mesh.n_points} -> {remeshed.n_points}")
print(f"Mean quality: {q.mean():.3f}")

remeshed.save("ellipsoid.vtk")

Tips

  1. Small steps: For large deformations, pass n_steps > 1 to dataset.mmg.move(...).
  2. Quality monitoring: Check dataset.mmg.element_qualities() after each step.
  3. Boundary handling: Use boundary_mask + propagate=True for interior smoothness.
  4. Remesh parameters: Combine with hmax, hausd for size control.
  5. Validation: Validate the mesh between steps with dataset.mmg.validate().