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:
File Check: Verifies the mesh file exists and is readable
Mesh Parsing: Loads nodes, elements, and physical groups from GMSH format
Connectivity: Checks for orphaned nodes, non-manifold edges/faces
Geometric Quality: Analyzes aspect ratios, volumes/areas, angles, edge lengths
Physical Regions: Reports element counts per physical group
Contacts/Boundaries: Lists physical groups by dimension
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)