Adaptive Remeshing (GMSH)#

The RemeshingStrategies can be passed to a GMSH remesher.

from pathlib import Path

import matplotlib.pyplot as plt
import meshio
import numpy as np
import shapely

from meshwell.cad_occ import cad_occ
from meshwell.mesh import mesh
from meshwell.occ_xao_writer import write_xao
from meshwell.polysurface import PolySurface
from meshwell.remesh import (
    BinaryScalingStrategy,
    remesh_gmsh,
)
from meshwell.visualization import plot2D
/home/runner/work/meshwell/meshwell/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Define Geometry#

We define two adjacent rectangles with different physical tags.

# Define geometry
large_rect = 10
mid_rect = 2

# Box 1: inner box
polygon1 = shapely.Polygon(
    [
        [-large_rect / 2, -mid_rect / 2],
        [large_rect / 2, -mid_rect / 2],
        [large_rect / 2, mid_rect / 2],
        [-large_rect / 2, mid_rect / 2],
        [-large_rect / 2, -mid_rect / 2],
    ],
)

# Box 2: global box
polygon2 = shapely.Polygon(
    [
        [-large_rect / 2, -large_rect / 2],
        [large_rect / 2, -large_rect / 2],
        [large_rect / 2, large_rect / 2],
        [-large_rect / 2, large_rect / 2],
        [-large_rect / 2, -large_rect / 2],
    ],
)

poly_obj1 = PolySurface(
    polygons=polygon1,
    mesh_order=1,
    physical_name="inner_box",
)
poly_obj2 = PolySurface(
    polygons=polygon2,
    mesh_order=2,
    physical_name="outer_box",
)

entities_list = [poly_obj1, poly_obj2]

# Generate CAD
write_xao(cad_occ(entities_list), "remesh_example.xao")

Initial Mesh#

We generate a coarse initial mesh.

# Generate initial mesh
initial_mesh = mesh(
    dim=2,
    input_file="remesh_example.xao",
    output_file="remesh_example_initial.msh",
    default_characteristic_length=1.0,  # Coarse mesh
    n_threads=1,
)

print(f"Initial mesh points: {len(initial_mesh.points)}")
plot2D(initial_mesh, title="Initial Coarse Mesh", wireframe=True)
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'remesh_example.xao'...
Info    : Done reading 'remesh_example.xao'
Initial mesh points: 148
_images/5b8920b36754b33810aa195e7b83dd0439fe3ca866a4ea3a1ef7bdf24e1ac920.png

Define Remeshing Strategy#

We define a strategy that refines the mesh along an oval shape. The strategy function calculates a solution/error (e.g., distance from boundary), and if it exceeds a threshold, refinement is triggered.

# Define oval parameters
center_x, center_y = 0.0, 0.0
radius_x, radius_y = 3.0, 3.0


def oval_looking_data(coords, _data=None):
    """Calculate solution/error based on proximity to oval boundary.

    Returns 1.0 if close to boundary, 0.0 otherwise.
    """
    x = coords[:, 0]
    y = coords[:, 1]

    # Normalized distance from center
    normalized_dist = ((x - center_x) ** 2 / radius_x**2) + (
        (y - center_y) ** 2 / radius_y**2
    )
    dist_from_boundary = np.abs(np.sqrt(normalized_dist) - 1.0)

    # Invert distance: high value near boundary
    # e.g., 1.0 at boundary, decaying to 0.0 at distance 1.0
    return np.maximum(0, 1.0 - dist_from_boundary)

Visualize Metric Field#

We can visualize the solution/error field on the initial mesh to see where refinement will occur.

# Calculate solution/error on initial mesh points
data_values = oval_looking_data(initial_mesh.points)

plt.figure(figsize=(8, 8))
plt.scatter(
    initial_mesh.points[:, 0],
    initial_mesh.points[:, 1],
    c=data_values,
    cmap="viridis",
    s=10,
)
plt.colorbar(label="Refinement Metric")
plt.title("Refinement Metric (Oval)")
plt.axis("equal")
plt.show()

# refinement_data as (N, 4) -> x, y, z, data
refinement_data = np.column_stack([initial_mesh.points, data_values])

# Create strategy with refinement_data
strategy = BinaryScalingStrategy(
    func=oval_looking_data,
    threshold=0.7,
    factor=0.2,
    refinement_data=refinement_data,
    min_size=0.1,
    max_size=2.0,
    field_smoothing_steps=2,
)

size_map = remesh_gmsh(
    input_mesh=Path("remesh_example_initial.msh"),
    geometry_file=Path("remesh_example.xao"),
    output_mesh=Path("remesh_example_final.msh"),
    strategies=[strategy],
    dim=2,
    verbosity=0,
    n_threads=1,
)

plt.figure(figsize=(8, 8))
# size_map is (N, 4) -> x, y, z, size
sc = plt.scatter(
    size_map[:, 0], size_map[:, 1], c=size_map[:, 3], cmap="viridis_r", s=5
)
plt.colorbar(sc, label="Target Mesh Size")
plt.title("Generated Size Field (with Interpolation)")
plt.axis("equal")
plt.show()

final_mesh = meshio.read("remesh_example_final.msh")
print(f"Final mesh points: {len(final_mesh.points)}")

plot2D(final_mesh, title="Adaptive Remesh (Oval Refinement)", wireframe=True)
_images/31f1c2b838c22468032a6fa1578bc723aff74025409a038414d97b2e3dc02222.png
Info    : Reading 'remesh_example_initial.msh'...
Info    : 21 entities
Info    : 148 nodes
Info    : 314 elements
Info    : Done reading 'remesh_example_initial.msh'
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'remesh_example.xao'...
Info    : Done reading 'remesh_example.xao'
_images/18d5cb08e5656e9cae161dac4d3f36d88a61e8a947c52eaf8203c206e55af8a3.png
Final mesh points: 1095
_images/b7c8bfc598a93e0ced3d0c8c5775c95f27792b448fe2a7ce5f5f959c3fdb06cb.png

Perform Remeshing with Finer Grid Evaluation#

To capture the oval shape more accurately, we can evaluate the solution/error on a dense grid of points in addition to the mesh nodes. This ensures that features smaller than the initial mesh elements are detected.

# Generate a dense grid of points
x = np.linspace(-large_rect / 2, large_rect / 2, 100)
y = np.linspace(-large_rect / 2, large_rect / 2, 100)
X, Y = np.meshgrid(x, y)
grid_coords = np.column_stack([X.ravel(), Y.ravel(), np.zeros_like(X.ravel())])

# Evaluate solution/error on the grid
grid_data = oval_looking_data(grid_coords)

# refinement_data as (N, 4) -> x, y, z, data
grid_refinement_data = np.column_stack([grid_coords, grid_data])

# Create strategy with grid refinement data
grid_strategy = BinaryScalingStrategy(
    threshold=0.8,
    factor=0.2,
    refinement_data=grid_refinement_data,
    min_size=0.1,
    max_size=2.0,
    field_smoothing_steps=5,
)

size_map = remesh_gmsh(
    input_mesh=Path("remesh_example_initial.msh"),
    geometry_file=Path("remesh_example.xao"),
    output_mesh=Path("remesh_example_final.msh"),
    strategies=[grid_strategy],
    dim=2,
    verbosity=0,
    n_threads=1,
)
Info    : Reading 'remesh_example_initial.msh'...
Info    : 21 entities
Info    : 148 nodes
Info    : 314 elements
Info    : Done reading 'remesh_example_initial.msh'
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'remesh_example.xao'...
Info    : Done reading 'remesh_example.xao'

Visualize Size Field#

We can visualize the generated size field, including interpolated points.

plt.figure(figsize=(8, 8))
# size_map is (N, 4) -> x, y, z, size
sc = plt.scatter(
    size_map[:, 0], size_map[:, 1], c=size_map[:, 3], cmap="viridis_r", s=5
)
plt.colorbar(sc, label="Target Mesh Size")
plt.title("Generated Size Field (with Interpolation)")
plt.axis("equal")
plt.show()
_images/58fbc95b6032e42c095c390ecf4826d9a9fd208628bc89b7bc479a292c226f64.png

Visualize Result#

We load and plot the final mesh to see the refinement.

final_mesh = meshio.read("remesh_example_final.msh")
print(f"Final mesh points: {len(final_mesh.points)}")

plot2D(final_mesh, title="Adaptive Remesh (Oval Refinement)", wireframe=True)
Final mesh points: 775
_images/227e52c5b9b84be1f795e2f34049b0feb7456ea62bf86257657cc849abe33690.png

Multiple Strategies#

We can combine multiple strategies to refine different regions. Here we’ll refine around a circle and along a vertical line.

# Define circle parameters
circle_center_x, circle_center_y = 2.0, 2.0
circle_radius = 1.5


def circle_looking_data(coords, data=None):
    """Calculate solution/error based on proximity to circle boundary."""
    if data is not None:
        return data

    x = coords[:, 0]
    y = coords[:, 1]

    # Distance from circle center
    dist_from_center = np.sqrt((x - circle_center_x) ** 2 + (y - circle_center_y) ** 2)
    dist_from_boundary = np.abs(dist_from_center - circle_radius)

    # High value near boundary
    return np.maximum(0, 1.0 - dist_from_boundary / 0.5)


# Define line parameters (vertical line at x = -2)
line_x = -2.0
line_width = 0.3


def line_looking_data(coords, data=None):
    """Calculate solution/error based on proximity to vertical line."""
    if data is not None:
        return data

    x = coords[:, 0]

    # Distance from line
    dist_from_line = np.abs(x - line_x)

    # High value near line
    return np.maximum(0, 1.0 - dist_from_line / line_width)


# Generate evaluation points for circle
x_circle = np.linspace(-large_rect / 2, large_rect / 2, 80)
y_circle = np.linspace(-large_rect / 2, large_rect / 2, 80)
X_circle, Y_circle = np.meshgrid(x_circle, y_circle)
circle_coords = np.column_stack(
    [X_circle.ravel(), Y_circle.ravel(), np.zeros_like(X_circle.ravel())]
)
circle_data_values = circle_looking_data(circle_coords)
circle_refinement_data = np.column_stack([circle_coords, circle_data_values])

# Generate evaluation points for line
x_line = np.linspace(-large_rect / 2, large_rect / 2, 80)
y_line = np.linspace(-large_rect / 2, large_rect / 2, 80)
X_line, Y_line = np.meshgrid(x_line, y_line)
line_coords = np.column_stack(
    [X_line.ravel(), Y_line.ravel(), np.zeros_like(X_line.ravel())]
)
line_data_values = line_looking_data(line_coords)
line_refinement_data = np.column_stack([line_coords, line_data_values])

# Create strategies
circle_strategy = BinaryScalingStrategy(
    func=circle_looking_data,
    threshold=0.5,  # Lower threshold to refine more area
    factor=0.15,  # Stronger refinement (smaller factor)
    refinement_data=circle_refinement_data,
    min_size=0.05,  # Much smaller minimum size
    max_size=2.0,
    field_smoothing_steps=5,
)

line_strategy = BinaryScalingStrategy(
    func=line_looking_data,
    threshold=0.4,  # Lower threshold to refine more area
    factor=0.2,  # Stronger refinement
    refinement_data=line_refinement_data,
    min_size=0.08,  # Much smaller minimum size
    max_size=2.0,
    field_smoothing_steps=5,
)

# Combine both strategies
multi_size_map = remesh_gmsh(
    input_mesh=Path("remesh_example_initial.msh"),
    geometry_file=Path("remesh_example.xao"),
    output_mesh=Path("remesh_example_multi.msh"),
    strategies=[circle_strategy, line_strategy],
    dim=2,
    verbosity=0,
    n_threads=1,
)
Info    : Reading 'remesh_example_initial.msh'...
Info    : 21 entities
Info    : 148 nodes
Info    : 314 elements
Info    : Done reading 'remesh_example_initial.msh'
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'remesh_example.xao'...
Info    : Done reading 'remesh_example.xao'

Visualize Multi-Strategy Result#

# Visualize the combined size field
plt.figure(figsize=(10, 10))
sc = plt.scatter(
    multi_size_map[:, 0],
    multi_size_map[:, 1],
    c=multi_size_map[:, 3],
    cmap="viridis_r",
    s=3,
)
plt.colorbar(sc, label="Target Mesh Size")
plt.title("Combined Size Field (Circle + Line)")
plt.axis("equal")
plt.show()

# Load and visualize the final mesh
multi_mesh = meshio.read("remesh_example_multi.msh")
print(f"Multi-strategy mesh points: {len(multi_mesh.points)}")
plot2D(multi_mesh, title="Multi-Strategy Refinement", wireframe=True)
_images/7fec4faf8604afad6b7dba748f9a8f1e92c6df75dad7672172f3b6299842d9f3.png
Multi-strategy mesh points: 383
_images/5d5329bef11565c1c0296ca765c266da0e64aef86f9918c8adabe0dcd0368093.png
# Clean up files
for f in [
    "remesh_example.xao",
    "remesh_example_initial.msh",
    "remesh_example_final.msh",
    "remesh_example_multi.msh",
    "remesh_example_3d.xao",
    "remesh_example_3d_initial.msh",
    "remesh_example_3d_final.msh",
]:
    if Path(f).exists():
        Path(f).unlink()
Path("remesh_example_direct.msh").unlink(missing_ok=True)