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 143 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: 9320 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 143 nodes
✓ Parsed 0 tetrahedra
✓ Parsed 244 triangular elements
✓ Parsed 0 surface elements
✓ Found 2 physical regions
✓ Detected mesh dimension: 2D

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

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.80e-01
  Mean area: 4.10e-01
  Area ratio (max/min): 2.18e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.43
  Mean aspect ratio: 1.11
  Quality distribution:
    Excellent (AR < 2): 244 (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.48e-01
  Max edge length: 1.23e+00
  Edge length ratio: 1.64e+00
Angle statistics:
  Min angle: 43.8°
  Max angle: 86.4°

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

=== Quality Metrics Per Physical Group ===

test_region (tag=1, 244 elements):
  Aspect ratio: min=1.00, max=1.43, mean=1.11
  Area: min=2.66e-01, max=5.80e-01, mean=4.10e-01
  Min angle: 43.8°

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

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

=== Manual Analysis ===

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

=== Geometric Quality Analysis ===
Area statistics:
  Min area: 2.66e-01
  Max area: 5.80e-01
  Mean area: 4.10e-01
  Area ratio (max/min): 2.18e+00
Aspect ratio statistics:
  Min aspect ratio: 1.00
  Max aspect ratio: 1.43
  Mean aspect ratio: 1.11
  Quality distribution:
    Excellent (AR < 2): 244 (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.48e-01
  Max edge length: 1.23e+00
  Edge length ratio: 1.64e+00
Angle statistics:
  Min angle: 43.8°
  Max angle: 86.4°

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

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%] Fragments                                                                                  
Info    : [ 10%] Fragments                                                                                  
Info    : [ 20%] Fragments - Performing Vertex-Face intersection                                                                                
Info    : [ 30%] Fragments                                                                                  
Info    : [ 40%] Fragments                                                                                  
Info    : [ 50%] Fragments                                                                                  
Info    : [ 60%] 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
     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:485, 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, gmsh_version)
    482     mesh_generator.load_xao_file(input_file)
    484 # Process geometry into mesh
--> 485 mesh_obj = mesh_generator.process_geometry(
    486     dim=dim,
    487     background_remeshing_file=background_remeshing_file,
    488     default_characteristic_length=default_characteristic_length,
    489     global_scaling=global_scaling,
    490     global_2D_algorithm=global_2D_algorithm,
    491     global_3D_algorithm=global_3D_algorithm,
    492     mesh_element_order=mesh_element_order,
    493     verbosity=verbosity,
    494     periodic_entities=periodic_entities,
    495     optimization_flags=optimization_flags,
    496     boundary_delimiter=boundary_delimiter,
    497     resolution_specs=resolution_specs,
    498     gmsh_version=gmsh_version,
    499 )
    501 # Save to file if output file provided
    502 if output_file is not None:

File ~/work/meshwell/meshwell/meshwell/mesh.py:409, 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)
    399 self._initialize_mesh_settings(
    400     verbosity=verbosity,
    401     default_characteristic_length=default_characteristic_length,
   (...)    405     mesh_element_order=mesh_element_order,
    406 )
    408 # Apply mesh refinement
--> 409 self._apply_mesh_refinement(
    410     background_remeshing_file=background_remeshing_file,
    411     boundary_delimiter=boundary_delimiter,
    412     resolution_specs=resolution_specs,
    413 )
    415 # Generate and return mesh
    416 return self.process_mesh(
    417     dim=dim,
    418     global_3D_algorithm=global_3D_algorithm,
   (...)    421     optimization_flags=optimization_flags,
    422 )

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:235, in Mesh._apply_entity_refinement(self, boundary_delimiter, resolution_specs)
    231 constant_collector = defaultdict(lambda: defaultdict(list))
    233 for entity in final_entity_list:
    234     refinement_field_indices.extend(
--> 235         entity.add_refinement_fields_to_model(
    236             final_entity_dict,
    237             boundary_delimiter,
    238             constant_collector=constant_collector,
    239             tag_to_entity_names=tag_to_entity_names,
    240         )
    241     )
    243 # Process constant fields in batches
    244 for resolution, entity_types in constant_collector.items():

File ~/work/meshwell/meshwell/meshwell/labeledentity.py:442, in LabeledEntities.add_refinement_fields_to_model(self, all_entities_dict, boundary_delimiter, constant_collector, tag_to_entity_names)
    437                 constant_collector[resolutionspec.resolution][
    438                     resolutionspec.entity_str
    439                 ].extend(entities_mass_dict_sharing.keys())
    440             else:
    441                 refinement_field_indices.append(
--> 442                     resolutionspec.apply(
    443                         model=self.model,
    444                         entities_mass_dict=entities_mass_dict_sharing,
    445                         restrict_to_str=restrict_to_str,  # RegionsList or SurfaceLists, depends on model dimensionality
    446                         restrict_to_tags=restrict_to_tags,
    447                     )
    448                 )
    450 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)