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:
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: 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)