PyVista Integration¶
This tutorial covers how mmgpy plugs into PyVista for visualization and mesh interoperability.
Overview¶
PyVista is a powerful 3D visualization library for Python. mmgpy registers itself with PyVista on import:
- A Medit reader/writer plugin so
pv.read("foo.mesh")anddataset.save("foo.mesh")work end-to-end. - A
.mmgdataset accessor sodataset.mmg.remesh(...)operates directly onpv.UnstructuredGrid/pv.PolyData.
This means the rest of mmgpy's API composes with PyVista without an intermediate wrapper.
Quick Visualization¶
PyVista's built-in plot() works on the dataset directly:
import pyvista as pv
import mmgpy # noqa: F401 -- registers reader/writer + accessor
mesh = pv.read("input.mesh")
remeshed = mesh.mmg.remesh(hmax=0.1)
# One-liner visualization
remeshed.plot(show_edges=True)
# Customize with any PyVista plot options
remeshed.plot(color="lightblue", opacity=0.8, show_edges=False)
Custom Plotter Integration¶
For more complex visualizations, use any standard PyVista plotter:
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
remeshed = mesh.mmg.remesh(hmax=0.1)
plotter = pv.Plotter()
plotter.add_mesh(remeshed, show_edges=True, color="lightblue")
plotter.show()
Working with PyVista datasets directly¶
The .mmg accessor lets you operate on PyVista datasets without ever wrapping them:
import pyvista as pv
import mmgpy # noqa: F401
# Surface mesh — accessor returns PolyData
sphere = pv.Sphere(radius=1.0)
remeshed_surface = sphere.mmg.remesh(hsiz=0.1)
# Volume mesh — accessor returns UnstructuredGrid
cube = pv.Box().triangulate().delaunay_3d()
remeshed_volume = cube.mmg.remesh(hsiz=0.2)
The accessor auto-detects the mesh kind (mesh.mmg.kind returns the matching MeshKind enum). For Medit .mesh/.meshb files, pv.read("foo.mesh") works directly via mmgpy's reader plugin.
Visualization Examples¶
Side-by-Side Comparison¶
Compare before and after remeshing:
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
remeshed = mesh.mmg.remesh(hmax=0.1)
pl = pv.Plotter(shape=(1, 2))
pl.subplot(0, 0)
pl.add_mesh(mesh, show_edges=True, color="lightblue")
pl.add_text("Before", font_size=12)
pl.subplot(0, 1)
pl.add_mesh(remeshed, show_edges=True, color="lightgreen")
pl.add_text("After", font_size=12)
pl.link_views()
pl.show()
Quality Visualization¶
Visualize element quality with PyVista's built-in cell metrics:
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
remeshed = mesh.mmg.remesh(hmax=0.1)
quality = remeshed.cell_quality("scaled_jacobian")
quality.plot(
scalars="scaled_jacobian",
cmap="RdYlGn",
show_edges=True,
scalar_bar_args={"title": "Quality"},
)
Animation¶
Animate a remeshing sequence:
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
remeshed = mesh.mmg.remesh(hmax=0.5, verbose=-1)
pl = pv.Plotter()
actor = pl.add_mesh(remeshed, show_edges=True)
pl.show(interactive_update=True, auto_close=False)
for hmax in [0.5, 0.3, 0.2, 0.15, 0.1]:
remeshed = mesh.mmg.remesh(hmax=hmax, verbose=-1)
actor.mapper.SetInputData(remeshed)
pl.update()
pl.close()
Working with Mesh Data¶
Per-Vertex Scalar Fields¶
User-defined point_data survives on the dataset; transfer_fields=True interpolates non-MMG scalars onto the remeshed dataset:
import numpy as np
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
vertices = np.asarray(mesh.points)
mesh.point_data["temperature"] = np.sin(vertices[:, 0] * 2 * np.pi)
remeshed = mesh.mmg.remesh(hmax=0.1, transfer_fields=True)
remeshed.plot(scalars="temperature", show_edges=True, cmap="coolwarm")
From PyVista Primitives with Data¶
import pyvista as pv
import mmgpy # noqa: F401
sphere = pv.Sphere()
sphere.point_data["elevation"] = sphere.points[:, 2]
remeshed = sphere.mmg.remesh(hsiz=0.1, transfer_fields=True)
elevation = remeshed.point_data["elevation"]
print(f"Elevation range: {elevation.min():.2f} to {elevation.max():.2f}")
Interactive Workflows¶
Interactive Refinement¶
import pyvista as pv
import mmgpy # noqa: F401
from pyvista import examples
bunny = examples.download_bunny()
def remesh_callback(value):
remeshed = bunny.mmg.remesh(hmax=value, verbose=-1)
actor.mapper.SetInputData(remeshed)
pl.render()
pl = pv.Plotter()
actor = pl.add_mesh(bunny, show_edges=True)
pl.add_slider_widget(
remesh_callback,
rng=[0.01, 0.1],
value=0.05,
title="hmax",
interaction_event="always",
)
pl.show()
Picking Points for Refinement¶
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("input.mesh")
pinned_sizing = []
def add_refinement(point):
pinned_sizing.append(
{"shape": "sphere", "center": point, "radius": 0.1, "size": 0.01},
)
remeshed = mesh.mmg.remesh(hmax=0.1, local_sizing=pinned_sizing, verbose=-1)
actor.mapper.SetInputData(remeshed)
pl.render()
pl = pv.Plotter()
actor = pl.add_mesh(mesh, show_edges=True, pickable=True)
pl.enable_point_picking(callback=add_refinement, show_message="Click to add refinement")
pl.show()
Complete Example¶
Full workflow from PyVista primitive to remeshed output:
import pyvista as pv
import mmgpy # noqa: F401
torus = pv.ParametricTorus(ringradius=1.0, crosssectionradius=0.3)
print(f"Original: {torus.n_faces} triangles")
remeshed = torus.mmg.remesh(
hmax=0.1,
hausd=0.001,
verbose=1,
local_sizing=[
{"shape": "sphere", "center": (1.0, 0, 0), "radius": 0.3, "size": 0.02},
],
)
print(f"Remeshed: {remeshed.n_faces} triangles")
quality_mean = remeshed.mmg.element_qualities().mean()
print(f"Quality: {quality_mean:.3f}")
pl = pv.Plotter()
pl.add_mesh(remeshed, show_edges=True, edge_color="gray")
pl.add_text(f"Quality: {quality_mean:.3f}", font_size=10)
pl.show()
Tips¶
- Memory: Large meshes may use significant memory. Consider streaming through files for very large meshes.
- Cell types: PyVista supports many cell types, but mmgpy requires triangles (surface/2D) or tetrahedra (3D). Use
triangulate()if needed. - Coordinates: PyVista uses 0-indexed arrays throughout. MMG-specific accessor methods (
adjacent_elements,vertex_neighbors) use 1-based indexing for parity with the underlying library. - Performance: For real-time visualization, use
interactive_update=Trueand batch updates.
The .mmg Dataset Accessor¶
When mmgpy is installed, every PyVista UnstructuredGrid and PolyData instance gains a .mmg accessor exposing MMG operations directly. The accessor takes and returns PyVista datasets.
Remeshing variants¶
import pyvista as pv
import mmgpy # noqa: F401
mesh = pv.read("brain.mesh")
# Standard adaptive remeshing
remeshed = mesh.mmg.remesh(hsiz=0.1)
# Quality optimization without topology changes
optimized = mesh.mmg.remesh_optimize()
# Uniform target edge size
uniform = mesh.mmg.remesh_uniform(0.05)
# Lagrangian (moving-mesh) — supports TET, 2D, and surface meshes
moved = mesh.mmg.move(displacement, hmax=0.1)
# Level-set discretization — extracts the zero isosurface as a boundary
carved = mesh.mmg.remesh_levelset(levelset)
Local sizing constraints¶
Pass a local_sizing keyword to any remesh variant. Each constraint is a dict whose "shape" selects the geometry:
constrained = mesh.mmg.remesh(
nosizreq=True,
hgrad=1.3,
local_sizing=[
{"shape": "sphere", "center": (0, 0, 0), "radius": 0.4, "size": 0.05},
{"shape": "box", "bounds": [[0, 0, 0], [1, 1, 1]], "size": 0.1},
{"shape": "cylinder", "point1": (0, 0, 0), "point2": (1, 0, 0), "radius": 0.2, "size": 0.05},
{"shape": "from_point", "point": (0.5, 0.5, 0.5), "near_size": 0.02, "far_size": 0.1, "influence_radius": 0.5},
],
)
Solution I/O¶
.sol/.solb files round-trip through the accessor; sibling .sol files are auto-loaded on pv.read("foo.mesh"):
mesh.point_data["metric"] = my_metric
mesh.mmg.save_sol("out.sol")
other = pv.read("foo.mesh")
other.mmg.load_sol("foo.sol")
Validation and quality¶
report = mesh.mmg.validate(detailed=True) # ValidationReport
print(report.quality.mean, report.is_valid)
# MMG's in-radius ratio (distinct from PyVista's cell_quality)
qualities = mesh.mmg.element_qualities()
MMG-specific topology and centroid¶
PyVista exposes 0-based VTK adjacency via dataset.cell_neighbors(idx) and dataset.point_neighbors(idx). When you specifically need MMG's 1-based adjacency or its volume/area-weighted centroid (distinct from dataset.center, which is the unweighted arithmetic mean), use the accessor:
mesh.mmg.adjacent_elements(1) # MMG-adjacent elements of element 1
mesh.mmg.vertex_neighbors(1) # MMG-adjacent vertices of vertex 1
mesh.mmg.center_of_mass() # volume-weighted (3D) or area-weighted (2D)
Mesh kind¶
Next Steps¶
- Level-Set Extraction - Extract isosurfaces
- API Reference - Detailed API documentation