Mesh Geometry System Audit¶
Implementation Location (Updated 2025-09-29)¶
Current Status: Mesh generation functions reorganized into specialized modules:
Cartesian:
src/underworld3/meshing/cartesian.py- UnstructuredSimplexBox, StructuredQuadBox, BoxInternalBoundarySpherical:
src/underworld3/meshing/spherical.py- SphericalShell, SphericalShellInternalBoundary, SegmentofSphere, CubedSphereAnnulus:
src/underworld3/meshing/annulus.py- Annulus, QuarterAnnulus, SegmentofAnnulus, AnnulusWithSpokes, AnnulusInternalBoundary, DiscInternalBoundariesGeographic:
src/underworld3/meshing/geographic.py- RegionalSphericalBoxSegmented:
src/underworld3/meshing/segmented.py- SegmentedSphericalSurface2D, SegmentedSphericalShell, SegmentedSphericalBall
All functions maintain backward compatibility via src/underworld3/meshing/__init__.py imports.
Current Mesh Types and Their Geometric Definitions¶
Cartesian Meshes¶
UnstructuredSimplexBox (
CoordinateSystemType.CARTESIAN)StructuredQuadBox (
CoordinateSystemType.CARTESIAN)BoxInternalBoundary (
CoordinateSystemType.CARTESIAN)
Confirmed unit vectors: ✅
unit_e_0= [1, 0] or [1, 0, 0] (x-direction)unit_e_1= [0, 1] or [0, 1, 0] (y-direction)unit_e_2= [0, 0, 1] (z-direction, 3D only)
Implementation: _rRotN = sympy.eye(mesh.dim) (identity matrix)
Proposed standardization:
@property
def unit_horizontal(self):
return self.CoordinateSystem.unit_e_0 # Primary horizontal
@property
def unit_vertical(self):
return self.CoordinateSystem.unit_e_1 # Upward direction (2D)
# return self.CoordinateSystem.unit_e_2 # Upward direction (3D)
Cylindrical/Polar Meshes¶
Annulus (
CoordinateSystemType.CYLINDRICAL2D)QuarterAnnulus (
CoordinateSystemType.CYLINDRICAL2D)SegmentofAnnulus (
CoordinateSystemType.CYLINDRICAL2D)AnnulusWithSpokes (
CoordinateSystemType.CYLINDRICAL2D)AnnulusInternalBoundary (
CoordinateSystemType.CYLINDRICAL2D)
Confirmed unit vectors: ✅
unit_e_0= [cos(θ), sin(θ)] (radial direction, outward from center)unit_e_1= [-sin(θ), cos(θ)] (tangential direction, counter-clockwise)
Implementation:
_rRotN = sympy.Matrix([
[sympy.cos(theta), sympy.sin(theta)],
[-sympy.sin(theta), sympy.cos(theta)]
])
# where theta = sympy.atan2(y, x)
Proposed standardization:
@property
def unit_radial(self):
return self.CoordinateSystem.unit_e_0
@property
def unit_tangential(self):
return self.CoordinateSystem.unit_e_1
@property
def unit_vertical(self):
# INTENTIONAL: "vertical" enables coordinate-system-independent mathematics
# In 2D annulus context, "vertical" means Cartesian y-direction
# This allows writing: force = gravity * mesh.unit_vertical
# The same code works across different mesh types with appropriate interpretation
return sympy.Matrix([0, 1]) # Cartesian vertical interpretation
Spherical Meshes¶
CubedSphere (
CoordinateSystemType.SPHERICAL)RegionalSphericalBox (
CoordinateSystemType.SPHERICALorSPHERICAL_NATIVE)SegmentofSphere (
CoordinateSystemType.SPHERICAL)
Confirmed unit vectors: ✅
unit_e_0= [x/r, y/r, z/r] (radial direction, outward from center)unit_e_1= spherical theta direction (colatitude/meridional)unit_e_2= spherical phi direction (azimuthal/longitude)
Implementation:
# For SPHERICAL coordinate system:
_rRotN = sympy.Matrix([
[x/r, y/r, z/r], # radial
[theta_direction_components], # meridional
[phi_direction_components] # azimuthal
])
# where r = sqrt(x² + y² + z²)
Proposed standardization:
@property
def unit_radial(self):
return self.CoordinateSystem.unit_e_0
@property
def unit_meridional(self):
return self.CoordinateSystem.unit_e_1
@property
def unit_azimuthal(self):
return self.CoordinateSystem.unit_e_2
@property
def unit_vertical(self):
# "Vertical" means "upward" consistently across all coordinate systems
# In spherical: radially outward from center = "up" (away from planet center)
# This maintains consistency with Cartesian "up" direction
return self.CoordinateSystem.unit_e_0 # Radially outward (upward)
Design Philosophy: Mathematical Independence¶
INTENTIONAL: Coordinate-System-Independent Mathematics¶
The apparent “ambiguity” of terms like “vertical” is actually intentional design to enable mathematical expressions that work across different coordinate systems:
# This code works regardless of mesh type:
gravitational_force = density * gravity * mesh.unit_vertical
buoyancy_force = density_difference * gravity * mesh.unit_vertical
thermal_diffusion = temperature.diff(mesh.unit_vertical)
# Different interpretations by coordinate system:
# - Cartesian: unit_vertical = [0,1] or [0,0,1] (y or z direction)
# - Spherical: unit_vertical = unit_radial (outward from center)
# - Annulus: unit_vertical = [0,1] (Cartesian y-direction)
Benefits:
Mathematical Abstraction: Write physics equations once, work everywhere
Code Reuse: Same solver code works with different mesh geometries
Physical Intuition: “Vertical” always means “upward” regardless of coordinate system
Flexibility: Mesh determines appropriate geometric interpretation
Example: Gravity-Driven Flow¶
# Works for any mesh type - Cartesian, spherical, annulus, etc.
def setup_gravity_flow(mesh, density_field, gravity_magnitude):
gravity_vector = gravity_magnitude * mesh.unit_vertical
body_force = density_field * gravity_vector
return body_force
This approach prioritizes mathematical elegance and coordinate-system independence over rigid geometric specificity.
Proposed Mesh Interface¶
Base Mesh Geometric Interface¶
class Mesh:
# Required geometric properties (all meshes must implement)
@property
def geometric_dimension_names(self):
"""Return names of geometric dimensions for this mesh type"""
raise NotImplementedError
@property
def primary_directions(self):
"""Return dict of primary geometric directions"""
raise NotImplementedError
# Sampling interface
def create_line_sample(self, start, direction_expr, length, num_points=50):
"""Create sample points along sympy-defined direction"""
# Generic implementation using coordinate system
def create_profile_sample(self, profile_type, **params):
"""Create sample for common profile types"""
# Mesh-specific implementation
Mesh-Specific Implementations¶
Cartesian Meshes¶
class StructuredQuadBox(Mesh):
@property
def geometric_dimension_names(self):
return ['horizontal', 'vertical'] if self.dim == 2 else ['horizontal_x', 'horizontal_y', 'vertical']
@property
def primary_directions(self):
if self.dim == 2:
return {
'unit_horizontal': self.CoordinateSystem.unit_e_0,
'unit_vertical': self.CoordinateSystem.unit_e_1,
'unit_x': self.CoordinateSystem.unit_e_0, # alias
'unit_y': self.CoordinateSystem.unit_e_1 # alias
}
else: # 3D
return {
'unit_horizontal_x': self.CoordinateSystem.unit_e_0,
'unit_horizontal_y': self.CoordinateSystem.unit_e_1,
'unit_vertical': self.CoordinateSystem.unit_e_2,
'unit_x': self.CoordinateSystem.unit_e_0, # alias
'unit_y': self.CoordinateSystem.unit_e_1, # alias
'unit_z': self.CoordinateSystem.unit_e_2 # alias
}
def create_profile_sample(self, profile_type, **params):
if profile_type == 'vertical':
x_pos = params.get('x_position', 0.5)
y_range = params.get('range', (self.minCoords[1], self.maxCoords[1]))
return self.create_line_sample(
start=[x_pos, y_range[0]],
direction_expr=self.primary_directions['unit_vertical'],
length=y_range[1] - y_range[0],
num_points=params.get('num_points', 50)
)
elif profile_type == 'horizontal':
y_pos = params.get('y_position', 0.5)
x_range = params.get('range', (self.minCoords[0], self.maxCoords[0]))
return self.create_line_sample(
start=[x_range[0], y_pos],
direction_expr=self.primary_directions['unit_horizontal'],
length=x_range[1] - x_range[0],
num_points=params.get('num_points', 50)
)
# etc.
Annulus Meshes¶
class Annulus(Mesh):
@property
def geometric_dimension_names(self):
return ['radial', 'tangential']
@property
def primary_directions(self):
return {
'unit_radial': self.CoordinateSystem.unit_e_0,
'unit_tangential': self.CoordinateSystem.unit_e_1,
'unit_vertical': sympy.Matrix([0, 1]), # Cartesian vertical
}
def create_profile_sample(self, profile_type, **params):
if profile_type == 'radial':
theta = params.get('theta', 0)
r_range = params.get('range', (self.r_inner, self.r_outer))
start_point = [r_range[0] * sympy.cos(theta), r_range[0] * sympy.sin(theta)]
direction = sympy.Matrix([sympy.cos(theta), sympy.sin(theta)])
return self.create_line_sample(
start=start_point,
direction_expr=direction,
length=r_range[1] - r_range[0],
num_points=params.get('num_points', 50)
)
elif profile_type == 'tangential':
radius = params.get('radius', (self.r_inner + self.r_outer)/2)
theta_range = params.get('range', (0, 2*sympy.pi))
# Arc sampling implementation
# ...
Action Plan¶
1. Immediate: Standardize Existing Properties¶
# Add to all mesh types as properties
@property
def unit_vertical(self):
# Mesh-specific implementation
@property
def geometric_directions(self):
# Return dict of available directions for this mesh type
2. Implement Generic Line Sampling¶
def create_line_sample(self, start, direction_expr, length, num_points=50):
"""Generic implementation using sympy evaluation"""
# This can be implemented once in base class
3. Update Visualization Library¶
# Updated parallel visualization
uw.visualization.parallel.parallel_profile_plot(
field, mesh,
profile_spec={'type': 'vertical', 'x_position': 0.5},
title="Vertical Profile"
)
Audit Results Summary¶
✅ Current System Strengths¶
Complete unit vector foundation: All mesh types have proper
unit_e_0,unit_e_1,unit_e_2definitionsConsistent coordinate system architecture: Each mesh type correctly implements
CoordinateSystemTypeProper transformation matrices:
_rRotNmatrices correctly transform between natural and Cartesian coordinatesBoundary normal integration: Existing boundary_normals use
unit_e_0appropriately (e.g., radial boundaries)Real-world usage: Examples like
Ex_Stokes_Disk_CylCoords.pydemonstrateunit_e_0as radial direction
✅ IMPLEMENTED: Geometric Direction Properties (Verified 2025-09-29)¶
✅ High-level geometric properties:
unit_vertical,unit_horizontal,unit_radial,unit_tangentialavailable incoordinates.py✅ Geometric dimension naming:
geometric_dimension_namesproperty exposes natural dimension names✅ Complete property dictionary:
primary_directionsprovides programmatic access to all available directions✅ Type-aware properties: Coordinate-system-specific properties with appropriate error handling
✅ Backward compatibility: All existing
unit_e_0etc. usage continues to work unchanged
✅ IMPLEMENTED: Complete Sampling Infrastructure (Verified 2025-09-29)¶
✅ Generic line sampling:
create_line_sample()implemented incoordinates.py:646+✅ Mesh-specific profile sampling:
create_profile_sample()implemented incoordinates.py:769+✅ Dual coordinate output: Both Cartesian (for
global_evaluate()) and natural (for plotting) coordinates✅ Coordinate conversion: Automatic transformation between Cartesian and natural coordinate systems
🔧 Implementation Status: GEOMETRY COMPLETE ✅ | SAFETY PENDING ⚠️¶
✅ ~~Geometric direction standardization~~: COMPLETED - All mesh-specific properties implemented
✅ ~~Sampling infrastructure~~: COMPLETED - Full sampling system with
global_evaluate()integration✅ ~~Dimension naming~~: COMPLETED -
geometric_dimension_namesproperty implemented✅ ~~2D vs 3D handling~~: COMPLETED - Clear conventions implemented for all dimensions
⚠️ Boundary name safety: CRITICAL ISSUE - Case sensitivity causes PETSc crashes (detailed above)
📋 Semantic boundary labels: PLANNED - Coordinate-independent boundary naming system
📊 Coordinate System Validation¶
All coordinate systems properly implemented:
✅ CARTESIAN: Identity matrix, standard x/y/z directions
✅ CYLINDRICAL2D: Proper radial/tangential transformation matrix
✅ SPHERICAL: Complete radial/meridional/azimuthal basis
✅ Native coordinate variants: Proper handling of natural coordinate storage
This approach:
✅ Builds on existing system (
unit_e_0,unit_e_1, etc.)✅ Uses sympy expressions for geometric definitions
✅ Mesh-centric authority over geometry
✅ Handles 2D/3D through geometric naming conventions
✅ Extensible to new mesh types
✅ Backward compatible with existing code
⚠️ Critical Issue: Boundary Name Case Sensitivity¶
Current Risk: Silent Failure → PETSc Crash¶
Problem: Boundary condition names are case-sensitive with no validation, leading to catastrophic failures:
# THIS WORKS:
solver.add_dirichlet_bc(1.0, "Top")
# THIS CRASHES PETSc:
solver.add_dirichlet_bc(1.0, "top") # Wrong case
solver.add_dirichlet_bc(1.0, "TOP") # Wrong case
solver.add_dirichlet_bc(1.0, "Topp") # Typo
Technical Root Cause¶
Location: petsc_generic_snes_solvers.pyx:706, 746, 1291, etc.
# Direct dictionary lookup with no error handling:
value = mesh.boundaries[bc.boundary].value # KeyError if wrong case → PETSc crash
Failure Chain:
User passes wrong case:
"top"instead of"Top"Python dictionary lookup fails:
KeyError: 'top'Exception propagates to PETSc C code
PETSc crashes with unhelpful error message
User gets cryptic crash, no indication of boundary name issue
Impact Assessment¶
Silent Failures: No validation means typos go undetected until runtime crash
Debugging Nightmare: PETSc crashes don’t indicate boundary name problems
User Experience: Extremely frustrating for new users
Production Risk: Models can fail catastrophically in batch runs
Proposed Solutions¶
Short-Term Fix: Better Error Messages¶
# In add_condition() method, add validation:
if bc.boundary not in mesh.boundaries:
available = list(mesh.boundaries.keys())
raise ValueError(f"Boundary '{bc.boundary}' not found. "
f"Available boundaries: {available}. "
f"Note: boundary names are case-sensitive.")
Medium-Term: Case-Insensitive Lookup¶
# Create case-insensitive boundary accessor:
def get_boundary(mesh, name):
# Try exact match first
if name in mesh.boundaries:
return mesh.boundaries[name]
# Try case-insensitive match
name_lower = name.lower()
matches = [k for k in mesh.boundaries.keys() if k.lower() == name_lower]
if len(matches) == 1:
return mesh.boundaries[matches[0]]
elif len(matches) > 1:
raise ValueError(f"Ambiguous boundary name '{name}'. Matches: {matches}")
else:
raise ValueError(f"Boundary '{name}' not found. Available: {list(mesh.boundaries.keys())}")
Long-Term: Standardized Boundary Names¶
Implement semantic boundary aliases (see next section)
Consistent naming conventions across mesh types
Automatic validation during mesh creation
Boundary Naming Standardization Challenge¶
Current State: Mesh-Specific Names¶
Different mesh types use different boundary naming conventions:
Mesh Type |
“Upper” Boundary |
“Lower” Boundary |
Sides |
|---|---|---|---|
Cartesian |
|
|
|
Spherical |
|
|
|
Annulus |
|
|
Various angular boundaries |
Proposed: Semantic Boundary Aliases¶
Goal: Enable coordinate-independent boundary specifications:
# This should work for any mesh type:
bc_bottom = uw.DirichletBC(mesh.boundary_labels.gravity_aligned_lower, value=0)
bc_top = uw.DirichletBC(mesh.boundary_labels.gravity_aligned_upper, value=1)
# Equivalent coordinate-specific access still available:
bc_cartesian = uw.DirichletBC(mesh.boundaries.Top, value=1) # Cartesian
bc_spherical = uw.DirichletBC(mesh.boundaries.Upper, value=1) # Spherical
bc_annulus = uw.DirichletBC(mesh.boundaries.Outer, value=1) # Annulus
Technical Challenge: gmsh + PETSc Label Integration¶
Issue: PETSc supports duplicate boundary labels, but gmsh integration complicates this:
PETSc Capability: Can create multiple labels for the same boundary
// PETSc can handle: DMSetLabelValue(dm, "Top", point, 1); // gmsh-specific name DMSetLabelValue(dm, "gravity_upper", point, 1); // semantic alias
gmsh Integration: Generates mesh with specific boundary names
gmsh creates boundaries with fixed names during mesh generation
Post-processing needed to add semantic aliases
Implementation Strategy¶
Phase 1: Post-Processing Semantic Labels¶
# After gmsh mesh creation, add semantic aliases:
def add_semantic_boundary_labels(mesh):
"""Add coordinate-independent boundary aliases"""
# Cartesian mesh
if mesh.coordinate_system_type == CoordinateSystemType.CARTESIAN:
mesh.add_boundary_alias("gravity_aligned_upper", "Top")
mesh.add_boundary_alias("gravity_aligned_lower", "Bottom")
mesh.add_boundary_alias("horizontal_left", "Left")
mesh.add_boundary_alias("horizontal_right", "Right")
# Spherical mesh
elif mesh.coordinate_system_type == CoordinateSystemType.SPHERICAL:
mesh.add_boundary_alias("gravity_aligned_upper", "Upper")
mesh.add_boundary_alias("gravity_aligned_lower", "Lower")
mesh.add_boundary_alias("meridional_north", "North")
mesh.add_boundary_alias("meridional_south", "South")
# Annulus mesh
elif mesh.coordinate_system_type == CoordinateSystemType.CYLINDRICAL2D:
mesh.add_boundary_alias("radial_outer", "Outer")
mesh.add_boundary_alias("radial_inner", "Inner")
# gravity_aligned depends on orientation
Phase 2: Enhanced API Access¶
class Mesh:
@property
def boundary_labels(self):
"""Access semantic boundary labels"""
return self._semantic_boundaries
def add_boundary_alias(self, semantic_name, gmsh_name):
"""Create semantic alias for existing gmsh boundary"""
if gmsh_name not in self.boundaries:
raise ValueError(f"Boundary '{gmsh_name}' not found")
# Use PETSc label duplication
original_value = self.boundaries[gmsh_name].value
self.dm.setLabelValue(semantic_name, original_value)
self._semantic_boundaries[semantic_name] = self.boundaries[gmsh_name]
Phase 3: Validation and Safety¶
def validate_boundary_access(mesh, boundary_name):
"""Validate boundary exists and provide helpful errors"""
# Check exact match
if boundary_name in mesh.boundaries:
return mesh.boundaries[boundary_name]
# Check semantic aliases
if hasattr(mesh, '_semantic_boundaries') and boundary_name in mesh._semantic_boundaries:
return mesh._semantic_boundaries[boundary_name]
# Provide helpful error with suggestions
all_boundaries = list(mesh.boundaries.keys())
if hasattr(mesh, '_semantic_boundaries'):
all_boundaries.extend(mesh._semantic_boundaries.keys())
# Suggest case-insensitive matches
suggestions = [b for b in all_boundaries if b.lower() == boundary_name.lower()]
error_msg = f"Boundary '{boundary_name}' not found.\n"
error_msg += f"Available boundaries: {all_boundaries}\n"
if suggestions:
error_msg += f"Did you mean: {suggestions}? (case-sensitive)"
raise ValueError(error_msg)
Benefits of This Approach¶
Backward Compatibility: Existing gmsh-specific names continue to work
Mathematical Independence: Same BC code works across mesh types
Gradual Migration: Can implement incrementally without breaking changes
Extensible: Easy to add new semantic categories as needed
Validation: Centralized error handling with helpful messages
⚠️ Future Work: Internal Surface Normal Orientation¶
Status: Planning - May require PETSc team discussion
Date Added: 2025-01-18
Related: docs/developer/design/PROJECTED_NORMALS_API_DESIGN.md
Background¶
Investigation into boundary normal accuracy for curved surfaces revealed:
Boundary surfaces: PETSc handles orientation correctly for surfaces that lie on the domain boundary
Internal surfaces: Orientation is problematic for internal surfaces (e.g., fault planes, material interfaces)
The Problem¶
For boundary surfaces (e.g., outer surface of a mesh):
PETSc provides consistent outward-pointing normals via
mesh.GammaThese are facet-based but correctly oriented
For internal surfaces (e.g., embedded faults, material interfaces):
PETSc may not provide consistent orientation
No guaranteed “outward” direction for internal surfaces
This affects:
Free-slip conditions on fault planes
Flux conditions across material interfaces
Traction boundary conditions on embedded surfaces
Proposed Solutions¶
Option 1: User-Specified Orientation Vector¶
# User provides reference direction for orientation
n_proj = mesh.project_surface_normals(
surfaces=["Fault"],
orientation_reference=sympy.Matrix([1, 0]), # User-specified
)
Pros: Explicit, user controls behavior Cons: Extra parameter required, may be confusing
Option 2: PETSc Enhancement Request¶
Discuss with PETSc team about:
Consistent internal surface labeling
Orientation metadata in DMPlex labels
API for specifying surface orientation
Option 3: Post-Processing Heuristics¶
# Detect and correct orientation based on geometry
def correct_internal_orientation(normals, surface_points, reference_point):
"""Ensure normals point away from reference point."""
for i, (n, p) in enumerate(zip(normals, surface_points)):
direction = p - reference_point
if np.dot(n, direction) < 0:
normals[i] = -n
return normals
Pros: Automatic for simple cases Cons: Fails for complex geometries
Action Items¶
Document Use Cases: Gather specific examples where internal surface orientation matters
Evaluate PETSc Capabilities: Check current PETSc features for internal surface handling
PETSc Team Discussion: If needed, prepare feature request or discussion topic
Interim Solution: Implement user-specified orientation as immediate workaround
Notes¶
This issue was identified during investigation of boundary normal accuracy on elliptical meshes. While projected normals significantly improve accuracy for curved boundaries (~99.8% improvement over raw facet normals), the approach relies on consistent orientation from PETSc, which is only guaranteed for domain boundary surfaces.
Added 2025-01-18: Investigation into mesh.Gamma normalization and projected normals