Mesh Quality Analysis#

After generating a mesh, it’s important to assess its quality before using it in simulations. Poor quality meshes can lead to convergence issues, numerical instability, or incorrect results. Meshwell provides a comprehensive MeshQualityAnalyzer that checks various aspects of your mesh.

from pathlib import Path

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.quality import MeshQualityAnalyzer, main
from meshwell.resolution import ResolutionSpec
/home/runner/work/meshwell/meshwell/.venv/lib/python3.13/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

Creating a Mesh to Analyze#

First, let’s create a simple 2D mesh to demonstrate the quality analyzer:

# Create geometry
polygon = shapely.box(-5, -5, 5, 5)
entity = PolySurface(polygons=polygon, physical_name="test_region", mesh_order=1)

# Generate CAD and mesh
write_xao(cad_occ([entity]), "quality_test.xao")
test_mesh = mesh(
    dim=2,
    input_file="quality_test.xao",
    output_file="quality_test.msh",
    default_characteristic_length=1.0,
)

print(f"Created mesh with {len(test_mesh.points)} vertices")
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'quality_test.xao'...
Info    : Done reading 'quality_test.xao'
Created mesh with 144 vertices

Using the Quality Analyzer#

The MeshQualityAnalyzer provides a comprehensive suite of checks. You can run all checks at once using the main() function:

# Run comprehensive quality analysis
exit_code = main("quality_test.msh")
============================================================
  ENHANCED MESH QUALITY ANALYZER
============================================================
Target mesh file: 'quality_test.msh'
=== Mesh File Check ===
✓ Mesh file exists: quality_test.msh
✓ File size: 9275 bytes
✓ File is readable, first 10 lines preview:
   1: $MeshFormat
   2: 4.1 0 8
   3: $EndMeshFormat
   4: $PhysicalNames
   5: 2
   6: 1 1 "test_region___None"
   7: 2 1 "test_region"
   8: $EndPhysicalNames
   9: $Entities
  10: 4 4 1 0

=== Parsing GMSH Mesh ===
✓ Detected GMSH format version 4.1, type 0
✓ Parsed 144 nodes
✓ Parsed 0 tetrahedra
✓ Parsed 246 triangular elements
✓ Parsed 0 surface elements
✓ Found 1 physical regions
✓ Detected mesh dimension: 2D

=== Mesh Connectivity Analysis ===
✓ Boundary edges: 40
✓ Internal edges: 349

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.25e-01
  Mean area: 4.07e-01
  Area ratio (max/min): 1.98e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.42
  Mean aspect ratio: 1.12
  Quality distribution:
    Excellent (AR < 2): 246 (100.0%)
    Good (2 ≤ AR < 5): 0 (0.0%)
    Poor (5 ≤ AR < 20): 0 (0.0%)
    Very poor (AR ≥ 20): 0 (0.0%)
Edge length statistics:
  Min edge length: 7.55e-01
  Max edge length: 1.16e+00
  Edge length ratio: 1.54e+00
Angle statistics:
  Min angle: 42.7°
  Max angle: 83.9°

=== Physical Region Analysis ===
Physical region element counts:
  test_region (dim=2, tag=1): 246 triangles = 246 total

=== Quality Metrics Per Physical Group ===

test_region (tag=1, 246 elements):
  Aspect ratio: min=1.00, max=1.42, mean=1.12
  Area: min=2.66e-01, max=5.25e-01, mean=4.07e-01
  Min angle: 42.7°

=== Physical Groups by Dimension ===
Surfaces (dimension 2):
  test_region (tag 1)

=== Mesh Gradation Analysis ===
Mesh gradation statistics:
  Mean size ratio: 1.09
  Max size ratio: 1.52

============================================================
  MESH QUALITY SUMMARY REPORT
============================================================
Total elements analyzed: 246
Physical regions: 1
Boundary edges: 0

✅ MESH QUALITY: EXCELLENT
   No critical issues detected. Mesh should work well for simulation.

📋 RECOMMENDATIONS:
   1. Mesh quality is excellent!
   2. Consider this mesh as a reference for future meshes
============================================================

🎉 All checks passed! Mesh is ready for simulations.

The analyzer performs the following checks:

  1. File Check: Verifies the mesh file exists and is readable

  2. Mesh Parsing: Loads nodes, elements, and physical groups from GMSH format

  3. Connectivity: Checks for orphaned nodes, non-manifold edges/faces

  4. Geometric Quality: Analyzes aspect ratios, volumes/areas, angles, edge lengths

  5. Physical Regions: Reports element counts per physical group

  6. Contacts/Boundaries: Lists physical groups by dimension

  7. Mesh Gradation: Detects abrupt size changes between adjacent elements

Programmatic Access#

You can also use the analyzer programmatically to access specific metrics:

# Create analyzer instance
analyzer = MeshQualityAnalyzer("quality_test.msh")

# Run parsing
analyzer.check_mesh_file()
analyzer.parse_gmsh_mesh()

# Run specific checks
print("\n=== Manual Analysis ===")
analyzer.check_mesh_connectivity()
analyzer.analyze_geometric_quality()

# Access computed metrics
metrics = analyzer.quality_metrics
print(
    f"\nAspect ratio range: {min(metrics['aspect_ratios']):.2f} - {max(metrics['aspect_ratios']):.2f}"
)
print(f"Mean aspect ratio: {np.mean(metrics['aspect_ratios']):.2f}")
=== Mesh File Check ===
✓ Mesh file exists: quality_test.msh
✓ File size: 9275 bytes
✓ File is readable, first 10 lines preview:
   1: $MeshFormat
   2: 4.1 0 8
   3: $EndMeshFormat
   4: $PhysicalNames
   5: 2
   6: 1 1 "test_region___None"
   7: 2 1 "test_region"
   8: $EndPhysicalNames
   9: $Entities
  10: 4 4 1 0

=== Parsing GMSH Mesh ===
✓ Detected GMSH format version 4.1, type 0
✓ Parsed 144 nodes
✓ Parsed 0 tetrahedra
✓ Parsed 246 triangular elements
✓ Parsed 0 surface elements
✓ Found 1 physical regions
✓ Detected mesh dimension: 2D

=== Manual Analysis ===

=== Mesh Connectivity Analysis ===
✓ Boundary edges: 40
✓ Internal edges: 349

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.25e-01
  Mean area: 4.07e-01
  Area ratio (max/min): 1.98e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.42
  Mean aspect ratio: 1.12
  Quality distribution:
    Excellent (AR < 2): 246 (100.0%)
    Good (2 ≤ AR < 5): 0 (0.0%)
    Poor (5 ≤ AR < 20): 0 (0.0%)
    Very poor (AR ≥ 20): 0 (0.0%)
Edge length statistics:
  Min edge length: 7.55e-01
  Max edge length: 1.16e+00
  Edge length ratio: 1.54e+00
Angle statistics:
  Min angle: 42.7°
  Max angle: 83.9°

Aspect ratio range: 1.00 - 1.42
Mean aspect ratio: 1.12

Understanding Quality Metrics#

Aspect Ratio#

The ratio of longest to shortest edge in an element. Lower is better:

  • Excellent: AR < 2 (2D) or AR < 3 (3D)

  • Good: 2-5 (2D) or 3-10 (3D)

  • Poor: 5-20 (2D) or 10-100 (3D)

  • Very Poor: >20 (2D) or >100 (3D)

Angles#

Interior angles of triangular/tetrahedral elements:

  • Ideal: Close to 60° for equilateral triangles/regular tetrahedra

  • Warning: < 5° or > 150°

  • Critical: < 1° (can cause numerical instability)

Mesh Gradation#

Size ratio between adjacent elements:

  • Good: Ratio < 2

  • Acceptable: Ratio < 3 (2D) or < 5 (3D)

  • Poor: Ratio > 3 (2D) or > 5 (3D) - may cause convergence issues

Per-Group Quality Metrics#

The analyzer can also report quality metrics broken down by physical group, helping you identify which specific regions have quality issues.

Let’s create a mesh with multiple regions to demonstrate:

# Create geometry with two regions - one with fine mesh, one with coarse
poly1 = shapely.box(-5, -5, 0, 0)
poly2 = shapely.box(0, 0, 5, 5)

region1 = PolySurface(polygons=poly1, physical_name="fine_region", mesh_order=1)
region2 = PolySurface(polygons=poly2, physical_name="coarse_region", mesh_order=1)

write_xao(cad_occ([region1, region2]), "multi_region.xao")

# Generate mesh with different sizes in each region

fine_spec = ResolutionSpec(resolution=0.3, apply_to="surfaces")
coarse_spec = ResolutionSpec(resolution=1.5, apply_to="surfaces")

multi_mesh = mesh(
    dim=2,
    input_file="multi_region.xao",
    output_file="multi_region.msh",
    default_characteristic_length=1.0,
    resolution_specs={
        "fine_region": [fine_spec],
        "coarse_region": [coarse_spec],
    },
)

print(f"Multi-region mesh: {len(multi_mesh.points)} vertices")

# Analyze with per-group reporting
analyzer_multi = MeshQualityAnalyzer("multi_region.msh")
analyzer_multi.check_mesh_file()
analyzer_multi.parse_gmsh_mesh()
analyzer_multi.analyze_geometric_quality()
analyzer_multi.report_per_group_quality()
Info    : Clearing all models and views...
Info    : Done clearing all models and views
Info    : Reading 'multi_region.xao'...
Info    : Done reading 'multi_region.xao'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 15
     11 
     12 fine_spec = ResolutionSpec(resolution=0.3, apply_to="surfaces")
     13 coarse_spec = ResolutionSpec(resolution=1.5, apply_to="surfaces")
     14 
---> 15 multi_mesh = mesh(
     16     dim=2,
     17     input_file="multi_region.xao",
     18     output_file="multi_region.msh",

File ~/work/meshwell/meshwell/meshwell/mesh.py:848, in mesh(dim, default_characteristic_length, input_file, output_file, resolution_specs, background_remeshing_file, global_scaling, global_2D_algorithm, global_3D_algorithm, mesh_element_order, verbosity, periodic_entities, optimization_flags, boundary_delimiter, n_threads, filename, model, point_tolerance, gmsh_version, interface_delimiter, pre_2d_hook, pre_3d_hook, post_3d_hook, cad_settings)
    845     mesh_generator.load_xao_file(input_file)
    847 # Process geometry into mesh
--> 848 mesh_obj = mesh_generator.process_geometry(
    849     dim=dim,
    850     background_remeshing_file=background_remeshing_file,
    851     default_characteristic_length=default_characteristic_length,
    852     global_scaling=global_scaling,
    853     global_2D_algorithm=global_2D_algorithm,
    854     global_3D_algorithm=global_3D_algorithm,
    855     mesh_element_order=mesh_element_order,
    856     verbosity=verbosity,
    857     periodic_entities=periodic_entities,
    858     optimization_flags=optimization_flags,
    859     boundary_delimiter=boundary_delimiter,
    860     resolution_specs=resolution_specs,
    861     gmsh_version=gmsh_version,
    862     interface_delimiter=interface_delimiter,
    863     pre_2d_hook=pre_2d_hook,
    864     pre_3d_hook=pre_3d_hook,
    865     post_3d_hook=post_3d_hook,
    866 )
    868 # Save to file if output file provided
    869 if output_file is not None:

File ~/work/meshwell/meshwell/meshwell/mesh.py:669, in Mesh.process_geometry(self, dim, default_characteristic_length, background_remeshing_file, global_scaling, global_2D_algorithm, global_3D_algorithm, mesh_element_order, verbosity, periodic_entities, optimization_flags, boundary_delimiter, resolution_specs, gmsh_version, interface_delimiter, pre_2d_hook, pre_3d_hook, post_3d_hook)
    667 if len(attempts) == 1:
    668     algo2d, algo3d = attempts[0]
--> 669     return _run_once(algo2d, algo3d)
    671 # Multi-attempt: persist CAD before the first try so we can restore
    672 # after a failed generate() leaves gmsh in an unrecoverable "busy"
    673 # state (neither mesh.clear() nor re-setting options releases it).
    674 with tempfile.TemporaryDirectory() as tmp:

File ~/work/meshwell/meshwell/meshwell/mesh.py:650, in Mesh.process_geometry.<locals>._run_once(algo2d, algo3d)
    641 def _run_once(algo2d: int, algo3d: int) -> meshio.Mesh:
    642     self._initialize_mesh_settings(
    643         verbosity=verbosity,
    644         default_characteristic_length=default_characteristic_length,
   (...)    648         mesh_element_order=mesh_element_order,
    649     )
--> 650     self._apply_mesh_refinement(
    651         background_remeshing_file=background_remeshing_file,
    652         boundary_delimiter=boundary_delimiter,
    653         resolution_specs=resolution_specs,
    654         interface_delimiter=interface_delimiter,
    655     )
    656     return self.process_mesh(
    657         dim=dim,
    658         global_3D_algorithm=algo3d,
   (...)    664         post_3d_hook=post_3d_hook,
    665     )

File ~/work/meshwell/meshwell/meshwell/mesh.py:180, in Mesh._apply_mesh_refinement(self, background_remeshing_file, boundary_delimiter, resolution_specs, interface_delimiter)
    175 """Apply mesh refinement settings.
    176 
    177 TODO: enable simultaneous background mesh and entity-based refinement
    178 """
    179 if background_remeshing_file is None:
--> 180     self._apply_entity_refinement(
    181         boundary_delimiter, resolution_specs, interface_delimiter
    182     )
    183 else:
    184     self._apply_background_refinement()

File ~/work/meshwell/meshwell/meshwell/mesh.py:379, in Mesh._apply_entity_refinement(self, boundary_delimiter, resolution_specs, interface_delimiter)
    375 constant_collector = defaultdict(lambda: defaultdict(list))
    377 for entity in final_entity_list:
    378     refinement_field_indices.extend(
--> 379         entity.add_refinement_fields_to_model(
    380             final_entity_dict,
    381             boundary_delimiter,
    382             constant_collector=constant_collector,
    383             tag_to_entity_names=tag_to_entity_names,
    384         )
    385     )
    387 # Process constant fields in batches
    388 for resolution, entity_types in constant_collector.items():

File ~/work/meshwell/meshwell/meshwell/_mesh_entity.py:475, in _MeshEntity.add_refinement_fields_to_model(self, all_entities_dict, boundary_delimiter, constant_collector, tag_to_entity_names)
    471     constant_collector[resolutionspec.resolution][
    472         resolutionspec.entity_str
    473     ].extend(entities_mass_dict_sharing.keys())
    474 else:
--> 475     field_idx = resolutionspec.apply(
    476         model=self.model,
    477         entities_mass_dict=entities_mass_dict_sharing,
    478         restrict_to_str=restrict_to_str,  # RegionsList or SurfaceLists, depends on model dimensionality
    479         restrict_to_tags=restrict_to_tags,
    480     )
    481     if field_idx is not None:
    482         refinement_field_indices.append(field_idx)

File ~/work/meshwell/meshwell/.venv/lib/python3.13/site-packages/pydantic/main.py:1042, in BaseModel.__getattr__(self, item)
   1039     return super().__getattribute__(item)  # Raises AttributeError if appropriate
   1040 else:
   1041     # this is the current error
-> 1042     raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')

AttributeError: 'ResolutionSpec' object has no attribute 'apply'

The per-group quality report shows:

  • Aspect ratios (min/max/mean) for each physical group

  • Areas or volumes statistics per group

  • Minimum angles per group

  • Warnings for elements with poor quality in specific groups

This helps you:

  • Identify which regions need refinement

  • Compare quality across different mesh zones

  • Focus optimization efforts on problematic areas

  • Understand quality distribution in multi-material or multi-physics simulations

Interpreting the Quality Report#

The final quality report categorizes meshes as:

  • ✅ EXCELLENT: No critical issues, mesh ready for simulation

  • ⚠️ GOOD: Minor issues present but mesh is usable

  • ❌ POOR: Critical issues that may prevent convergence

Common issues and solutions:

Issue

Cause

Solution

High aspect ratios

Stretched elements near boundaries

Refine mesh or use boundary layers

Degenerate elements

Overlapping nodes or zero volume

Check geometry for self-intersections

Extreme edge ratios

Mixed coarse/fine regions

Use gradual size transitions

Very small angles

Sharp geometric features

Smooth geometry or use local refinement

Non-manifold edges

T-junctions or inconsistent connectivity

Fix geometry or remesh

# Clean up files

for f in [
    "quality_test.xao",
    "quality_test.msh",
    "multi_region.xao",
    "multi_region.msh",
    "poor_quality.xao",
    "poor_quality.msh",
    "quality_3d.xao",
    "quality_3d.msh",
]:
    Path(f).unlink(missing_ok=True)