Skip to content

Metrics

This page documents the metric tensor operations in the mmgpy.metrics module.

Overview

Metric tensors control anisotropic mesh adaptation. A metric at each vertex specifies:

  • Isotropic, target edge length (single scalar or (n,1) array).
  • Anisotropic, target lengths along principal directions (a symmetric tensor in Voigt form).

The metric is attached to the dataset as point_data["metric"]; dataset.mmg.remesh(...) automatically picks it up.

Metric Creation

mmgpy.metrics.create_isotropic_metric

create_isotropic_metric(
    h: float | NDArray[float64], n_vertices: int | None = None, dim: int = 3
) -> NDArray[np.float64]

Create an isotropic metric field from scalar sizing values.

Parameters

h : float or array_like Desired element size(s). If scalar, same size at all vertices. If array, must have shape (n_vertices,) or (n_vertices, 1). n_vertices : int, optional Number of vertices. Required if h is a scalar. dim : int, optional Mesh dimension (2 or 3). Default is 3.

Returns

NDArray[np.float64] Metric tensor array with shape (n_vertices, n_components) where n_components is 6 for 3D and 3 for 2D.

Raises

ValueError If dim is not 2 or 3, n_vertices is missing when h is a scalar, or h does not flatten to a 1D array.

Examples

metric = create_isotropic_metric(0.1, n_vertices=100, dim=3) metric.shape (100, 6)

sizes = np.linspace(0.1, 0.5, 100) metric = create_isotropic_metric(sizes, dim=3) metric.shape (100, 6)

options: show_root_heading: true

mmgpy.metrics.create_anisotropic_metric

create_anisotropic_metric(
    sizes: NDArray[float64], directions: NDArray[float64] | None = None
) -> NDArray[np.float64]

Create an anisotropic metric tensor from principal sizes and directions.

The metric tensor M is constructed as: M = R @ D @ R.T where D is diagonal with D[i,i] = 1/sizes[i]^2 and R contains the principal direction vectors as columns.

Parameters

sizes : array_like Principal element sizes. Shape (3,) for 3D, (2,) for 2D. Can also be (n_vertices, 3) or (n_vertices, 2) for per-vertex sizes. directions : array_like, optional Principal direction vectors. Shape (3, 3) or (2, 2) for single metric, or (n_vertices, 3, 3) or (n_vertices, 2, 2) for per-vertex directions. Columns are eigenvectors. If None, uses identity (coordinate-aligned).

Returns

NDArray[np.float64] Metric tensor(s). Shape (6,) for single 3D metric, (3,) for single 2D, or (n_vertices, 6) / (n_vertices, 3) for per-vertex metrics.

Raises

ValueError If sizes is not 1D / 2D, the dimension is not 2 or 3, or directions does not match the expected shape.

Examples

Create a metric with 10x stretch in x-direction:

sizes = np.array([0.1, 1.0, 1.0]) # Small in x, large in y,z metric = create_anisotropic_metric(sizes) metric array([100., 0., 0., 1., 0., 1.])

Create a rotated metric:

import numpy as np theta = np.pi / 4 # 45 degrees R = np.array([[np.cos(theta), -np.sin(theta), 0], ... [np.sin(theta), np.cos(theta), 0], ... [0, 0, 1]]) sizes = np.array([0.1, 1.0, 1.0]) metric = create_anisotropic_metric(sizes, R)

options: show_root_heading: true

mmgpy.metrics.create_metric_from_hessian

create_metric_from_hessian(
    hessian: NDArray[float64],
    target_error: float = 0.001,
    hmin: float | None = None,
    hmax: float | None = None,
) -> NDArray[np.float64]

Create metric tensor from Hessian matrix for interpolation error control.

Given a Hessian H of a solution field, constructs a metric M such that the interpolation error is bounded by target_error. This is used for solution-adaptive mesh refinement.

The metric eigenvalues are: lambda_i = c * |hessian_eigenvalue_i| / target_error where c is a constant depending on the interpolation order.

Parameters

hessian : array_like Hessian tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D. Components: [H11, H12, H13, H22, H23, H33] for 3D. target_error : float, optional Target interpolation error. Default is 1e-3. hmin : float, optional Minimum element size. Limits maximum metric eigenvalues. hmax : float, optional Maximum element size. Limits minimum metric eigenvalues.

Returns

NDArray[np.float64] Metric tensor(s) for adaptive remeshing.

Notes

For P1 interpolation, the interpolation error is bounded by: e <= (1/8) * h^2 * |d²u/ds²|_max

This function computes the metric that achieves a specified error bound.

options: show_root_heading: true

Hessian Recovery

mmgpy.metrics.compute_hessian

compute_hessian(
    vertices: NDArray[float64], elements: NDArray[int32], field: NDArray[float64]
) -> NDArray[np.float64]

Compute the Hessian of a scalar field on a mesh via least-squares recovery.

Uses a patch-based least-squares approach: for each vertex, a quadratic polynomial is fitted to the field values at neighboring vertices, and the second-order coefficients give the Hessian components.

This is the missing piece for solution-adaptive remeshing: compute a solution field with your FE solver, pass it here to get the Hessian, then use :func:create_metric_from_hessian to build an adaptation metric.

Parameters

vertices : ndarray Nx2 or Nx3 array of vertex coordinates. elements : ndarray Mx(nodes_per_element) array of element connectivity. field : ndarray N array of scalar field values at vertices.

Returns

ndarray Hessian tensor array. Shape (N, 3) for 2D [H11, H12, H22] or (N, 6) for 3D [H11, H12, H13, H22, H23, H33].

Raises

ValueError If field does not have one value per vertex.

options: show_root_heading: true

Pairing compute_hessian with create_metric_from_hessian enables solution-adaptive remeshing: the same vertex budget concentrates elements where the field has high curvature.

2D uniform vs Hessian-adapted mesh 3D uniform vs Hessian-adapted mesh

For an end-to-end walk-through of the solve → recover → adapt loop, see the Hessian-Based Adaptation tutorial.

Metric Operations

mmgpy.metrics.intersect_metrics

intersect_metrics(
    m1: NDArray[float64], m2: NDArray[float64], dim: int | None = None
) -> NDArray[np.float64]

Compute the intersection of two metric tensors.

The intersection produces a metric that is at least as refined as both input metrics in all directions. This is useful for combining metrics from different sources (e.g., boundary layer + feature-based).

The intersection is computed via simultaneous diagonalization: M_intersect = M1^(1/2) @ N @ M1^(1/2) where N is diagonal with max eigenvalues of M1^(-1/2) @ M2 @ M1^(-1/2).

Parameters

m1, m2 : array_like Metric tensors to intersect. Must have same shape. Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D. dim : int, optional Dimension (2 or 3). Inferred from tensor shape if not provided.

Returns

NDArray[np.float64] Intersected metric tensor(s), same shape as inputs.

Raises

ValueError If m1 and m2 do not share the same shape.

options: show_root_heading: true

mmgpy.metrics.compute_metric_eigenpairs

compute_metric_eigenpairs(
    tensor: NDArray[float64], dim: int | None = None
) -> tuple[NDArray[np.float64], NDArray[np.float64]]

Extract principal sizes and directions from metric tensor(s).

Parameters

tensor : array_like Metric tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D. dim : int, optional Dimension (2 or 3). Inferred from tensor shape if not provided.

Returns

tuple[NDArray, NDArray] (sizes, directions) where: - sizes: Principal element sizes, shape (3,) or (n, 3) for 3D - directions: Eigenvector matrices, shape (3, 3) or (n, 3, 3) for 3D Columns are eigenvectors corresponding to sizes.

Examples

tensor = np.array([100., 0., 0., 1., 0., 1.]) # 10x stretch in x sizes, directions = compute_metric_eigenpairs(tensor) sizes array([0.1, 1. , 1. ])

options: show_root_heading: true

Tensor Utilities

mmgpy.metrics.tensor_to_matrix

tensor_to_matrix(
    tensor: NDArray[float64], dim: int | None = None
) -> NDArray[np.float64]

Convert tensor storage format to full symmetric matrix.

Parameters

tensor : array_like Tensor in storage format. Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D. dim : int, optional Dimension (2 or 3). Inferred from tensor shape if not provided.

Returns

NDArray[np.float64] Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D, (2, 2) or (n, 2, 2) for 2D.

options: show_root_heading: true

mmgpy.metrics.matrix_to_tensor

matrix_to_tensor(matrix: NDArray[float64]) -> NDArray[np.float64]

Convert full symmetric matrix to tensor storage format.

Parameters

matrix : array_like Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D, (2, 2) or (n, 2, 2) for 2D.

Returns

NDArray[np.float64] Tensor in storage format. Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.

options: show_root_heading: true

mmgpy.metrics.validate_metric_tensor

validate_metric_tensor(
    tensor: NDArray[float64], dim: int | None = None, *, raise_on_invalid: bool = True
) -> tuple[bool, str]

Validate that metric tensor(s) are positive-definite.

A valid metric tensor must be symmetric positive-definite, meaning all eigenvalues must be strictly positive.

Parameters

tensor : array_like Tensor(s) to validate. Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D. dim : int, optional Dimension (2 or 3). Inferred from tensor shape if not provided. raise_on_invalid : bool, optional If True, raises ValueError on invalid tensors. Default is True.

Returns

tuple[bool, str] (is_valid, message) tuple.

Raises

ValueError If raise_on_invalid is True and tensor is not valid.

Examples

valid_tensor = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 1.0]) validate_metric_tensor(valid_tensor) (True, 'Valid positive-definite metric tensor')

invalid_tensor = np.array([-1.0, 0.0, 0.0, 1.0, 0.0, 1.0]) validate_metric_tensor(invalid_tensor, raise_on_invalid=False) (False, 'Tensor has non-positive eigenvalues...')

options: show_root_heading: true

Usage Examples

Isotropic Metric

Create a metric for uniform element sizes:

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

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

sizes = np.ones(mesh.n_points) * 0.1
mesh.point_data["metric"] = metrics.create_isotropic_metric(sizes)

remeshed = mesh.mmg.remesh()

Variable Size Metric

Size varying with position:

import numpy as np

vertices = np.asarray(mesh.points)

# Size increases with distance from origin
distances = np.linalg.norm(vertices, axis=1)
sizes = 0.01 + 0.1 * distances

mesh.point_data["metric"] = metrics.create_isotropic_metric(sizes)
remeshed = mesh.mmg.remesh()

Anisotropic Metric

Different sizes in different directions:

import numpy as np

# sizes: desired element sizes along each principal direction
sizes = np.array([0.1, 0.1, 0.05])  # Smaller in z

single_tensor = metrics.create_anisotropic_metric(sizes)
mesh.point_data["metric"] = np.tile(single_tensor, (mesh.n_points, 1))

remeshed = mesh.mmg.remesh()

Metric from Hessian

Adapt mesh to solution curvature:

from mmgpy.metrics import compute_hessian, create_metric_from_hessian

solution = np.sin(vertices[:, 0] * 2 * np.pi)
hessian = compute_hessian(vertices, triangles, solution)

mesh.point_data["metric"] = create_metric_from_hessian(
    hessian,
    target_error=0.01,
    hmin=1e-3,
    hmax=1e-1,
)

adapted = mesh.mmg.remesh(hgrad=2.0)

Metric Intersection

Combine multiple metrics (minimum size wins):

sizes1 = np.ones(mesh.n_points) * 0.05
sizes2 = np.ones(mesh.n_points) * 0.08
metric1 = metrics.create_isotropic_metric(sizes1)
metric2 = metrics.create_isotropic_metric(sizes2)

combined = metrics.intersect_metrics(metric1, metric2)
mesh.point_data["metric"] = combined

Extracting Metric Information

metric = np.asarray(mesh.point_data["metric"])

sizes, directions = metrics.compute_metric_eigenpairs(metric)

# sizes shape: (n_vertices, dim) — element sizes along each principal direction
# directions shape: (n_vertices, dim, dim) — principal directions as columns

print(f"Size range: {sizes.min():.4f} to {sizes.max():.4f}")

Tensor Format Conversion

MMG uses symmetric tensors in Voigt notation:

# 3D: 6 components per vertex
# [M11, M12, M13, M22, M23, M33]

# 2D: 3 components per vertex
# [M11, M12, M22]

tensor = np.asarray(mesh.point_data["metric"])[0]  # First vertex
matrix = metrics.tensor_to_matrix(tensor)
print(matrix.shape)  # (3, 3)

tensor_back = metrics.matrix_to_tensor(matrix)

Validation

Check metric tensor validity:

metric = np.asarray(mesh.point_data["metric"])

is_valid = metrics.validate_metric_tensor(metric)
if not is_valid:
    print("Warning: invalid metric tensor")

Metric Formats

3D Metrics (TETRAHEDRAL)

Symmetric 3x3 tensor stored as 6 components:

    [M11  M12  M13]
M = [M12  M22  M23]  -> [M11, M12, M13, M22, M23, M33]
    [M13  M23  M33]

Metric field shape: (n_vertices, 6).

2D Metrics (TRIANGULAR_2D)

Symmetric 2x2 tensor stored as 3 components:

    [M11  M12]
M = [M12  M22]  -> [M11, M12, M22]

Metric field shape: (n_vertices, 3).

Surface Metrics (TRIANGULAR_SURFACE)

Same as 3D: (n_vertices, 6).

Tips

  1. Isotropic first, start with isotropic metrics, add anisotropy only when needed.
  2. Size bounds, ensure metric sizes are within reasonable bounds relative to the domain.
  3. Gradation, MMG's hgrad parameter controls size gradation regardless of metric.
  4. Validation, always validate metric tensors before remeshing.
  5. Combination, use intersect_metrics to combine sizing from different sources.