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.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

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 142 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: 10129 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 142 nodes
✓ Parsed 0 tetrahedra
✓ Parsed 242 triangular elements
✓ Parsed 0 surface elements
✓ Found 1 physical regions
✓ Detected mesh dimension: 2D

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

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.98e-01
  Mean area: 4.13e-01
  Area ratio (max/min): 2.25e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.40
  Mean aspect ratio: 1.11
  Quality distribution:
    Excellent (AR < 2): 242 (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.49e-01
  Max edge length: 1.27e+00
  Edge length ratio: 1.70e+00
Angle statistics:
  Min angle: 45.0°
  Max angle: 86.7°

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

=== Quality Metrics Per Physical Group ===

test_region (tag=1, 242 elements):
  Aspect ratio: min=1.00, max=1.40, mean=1.11
  Area: min=2.66e-01, max=5.98e-01, mean=4.13e-01
  Min angle: 45.0°

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

=== Mesh Gradation Analysis ===
Mesh gradation statistics:
  Mean size ratio: 1.08
  Max size ratio: 1.58

============================================================
  MESH QUALITY SUMMARY REPORT
============================================================
Total elements analyzed: 242
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: 10129 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 142 nodes
✓ Parsed 0 tetrahedra
✓ Parsed 242 triangular elements
✓ Parsed 0 surface elements
✓ Found 1 physical regions
✓ Detected mesh dimension: 2D

=== Manual Analysis ===

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

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.98e-01
  Mean area: 4.13e-01
  Area ratio (max/min): 2.25e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.40
  Mean aspect ratio: 1.11
  Quality distribution:
    Excellent (AR < 2): 242 (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.49e-01
  Max edge length: 1.27e+00
  Edge length ratio: 1.70e+00
Angle statistics:
  Min angle: 45.0°
  Max angle: 86.7°

Aspect ratio range: 1.00 - 1.40
Mean aspect ratio: 1.11

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    : Snapping geometry point 3 to curve (distance = 1.41421e-05)
Info    : Snapping geometry point 3 to curve (distance = 2.82843e-05)
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:648, 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)
    645     mesh_generator.load_xao_file(input_file)
    647 # Process geometry into mesh
--> 648 mesh_obj = mesh_generator.process_geometry(
    649     dim=dim,
    650     background_remeshing_file=background_remeshing_file,
    651     default_characteristic_length=default_characteristic_length,
    652     global_scaling=global_scaling,
    653     global_2D_algorithm=global_2D_algorithm,
    654     global_3D_algorithm=global_3D_algorithm,
    655     mesh_element_order=mesh_element_order,
    656     verbosity=verbosity,
    657     periodic_entities=periodic_entities,
    658     optimization_flags=optimization_flags,
    659     boundary_delimiter=boundary_delimiter,
    660     resolution_specs=resolution_specs,
    661     gmsh_version=gmsh_version,
    662     interface_delimiter=interface_delimiter,
    663 )
    665 # Save to file if output file provided
    666 if output_file is not None:

File ~/work/meshwell/meshwell/meshwell/mesh.py:552, 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)
    550 if len(attempts) == 1:
    551     algo2d, algo3d = attempts[0]
--> 552     return _run_once(algo2d, algo3d)
    554 # Multi-attempt: persist CAD before the first try so we can restore
    555 # after a failed generate() leaves gmsh in an unrecoverable "busy"
    556 # state (neither mesh.clear() nor re-setting options releases it).
    557 with tempfile.TemporaryDirectory() as tmp:

File ~/work/meshwell/meshwell/meshwell/mesh.py:536, in Mesh.process_geometry.<locals>._run_once(algo2d, algo3d)
    527 def _run_once(algo2d: int, algo3d: int) -> meshio.Mesh:
    528     self._initialize_mesh_settings(
    529         verbosity=verbosity,
    530         default_characteristic_length=default_characteristic_length,
   (...)    534         mesh_element_order=mesh_element_order,
    535     )
--> 536     self._apply_mesh_refinement(
    537         background_remeshing_file=background_remeshing_file,
    538         boundary_delimiter=boundary_delimiter,
    539         resolution_specs=resolution_specs,
    540         interface_delimiter=interface_delimiter,
    541     )
    542     return self.process_mesh(
    543         dim=dim,
    544         global_3D_algorithm=algo3d,
   (...)    547         optimization_flags=optimization_flags,
    548     )

File ~/work/meshwell/meshwell/meshwell/mesh.py:152, in Mesh._apply_mesh_refinement(self, background_remeshing_file, boundary_delimiter, resolution_specs, interface_delimiter)
    147 """Apply mesh refinement settings.
    148 
    149 TODO: enable simultaneous background mesh and entity-based refinement
    150 """
    151 if background_remeshing_file is None:
--> 152     self._apply_entity_refinement(
    153         boundary_delimiter, resolution_specs, interface_delimiter
    154     )
    155 else:
    156     self._apply_background_refinement()

File ~/work/meshwell/meshwell/meshwell/mesh.py:346, in Mesh._apply_entity_refinement(self, boundary_delimiter, resolution_specs, interface_delimiter)
    342 constant_collector = defaultdict(lambda: defaultdict(list))
    344 for entity in final_entity_list:
    345     refinement_field_indices.extend(
--> 346         entity.add_refinement_fields_to_model(
    347             final_entity_dict,
    348             boundary_delimiter,
    349             constant_collector=constant_collector,
    350             tag_to_entity_names=tag_to_entity_names,
    351         )
    352     )
    354 # Process constant fields in batches
    355 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)
    470                 constant_collector[resolutionspec.resolution][
    471                     resolutionspec.entity_str
    472                 ].extend(entities_mass_dict_sharing.keys())
    473             else:
    474                 refinement_field_indices.append(
--> 475                     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                 )
    483 return refinement_field_indices

File ~/work/meshwell/meshwell/.venv/lib/python3.12/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)