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