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 import cad
from meshwell.mesh import mesh
from meshwell.polysurface import PolySurface
from meshwell.quality import MeshQualityAnalyzer, main
from meshwell.resolution import ResolutionSpec

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
cad(entities_list=[entity], output_file="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    : Writing 'quality_test.xao'...
Info    : Done writing 'quality_test.xao'
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: 9424 bytes
✓ File is readable, first 10 lines preview:
   1: $MeshFormat
   2: 4.1 0 8
   3: $EndMeshFormat
   4: $PhysicalNames
   5: 2
   6: 1 2 "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 2 physical regions
✓ Detected mesh dimension: 2D

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

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.70e-01
  Max area: 5.60e-01
  Mean area: 4.07e-01
  Area ratio (max/min): 2.08e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.44
  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.26e-01
  Max edge length: 1.24e+00
  Edge length ratio: 1.71e+00
Angle statistics:
  Min angle: 43.7°
  Max angle: 84.7°

=== 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.44, mean=1.12
  Area: min=2.70e-01, max=5.60e-01, mean=4.07e-01
  Min angle: 43.7°

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

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

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

✅ 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: 9424 bytes
✓ File is readable, first 10 lines preview:
   1: $MeshFormat
   2: 4.1 0 8
   3: $EndMeshFormat
   4: $PhysicalNames
   5: 2
   6: 1 2 "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 2 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.70e-01
  Max area: 5.60e-01
  Mean area: 4.07e-01
  Area ratio (max/min): 2.08e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.44
  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.26e-01
  Max edge length: 1.24e+00
  Edge length ratio: 1.71e+00
Angle statistics:
  Min angle: 43.7°
  Max angle: 84.7°

Aspect ratio range: 1.00 - 1.44
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)

cad(entities_list=[region1, region2], output_file="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    : [  0%] Difference                                                                                  
Info    : [ 10%] Difference                                                                                  
Info    : [ 20%] Difference                                                                                  
Info    : [ 30%] Difference                                                                                  
Info    : [ 40%] Difference                                                                                  
Info    : [ 50%] Difference - Performing Face-Face intersection                                                                                
Info    : [ 70%] Difference                                                                                  
Info    : [ 80%] Difference - Splitting faces                                                                                
                                                                                
Info    : [  0%] Fragments                                                                                  
Info    : [ 10%] Fragments                                                                                  
Info    : [ 20%] Fragments                                                                                  
Info    : [ 30%] Fragments                                                                                  
Info    : [ 40%] Fragments                                                                                  
Info    : [ 50%] Fragments - Performing Face-Face intersection                                                                                
Info    : [ 70%] Fragments                                                                                  
Info    : [ 80%] Fragments - Splitting faces                                                                                
                                                                                
Info    : [  0%] Fragments                                                                                  
Info    : [ 10%] Fragments                                                                                  
Info    : [ 20%] Fragments                                                                                  
Info    : [ 30%] Fragments                                                                                  
Info    : [ 40%] Fragments                                                                                  
Info    : [ 50%] Fragments - Performing Face-Face intersection                                                                                
Info    : [ 70%] Fragments                                                                                  
Info    : [ 80%] Fragments - Splitting faces                                                                                
                                                                                
Info    : Writing 'multi_region.xao'...
Info    : Done writing 'multi_region.xao'
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
     12 fine_spec = ResolutionSpec(resolution=0.3, apply_to="surfaces")
     13 coarse_spec = ResolutionSpec(resolution=1.5, apply_to="surfaces")
---> 15 multi_mesh = mesh(
     16     dim=2,
     17     input_file="multi_region.xao",
     18     output_file="multi_region.msh",
     19     default_characteristic_length=1.0,
     20     resolution_specs={
     21         "fine_region": [fine_spec],
     22         "coarse_region": [coarse_spec],
     23     },
     24 )
     26 print(f"Multi-region mesh: {len(multi_mesh.points)} vertices")
     28 # Analyze with per-group reporting

File ~/work/meshwell/meshwell/meshwell/mesh.py:449, in mesh(dim, input_file, output_file, default_characteristic_length, 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, gmsh_version)
    446 mesh_generator.load_xao_file(input_file)
    448 # Process geometry into mesh
--> 449 mesh_obj = mesh_generator.process_geometry(
    450     dim=dim,
    451     background_remeshing_file=background_remeshing_file,
    452     default_characteristic_length=default_characteristic_length,
    453     global_scaling=global_scaling,
    454     global_2D_algorithm=global_2D_algorithm,
    455     global_3D_algorithm=global_3D_algorithm,
    456     mesh_element_order=mesh_element_order,
    457     verbosity=verbosity,
    458     periodic_entities=periodic_entities,
    459     optimization_flags=optimization_flags,
    460     boundary_delimiter=boundary_delimiter,
    461     resolution_specs=resolution_specs,
    462     gmsh_version=gmsh_version,
    463 )
    465 # Save to file
    466 mesh_generator.save_to_file(output_file)

File ~/work/meshwell/meshwell/meshwell/mesh.py:373, 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)
    363 self._initialize_mesh_settings(
    364     verbosity=verbosity,
    365     default_characteristic_length=default_characteristic_length,
   (...)    369     mesh_element_order=mesh_element_order,
    370 )
    372 # Apply mesh refinement
--> 373 self._apply_mesh_refinement(
    374     background_remeshing_file=background_remeshing_file,
    375     boundary_delimiter=boundary_delimiter,
    376     resolution_specs=resolution_specs,
    377 )
    379 # Generate and return mesh
    380 return self.process_mesh(
    381     dim=dim,
    382     global_3D_algorithm=global_3D_algorithm,
   (...)    385     optimization_flags=optimization_flags,
    386 )

File ~/work/meshwell/meshwell/meshwell/mesh.py:122, in Mesh._apply_mesh_refinement(self, background_remeshing_file, boundary_delimiter, resolution_specs)
    117 """Apply mesh refinement settings.
    118 
    119 TODO: enable simultaneous background mesh and entity-based refinement
    120 """
    121 if background_remeshing_file is None:
--> 122     self._apply_entity_refinement(boundary_delimiter, resolution_specs)
    123 else:
    124     self._apply_background_refinement()

File ~/work/meshwell/meshwell/meshwell/mesh.py:221, in Mesh._apply_entity_refinement(self, boundary_delimiter, resolution_specs)
    217         refinement_field_indices.append(field_index)
    219 for entity in final_entity_list:
    220     refinement_field_indices.extend(
--> 221         entity.add_refinement_fields_to_model(
    222             final_entity_dict,
    223             boundary_delimiter,
    224         )
    225     )
    227 # If we have refinement fields, create a minimum field
    228 if refinement_field_indices:
    229     # Use the smallest element size overall

File ~/work/meshwell/meshwell/meshwell/labeledentity.py:376, in LabeledEntities.add_refinement_fields_to_model(self, all_entities_dict, boundary_delimiter)
    372             restrict_to_str = "PointsList"
    374         if entities_mass_dict_sharing:
    375             refinement_field_indices.append(
--> 376                 resolutionspec.apply(
    377                     model=self.model,
    378                     entities_mass_dict=entities_mass_dict_sharing,
    379                     restrict_to_str=restrict_to_str,  # RegionsList or SurfaceLists, depends on model dimensionality
    380                     restrict_to_tags=restrict_to_tags,
    381                 )
    382             )
    384 return refinement_field_indices

File ~/work/meshwell/meshwell/.venv/lib/python3.12/site-packages/pydantic/main.py:1026, in BaseModel.__getattr__(self, item)
   1023     return super().__getattribute__(item)  # Raises AttributeError if appropriate
   1024 else:
   1025     # this is the current error
-> 1026     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)