Source code for geos.processing.pre_processing.TetQualityAnalysis

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
# SPDX-FileContributor: Bertrand Denel, Paloma Martinez
import logging
import numpy as np
import numpy.typing as npt
from typing_extensions import Self, Any

from vtkmodules.vtkCommonDataModel import vtkDataSet
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Rectangle

from geos.utils.Logger import ( getLogger, Logger, CountVerbosityHandler, isHandlerInLogger, getLoggerHandlerType )
from geos.mesh.stats.tetrahedraAnalysisHelpers import ( getCoordinatesDoublePrecision, extractTetConnectivity,
                                                        analyzeAllTets, computeQualityScore )

__doc__ = """
TetQualityAnalysis module is a filter that performs an analysis of tetrahedras quality of one or several meshes and generates a plot as summary.
Metrics computed include aspect ratio, shape quality, volume, min and max edges, edge ratio, min and max dihedral angles, quality score.

The meshes are compared based on their median quality score and the change from the best one is evaluated for these metrics.

Filter input should be vtkUnstructuredGrid.


To use the filter:

.. code-block:: python

    from geos.processing.pre_processing.TetQualityAnalysis import TetQualityAnalysis

    # Filter inputs
    inputMesh: dict[str, vtkUnstructuredGrid]
    speHandler: bool # optional

    # Instantiate the filter
    tetQualityAnalysisFilter: TetQualityAnalysis = TetQualityAnalysis( inputMesh, speHandler )

    # Use your own handler (if speHandler is True)
    yourHandler: logging.Handler
    tetQualityAnalysisFilter.setLoggerHandler( yourHandler )

    # Change output filename [optional]
    tetQualityAnalysisFilter.setFilename( filename )

    # Do calculations
    try:
        tetQualityAnalysisFilter.applyFilter()
    except Exception as e:
        tetQualityAnalysisFilter.logger.error( f"The filter { tetQualityAnalysisFilter.logger.name } failed due to: { e }" )
"""

loggerTitle: str = "Tetrahedra Quality Analysis"


[docs] class TetQualityAnalysis: def __init__( self: Self, meshes: dict[ str, vtkDataSet ], speHandler: bool = False ) -> None: """Tetrahedra Quality Analysis. Args: meshes (dict[str,vtkUnstructuredGrid]): Meshes to analyze. speHandler (bool, optional): True to use a specific handler, False to use the internal handler. Defaults to False. """ self.meshes: dict[ str, vtkDataSet ] = meshes self.analyzedMesh: dict[ int, dict[ str, Any ] ] = {} self.issues: dict[ int, Any ] = {} self.qualityScore: dict[ int, Any ] = {} self.validMetrics: dict[ int, dict[ str, Any ] ] = {} self.medians: dict[ int, dict[ str, Any ] ] = {} self.sample: dict[ int, npt.NDArray[ Any ] ] = {} self.tets: dict[ int, int ] = {} self.filename = 'mesh_comparison.png' # Logger self.logger: Logger if not speHandler: self.logger = getLogger( loggerTitle, True ) else: self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) self.logger.propagate = False counter: CountVerbosityHandler = CountVerbosityHandler() self.counter: CountVerbosityHandler self.nbWarnings: int = 0 try: self.counter = getLoggerHandlerType( type( counter ), self.logger ) self.counter.resetWarningCount() except ValueError: self.counter = counter self.counter.setLevel( logging.INFO ) self.logger.addHandler( self.counter )
[docs] def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: """Set a specific handler for the filter logger. In this filter 4 log levels are use, .info, .error, .warning and .critical, be sure to have at least the same 4 levels. Args: handler (logging.Handler): The handler to add. """ if not isHandlerInLogger( handler, self.logger ): self.logger.addHandler( handler ) else: self.logger.warning( "The logger already has this handler, it has not been added." )
[docs] def applyFilter( self: Self ) -> None: """Apply Tetrahedra Analysis.""" self.logger.info( f"Apply filter { self.logger.name }." ) self.__loggerSection( "MESH COMPARISON DASHBOARD" ) for n, ( nfilename, mesh ) in enumerate( self.meshes.items(), 1 ): coords = getCoordinatesDoublePrecision( mesh ) tetrahedraIds, tetrahedraConnectivity = extractTetConnectivity( mesh ) ntets = len( tetrahedraIds ) self.logger.info( f" Mesh {n} info: \n" + f" Name: {nfilename}\n" + f" Total cells: {mesh.GetNumberOfCells()}\n" + f" Tetrahedra: {ntets}\n" + f" Points: {mesh.GetNumberOfPoints()}" + "\n" + "-" * 80 + "\n" ) # Analyze both meshes self.analyzedMesh[ n ] = analyzeAllTets( coords, tetrahedraConnectivity ) metrics = self.analyzedMesh[ n ] self.tets[ n ] = ntets # Extract data with consistent filtering validAspectRatio = np.isfinite( metrics[ 'aspectRatio' ] ) validRadiusRatio = np.isfinite( metrics[ 'radiusRatio' ] ) # Combined valid mask validMask = validAspectRatio & validRadiusRatio aspectRatio = metrics[ 'aspectRatio' ][ validMask ] radiusRatio = metrics[ 'radiusRatio' ][ validMask ] flatnessRatio = metrics[ 'flatnessRatio' ][ validMask ] volume = metrics[ 'volumes' ][ validMask ] shapeQuality = metrics[ 'shapeQuality' ][ validMask ] # Edge length data minEdge = metrics[ 'minEdge' ][ validMask ] maxEdge = metrics[ 'maxEdge' ][ validMask ] # Edge length ratio edgeRatio = maxEdge / np.maximum( minEdge, 1e-15 ) # Dihedral angles minDihedral = metrics[ 'minDihedral' ][ validMask ] maxDihedral = metrics[ 'maxDihedral' ][ validMask ] dihedralRange = metrics[ 'dihedralRange' ][ validMask ] qualityScore = computeQualityScore( aspectRatio, shapeQuality, edgeRatio, minDihedral ) # Store all values self.validMetrics[ n ] = { 'aspectRatio': aspectRatio, 'radiusRatio': radiusRatio, 'flatnessRatio': flatnessRatio, 'volume': volume, 'shapeQuality': shapeQuality, 'minEdge': minEdge, 'maxEdge': maxEdge, 'edgeRatio': edgeRatio, 'minDihedral': minDihedral, 'maxDihedral': maxDihedral, 'dihedralRange': dihedralRange, 'qualityScore': qualityScore } # # ==================== Print Distribution Statistics ==================== # Problem element counts highAspectRatio = np.sum( aspectRatio > 100 ) lowShapeQuality = np.sum( shapeQuality < 0.3 ) lowFlatness = np.sum( flatnessRatio < 0.01 ) highEdgeRatio = np.sum( edgeRatio > 10 ) criticalMinDihedral = np.sum( minDihedral < 5 ) criticalMaxDihedral = np.sum( maxDihedral > 175 ) combined = np.sum( ( aspectRatio > 100 ) & ( shapeQuality < 0.3 ) ) criticalCombo = np.sum( ( aspectRatio > 100 ) & ( shapeQuality < 0.3 ) & ( minDihedral < 5 ) ) self.issues[ n ] = { "highAspectRatio": highAspectRatio, "lowShapeQuality": lowShapeQuality, "lowFlatness": lowFlatness, "highEdgeRatio": highEdgeRatio, "criticalMinDihedral": criticalMinDihedral, "criticalMaxDihedral": criticalMaxDihedral, "combined": combined, "criticalCombo": criticalCombo } # Overall quality scores excellent = np.sum( qualityScore > 80 ) / len( qualityScore ) * 100 good = np.sum( ( qualityScore > 60 ) & ( qualityScore <= 80 ) ) / len( qualityScore ) * 100 fair = np.sum( ( qualityScore > 30 ) & ( qualityScore <= 60 ) ) / len( qualityScore ) * 100 poor = np.sum( qualityScore <= 30 ) / len( qualityScore ) * 100 self.qualityScore[ n ] = { 'excellent': excellent, 'good': good, 'fair': fair, 'poor': poor } extremeAspectRatio = aspectRatio > 1e4 self.issues[ n ][ "extremeAspectRatio" ] = extremeAspectRatio # Nearly degenerate elements degenerateElements = ( minDihedral < 0.1 ) | ( maxDihedral > 179.9 ) self.issues[ n ][ "degenerate" ] = degenerateElements self.medians[ n ] = { "aspectRatio": np.median( self.validMetrics[ n ][ "aspectRatio" ] ), "shapeQuality": np.median( self.validMetrics[ n ][ "shapeQuality" ] ), "volume": np.median( self.validMetrics[ n ][ "volume" ] ), "minEdge": np.median( self.validMetrics[ n ][ "minEdge" ] ), "maxEdge": np.median( self.validMetrics[ n ][ "maxEdge" ] ), "edgeRatio": np.median( self.validMetrics[ n ][ "edgeRatio" ] ), "minDihedral": np.median( self.validMetrics[ n ][ "minDihedral" ] ), "maxDihedral": np.median( self.validMetrics[ n ][ "maxDihedral" ] ), "qualityScore": np.median( self.validMetrics[ n ][ "qualityScore" ] ), } # ==================== Report ==================== self.printDistributionStatistics() self.__orderMeshes() self.printPercentileAnalysis() self.printQualityIssueSummary() self.printExtremeOutlierAnalysis() self.printSummary() self.computeDeltasFromBest() self.createComparisonDashboard() result: str = f"The filter { self.logger.name } succeeded" if self.counter.warningCount > 0: self.logger.warning( f"{ result } but { self.counter.warningCount } warnings have been logged." ) else: self.logger.info( f"{ result }." ) # Keep number of warnings logged during the filter application and reset the warnings count in case the filter is applied again. self.nbWarnings = self.counter.warningCount self.counter.resetWarningCount() return
[docs] def printDistributionStatistics( self: Self ) -> None: """Print the distribution statistics for various metrics.""" self.__loggerSection( "DISTRIBUTION STATISTICS (MIN / MEDIAN / MAX)" ) def printMetricStats( metricName: str, name: str, fmt: str = '.2e' ) -> None: """Helper function to print min/median/max for a metric. Args: metricName (str): The metric name to display. name (str): Metric name in dict of values. fmt (str): Display format. """ msg = f"{metricName}:\n" for n, _ in enumerate( self.meshes.items(), 1 ): data = self.validMetrics[ n ][ name ] msg += f" Mesh{n:2}: Min={data.min():<10{fmt}}Median={np.median(data):<10{fmt}}Max={data.max():<10{fmt}}\n" self.logger.info( msg ) printMetricStats( "Aspect Ratio", 'aspectRatio' ) printMetricStats( "Radius Ratio", 'radiusRatio' ) printMetricStats( "Flatness Ratio", 'flatnessRatio' ) printMetricStats( "Shape Quality", 'shapeQuality', fmt='.4f' ) printMetricStats( "Volume", 'volume' ) printMetricStats( "Min Edge Length", 'minEdge' ) printMetricStats( "Max Edge Length", 'maxEdge' ) printMetricStats( "Edge Length Ratio", 'edgeRatio', fmt='.2f' ) printMetricStats( "Min Dihedral Angle (degrees)", 'minDihedral', fmt='.2f' ) printMetricStats( "Max Dihedral Angle (degrees)", 'maxDihedral', fmt='.2f' ) printMetricStats( "Dihedral Range (degrees)", 'dihedralRange', fmt='.2f' ) printMetricStats( "Overall Quality Score (0-100)", 'qualityScore', fmt='.2f' )
[docs] def printPercentileAnalysis( self: Self, fmt: str = '.2f' ) -> None: """Print percentile analysis. Args: fmt (str): Display formatting. """ self.__loggerSection( "PERCENTILE ANALYSIS (25th / 75th / 90th / 99th)" ) for metricName, name in zip( *[ ( "Aspect Ratio", "Shape Quality", "Edge Length Ratio", "Min Dihedral Angle (degrees)", "Overall Quality Score" ), ( 'aspectRatio', 'shapeQuality', 'edgeRatio', 'minDihedral', 'qualityScore' ) ] ): msg = f"{metricName}:\n" for n, _ in enumerate( self.meshes.items(), 1 ): data = self.validMetrics[ n ][ name ] p1 = np.percentile( data, [ 25, 75, 90, 99 ] ) msg += f" Mesh {n}: 25th = {p1[0]:<7,{fmt}}75th = {p1[1]:<7,{fmt}}90th = {p1[2]:<7,{fmt}}99th = {p1[3]:<7,{fmt}}\n" self.logger.info( msg )
[docs] def printQualityIssueSummary( self: Self ) -> None: """Print the quality issues.""" self.__loggerSection( "QUALITY ISSUE SUMMARY" ) fmt = '.2f' for n, _ in enumerate( self.meshes.items(), 1 ): msg = f"Mesh {n} Issues:\n" w = False for issueType, name, reference in zip( *[ ( "Aspect Ratio > 100", "Shape Quality < 0.3", "Flatness < 0.01", "Edge Ratio > 10", "Min Dihedral < 5°", "Max Dihedral > 175°", "Combined (AR>100 & Q<0.3)", "CRITICAL (AR>100 & Q<0.3 & MinDih<5°" ), ( "highAspectRatio", "lowShapeQuality", "lowFlatness", "highEdgeRatio", "criticalMinDihedral", "criticalMaxDihedral", "combined", "criticalCombo" ), ( 'aspectRatio', 'shapeQuality', "flatnessRatio", "edgeRatio", "minDihedral", 'maxDihedral', 'aspectRatio', 'aspectRatio' ) ] ): pb = self.issues[ n ][ name ] m = len( self.validMetrics[ n ][ reference ] ) pb / m * 100 msg += f" {f'{issueType}:':37}{pb:>8,} ({(pb/m*100):{fmt}}%)\n" if pb != 0: w = True self.logger.warning( msg ) if w else self.logger.info( msg ) self.compareIssuesFromBest() self.printOverallQualityScore()
[docs] def printOverallQualityScore( self: Self ) -> None: """Print the quality score distribution from excellent to poor.""" msg = "Overall Quality Score Distribution:\n" msg += f" {'Mesh n':10}{'Excellent (>80)':20}{'Good (60-80)':17}{'Fair (30-60)':15}{'Poor (≤30)':15}\n" for n, _ in enumerate( self.meshes.items(), 1 ): qualityScore = self.qualityScore[ n ] msg += f" {f'Mesh {n}':10}{qualityScore[ 'excellent' ]:10,.1f}%{qualityScore[ 'good' ]:15,.1f}% {qualityScore[ 'fair' ]:15,.1f}%{qualityScore[ 'poor' ]:15,.1f}%\n" self.logger.info( msg )
[docs] def printExtremeOutlierAnalysis( self: Self ) -> None: """Print the extreme outlier analysis results.""" self.__loggerSection( "EXTREME OUTLIER ANALYSIS" ) msg = "Elements with Aspect Ratio > 10,000:\n" msg2 = "" # Change log type to warning if problematic elements w = False w2 = False for n, _ in enumerate( self.meshes, 1 ): extremeAspectRatio = self.issues[ n ][ 'extremeAspectRatio' ] data = self.analyzedMesh[ n ] aspectRatio = data[ 'aspectRatio' ] if np.sum( extremeAspectRatio ) > 0: msg += f" Mesh {n}: {np.sum(extremeAspectRatio):,} elements ({np.sum(extremeAspectRatio)/len(aspectRatio)*100:.3f}%)" w = True volume = data[ "volume" ] minDihedral = data[ "minDihedral" ] shapeQuality = data[ "shapeQuality" ] msg += f" Worst AR: {aspectRatio[extremeAspectRatio].max():.2e}\n" msg += f" Avg volume: {volume[extremeAspectRatio].mean():.2e}\n" msg += f" Min dihedral: {minDihedral[extremeAspectRatio].min():.2f}° - {minDihedral[extremeAspectRatio].mean():.2f}° (avg)\n" msg += f" Shape quality: {shapeQuality[extremeAspectRatio].min():.4f} - {shapeQuality[extremeAspectRatio].mean():.4f} (avg)\n" if np.sum( extremeAspectRatio ) > 10: w2 = True msg2 += f" Recommendation: Investigate/remove {np.sum(extremeAspectRatio):,} extreme elements in Mesh {n}\n These are likely artifacts from mesh generation or geometry issues.\n" self.logger.warning( msg ) if w else self.logger.info( msg + " N/A\n" ) if w2: self.logger.warning( msg2 ) # Nearly degenerate elements degMsg = "Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):\n" ww = False for n, _ in enumerate( self.meshes, 1 ): degenerate = self.issues[ n ][ "degenerate" ] data = self.validMetrics[ n ][ "minDihedral" ] if np.sum( degenerate ) > 0: ww = True degMsg += f" Mesh {n}: {np.sum(degenerate):,} elements ({np.sum(degenerate)/len(data)*100:.3f}%)\n" self.logger.warning( degMsg ) if ww else self.logger.info( degMsg + " N/A\n" )
[docs] def printSummary( self: Self ) -> None: """Print the summary.""" self.__loggerSection( "COMPARISON SUMMARY" ) for n, _ in enumerate( self.meshes, 1 ): name = f"Mesh {n}" if n == self.best: name += " [BEST]" elif n == self.worst: name += " [LEAST GOOD]" msg = f"{name}\n" msg += f" Tetrahedra: {self.tets[n]:,}\n" msg += f" Median Aspect Ratio: {self.medians[n]['aspectRatio']:.2f}\n" msg += f" Median Shape Quality: {self.medians[n]['shapeQuality']:.4f}\n" msg += f" Median Volume: {self.medians[n]['volume']:.2e}\n" msg += f" Median Min Edge: {self.medians[n]['minEdge']:.2e}\n" msg += f" Median Max Edge: {self.medians[n]['maxEdge']:.2e}\n" msg += f" Median Edge Ratio: {self.medians[n]['edgeRatio']:.2f}\n" msg += f" Median Min Dihedral: {self.medians[n]['minDihedral']:.1f}°\n" msg += f" Median Max Dihedral: {self.medians[n]['maxDihedral']:.1f}°\n" msg += f" Median Quality Score: {self.medians[n]['qualityScore']:.1f}/100\n" self.logger.info( msg )
[docs] def computeDeltasFromBest( self: Self ) -> None: """Compute and print the.""" self.logger.info( f"Best mesh: Mesh {self.best}" ) self.deltas: dict[ int, Any ] = {} for n, _ in enumerate( self.meshes, 1 ): self.deltas[ n ] = {} self.deltas[ n ][ "tetrahedra" ] = ( ( self.tets[ n ] - self.tets[ self.best ] ) / self.tets[ self.best ] * 100 ) if self.tets[ self.best ] > 0 else 0 for metric in ( "aspectRatio", "shapeQuality", "volume", "minEdge", "maxEdge", "edgeRatio" ): value = self.medians[ n ][ metric ] valueBest = self.medians[ self.best ][ metric ] self.deltas[ n ][ metric ] = ( ( value - valueBest ) / valueBest * 100 ) if valueBest > 0 else 0 deltaTets = [ f"{self.deltas[ n ][ 'tetrahedra' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaAspectRatio = [ f"{self.deltas[ n ][ 'aspectRatio' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaShapeQuality = [ f"{self.deltas[ n ][ 'shapeQuality' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaVolume = [ f"{self.deltas[ n ][ 'volume' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaMinEdge = [ f"{self.deltas[ n ][ 'minEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaMaxEdge = [ f"{self.deltas[ n ][ 'maxEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] deltaEdgeRatio = [ f"{self.deltas[ n ][ 'edgeRatio' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] names = [ f"{f'Mesh {n}':>13}" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ] self.logger.info( f"Changes vs BEST [Mesh {self.best}]:\n" + f"{' Mesh:':<20}{('').join(names)}\n" + f"{' Tetrahedra:':<20}{('').join(deltaTets)}\n" + f"{' Aspect Ratio:':<20}{('').join(deltaAspectRatio)}\n" + f"{' Shape Quality:':<20}{('').join(deltaShapeQuality)}\n" + f"{' Volume:':<20}{('').join(deltaVolume)}\n" + f"{' Min Edge Length:':<20}{('').join(deltaMinEdge)}\n" + f"{' Max Edge Length:':<20}{('').join(deltaMaxEdge)}\n" + f"{' Edge Length Ratio:':<20}{('').join(deltaEdgeRatio)}\n" )
[docs] def createComparisonDashboard( self: Self ) -> None: """Create the comparison dashboard.""" lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes, 1 ) ] # Determine smart plot limits ar99 = [] for n, _ in enumerate( self.meshes, 1 ): ar99.append( np.percentile( self.validMetrics[ n ][ "aspectRatio" ], 99 ) ) ar99Max = np.max( np.array( ar99 ) ) if ar99Max < 10: arPlotLimit = 100 elif ar99Max < 100: arPlotLimit = 1000 else: arPlotLimit = 10000 # Set style plt.rcParams[ 'figure.facecolor' ] = 'white' plt.rcParams.update( { 'font.size': 9, 'axes.titlesize': 10, 'axes.labelsize': 9, 'xtick.labelsize': 8, 'ytick.labelsize': 8, 'legend.fontsize': 8 } ) # Create figure with flexible layout fig = plt.figure( figsize=( 25, 20 ) ) # Row 1: Executive Summary (3 columns - wider) gs_row1 = gridspec.GridSpec( 1, 3, figure=fig, left=0.05, right=0.95, top=0.94, bottom=0.84, wspace=0.20 ) # Rows 2-5: Main dashboard (5 columns each) gs_main = gridspec.GridSpec( 4, 5, figure=fig, left=0.05, right=0.95, top=0.80, bottom=0.05, hspace=0.35, wspace=0.30 ) # Title suptitle = 'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n' suptitle += ( ' - ' ).join( [ f'Mesh {n}: {self.tets[n]} tets ' for n, _ in enumerate( self.meshes, 1 ) ] ) fig.suptitle( suptitle, fontsize=16, fontweight='bold', y=0.99 ) # Color scheme color = plt.cm.tab10( np.arange( 20 ) ) # type: ignore[attr-defined] # ==================== ROW 1: EXECUTIVE SUMMARY ==================== # 1. Overall Quality Score Distribution ax1 = fig.add_subplot( gs_row1[ 0, 0 ] ) bins = np.linspace( 0, 100, 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): qualityScore = self.validMetrics[ n ][ 'qualityScore' ] ax1.hist( qualityScore, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax1.axvline( np.median( qualityScore ), color=color[ n - 1 ], linestyle='--', linewidth=2.5, alpha=0.9 ) # Add quality zones ax1.axvspan( 0, 30, alpha=0.15, color='red', zorder=0 ) ax1.axvspan( 30, 60, alpha=0.15, color='yellow', zorder=0 ) ax1.axvspan( 60, 80, alpha=0.15, color='lightgreen', zorder=0 ) ax1.axvspan( 80, 100, alpha=0.15, color='darkgreen', zorder=0 ) # Add summary text #### ONLY BEST AND WORST MESH? ax1.text( 0.98, 0.92, f'Median Score:\n{f"M{self.best}[+]:":<5}{np.median(self.validMetrics[self.best][ "qualityScore" ]):.1f}\n' + f'{f"M{self.worst}[-]:":<5}{np.median(self.validMetrics[self.worst][ "qualityScore" ]):.1f}\n\n' + f'Excellent (>80):\n{f"M{self.best}[+]:":<5}{self.qualityScore[self.best]["excellent"]:.1f}%\n' + f'{f"M{self.worst}[-]:":<5}{self.qualityScore[self.worst]["excellent"]:.1f}%', transform=ax1.transAxes, va='top', ha='right', bbox={ "boxstyle": 'round', "facecolor": 'wheat', "alpha": 0.9 }, fontsize=9, fontweight='bold' ) ax1.set_xlabel( 'Combined Quality Score', fontweight='bold' ) ax1.set_ylabel( 'Count', fontweight='bold' ) ax1.set_title( 'OVERALL MESH QUALITY VERDICT', fontsize=12, fontweight='bold', color='darkblue', pad=10 ) ax1.legend( loc='upper left', fontsize=9 ) ax1.grid( True, alpha=0.3 ) # Add zone labels ax1.text( 15, ax1.get_ylim()[ 1 ] * 0.95, 'Poor', ha='center', fontsize=8, color='darkred' ) ax1.text( 45, ax1.get_ylim()[ 1 ] * 0.95, 'Fair', ha='center', fontsize=8, color='orange' ) ax1.text( 70, ax1.get_ylim()[ 1 ] * 0.95, 'Good', ha='center', fontsize=8, color='green' ) ax1.text( 90, ax1.get_ylim()[ 1 ] * 0.95, 'Excellent', ha='center', fontsize=8, color='darkgreen' ) # 2. Shape Quality vs Aspect Ratio ax2 = fig.add_subplot( gs_row1[ 0, 1 ] ) # Create sample for plotting for n, _ in enumerate( self.meshes, 1 ): aspectRatio = self.validMetrics[ n ][ "aspectRatio" ] shapeQuality = self.validMetrics[ n ][ "shapeQuality" ] self.setSampleForPlot( aspectRatio, n ) idx = self.sample[ n ] mask1Plot = aspectRatio[ idx ] < arPlotLimit ax2.scatter( aspectRatio[ idx ][ mask1Plot ], shapeQuality[ idx ][ mask1Plot ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) # Add quality threshold lines ax2.axhline( y=0.3, color='red', linestyle='--', linewidth=2, alpha=0.8, label='Poor (Q < 0.3)', zorder=5 ) ax2.axhline( y=0.7, color='green', linestyle='--', linewidth=2, alpha=0.8, label='Good (Q > 0.7)', zorder=5 ) ax2.axvline( x=100, color='orange', linestyle='--', linewidth=2, alpha=0.8, label='High AR (> 100)', zorder=5 ) # Highlight problem zone problemZone = Rectangle( ( 100, 0 ), arPlotLimit - 100, 0.3, alpha=0.2, facecolor='red', edgecolor='none', zorder=0 ) ax2.add_patch( problemZone ) # Count ALL elements np.sum( ( aspectRatio > 100 ) & ( shapeQuality < 0.3 ) ) np.sum( aspectRatio > arPlotLimit ) # Problem annotation annotateIssues = ( '\n' ).join( [ f"{f'M{n}':4}{np.sum(self.issues[n]['combined']):,}" for n, _ in enumerate( self.meshes, 1 ) ] ) ax2.text( 0.98, 0.02, 'PROBLEM ELEMENTS\n(AR>100 & Q<0.3):\n\n' + annotateIssues, transform=ax2.transAxes, va='bottom', ha='right', bbox={ "boxstyle": 'round', "facecolor": '#ffcccc', "alpha": 0.95, "edgecolor": 'darkred', "linewidth": 2 }, fontsize=8, fontweight='bold' ) ax2.set_xscale( 'log' ) ax2.set_xlabel( 'Aspect Ratio', fontweight='bold' ) ax2.set_ylabel( 'Shape Quality', fontweight='bold' ) ax2.set_title( 'KEY QUALITY INDICATOR: Shape Quality vs Aspect Ratio', fontsize=12, fontweight='bold', color='darkred', pad=10 ) ax2.set_xlim( ( 1, arPlotLimit ) ) ax2.set_ylim( ( 0, 1.05 ) ) ax2.legend( loc='upper right', fontsize=7, framealpha=0.95 ) ax2.grid( True, alpha=0.3 ) # 3. Critical Issues Summary Table ax3 = fig.add_subplot( gs_row1[ 0, 2 ] ) ax3.axis( 'off' ) summaryStats = [] summaryStats.append( [ 'CRITICAL ISSUE', f'BEST [M{self.best}]', f'WORST [M{self.worst}]', 'CHANGE' ] ) summaryStats.append( [ '─' * 18, '─' * 10, '─' * 10, '─' * 10 ] ) criticalCombo = self.issues[ self.best ][ "criticalCombo" ] criticalCombo2 = self.issues[ self.worst ][ "criticalCombo" ] aspectRatio = self.validMetrics[ self.best ][ "aspectRatio" ] aspectRatio2 = self.validMetrics[ self.worst ][ "aspectRatio" ] highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ] highAspectRatio2 = self.issues[ self.worst ][ "highAspectRatio" ] lowShapeQuality = self.issues[ self.best ][ "lowShapeQuality" ] lowShapeQuality2 = self.issues[ self.worst ][ "lowShapeQuality" ] criticalMinDihedral = self.issues[ self.best ][ "criticalMinDihedral" ] criticalMinDihedral2 = self.issues[ self.worst ][ "criticalMinDihedral" ] criticalMaxDihedral = self.issues[ self.best ][ "criticalMaxDihedral" ] criticalMaxDihedral2 = self.issues[ self.worst ][ "criticalMaxDihedral" ] highEdgeRatio = self.issues[ self.best ][ "highEdgeRatio" ] highEdgeRatio2 = self.issues[ self.worst ][ "highEdgeRatio" ] summaryStats.append( [ 'CRITICAL Combo', f'{criticalCombo:,}', f'{criticalCombo2:,}', f'{((criticalCombo2-criticalCombo)/max(criticalCombo,1)*100):+.1f}%' if criticalCombo > 0 else 'N/A' ] ) summaryStats.append( [ '(AR>100 & Q<0.3', f'({criticalCombo/len(aspectRatio)*100:.2f}%)', f'({criticalCombo2/len(aspectRatio2)*100:.2f}%)', '' ] ) summaryStats.append( [ ' & MinDih<5°)', '', '', '' ] ) summaryStats.append( [ '', '', '', '' ] ) summaryStats.append( [ 'AR > 100', f'{highAspectRatio:,}', f'{highAspectRatio2:,}', f'{((highAspectRatio2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%' ] ) summaryStats.append( [ 'Quality < 0.3', f'{lowShapeQuality:,}', f'{lowShapeQuality2:,}', f'{((lowShapeQuality2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%' ] ) summaryStats.append( [ 'MinDih < 5°', f'{criticalMinDihedral:,}', f'{criticalMinDihedral2:,}', f'{((criticalMinDihedral2-criticalMinDihedral)/max(criticalMinDihedral,1)*100):+.1f}%' if criticalMinDihedral > 0 else 'N/A' ] ) summaryStats.append( [ 'MaxDih > 175°', f'{criticalMaxDihedral:,}', f'{criticalMaxDihedral2:,}', f'{((criticalMaxDihedral2-criticalMaxDihedral)/max(criticalMaxDihedral,1)*100):+.1f}%' if criticalMaxDihedral > 0 else 'N/A' ] ) summaryStats.append( [ 'Edge Ratio > 10', f'{highEdgeRatio:,}', f'{highEdgeRatio2:,}', f'{((highEdgeRatio2-highEdgeRatio)/max(highEdgeRatio,1)*100):+.1f}%' ] ) summaryStats.append( [ '─' * 18, '─' * 10, '─' * 10, '─' * 10 ] ) summaryStats.append( [ 'Quality Grade', '', '', '' ] ) excellent = self.qualityScore[ self.best ][ "excellent" ] excellent2 = self.qualityScore[ self.worst ][ "excellent" ] good = self.qualityScore[ self.best ][ "good" ] good2 = self.qualityScore[ self.worst ][ "good" ] poor = self.qualityScore[ self.best ][ "poor" ] poor2 = self.qualityScore[ self.worst ][ "poor" ] summaryStats.append( [ ' Excellent (>80)', f'{excellent:.1f}%', f'{excellent2:.1f}%', f'{excellent2-excellent:+.1f}%' ] ) summaryStats.append( [ ' Good (60-80)', f'{good:.1f}%', f'{good2:.1f}%', f'{good2-good:+.1f}%' ] ) summaryStats.append( [ ' Poor (≤30)', f'{poor:.1f}%', f'{poor2:.1f}%', f'{poor2-poor:+.1f}%' ] ) table = ax3.table( cellText=summaryStats, cellLoc='left', bbox=[ 0, 0, 1, 1 ], # type: ignore[arg-type] edges='open' ) table.auto_set_font_size( False ) table.set_fontsize( 8 ) # Style header for i in range( 4 ): table[ ( 0, i ) ].set_facecolor( '#34495e' ) table[ ( 0, i ) ].set_text_props( weight='bold', color='black', fontsize=9 ) # Highlight CRITICAL row for col in range( 4 ): table[ ( 2, col ) ].set_facecolor( '#fadbd8' ) table[ ( 2, col ) ].set_text_props( weight='bold', fontsize=9 ) # Color code changes for row in [ 2, 6, 7, 8, 9, 10, 13, 14, 15 ]: if row < len( summaryStats ): changeText = summaryStats[ row ][ 3 ] if '%' in changeText and changeText != 'N/A': val = float( changeText.replace( '%', '' ).replace( '+', '' ) ) if row in [ 2, 6, 7, 8, 9, 10, 15 ]: # Lower is better if val < -10: table[ ( row, 3 ) ].set_facecolor( '#d5f4e6' ) # Green elif val > 10: table[ ( row, 3 ) ].set_facecolor( '#fadbd8' ) # Red else: # Higher is better (excellent, good) if val > 10: table[ ( row, 3 ) ].set_facecolor( '#d5f4e6' ) elif val < -10: table[ ( row, 3 ) ].set_facecolor( '#fadbd8' ) ax3.set_title( 'CRITICAL ISSUES SUMMARY', fontsize=12, fontweight='bold', color='darkgreen', pad=10 ) # ==================== ROW 2: QUALITY DISTRIBUTIONS ==================== # 4. Shape Quality Histogram ax4 = fig.add_subplot( gs_main[ 0, 0 ] ) bins = np.linspace( 0, 1, 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): shapeQuality = self.validMetrics[ n ][ "shapeQuality" ] ax4.hist( shapeQuality, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax4.set_xlabel( 'Shape Quality', fontweight='bold' ) ax4.set_ylabel( 'Count', fontweight='bold' ) ax4.set_title( 'Shape Quality Distribution', fontweight='bold' ) ax4.legend() ax4.grid( True, alpha=0.3 ) # 5. Aspect Ratio Histogram ax5 = fig.add_subplot( gs_main[ 0, 1 ] ) arMax = np.array( [ self.validMetrics[ n ][ "aspectRatio" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ) bins = np.logspace( 0, np.log10( min( arPlotLimit, arMax.max() ) ), 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): aspectRatio = self.validMetrics[ n ][ 'aspectRatio' ] ax5.hist( aspectRatio[ aspectRatio < arPlotLimit ], bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax5.set_xscale( 'log' ) ax5.set_xlabel( 'Aspect Ratio', fontweight='bold' ) ax5.set_ylabel( 'Count', fontweight='bold' ) ax5.set_title( 'Aspect Ratio Distribution', fontweight='bold' ) ax5.legend() ax5.grid( True, alpha=0.3 ) # 6. Min Dihedral Histogram ax6 = fig.add_subplot( gs_main[ 0, 2 ] ) bins = np.linspace( 0, 90, 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): minDihedral = self.validMetrics[ n ][ "minDihedral" ] ax6.hist( minDihedral, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax6.axvline( 5, color='red', linestyle='--', linewidth=1.5, alpha=0.7 ) ax6.set_xlabel( 'Min Dihedral Angle (degrees)', fontweight='bold' ) ax6.set_ylabel( 'Count', fontweight='bold' ) ax6.set_title( 'Min Dihedral Angle Distribution', fontweight='bold' ) ax6.legend() ax6.grid( True, alpha=0.3 ) # 7. Edge Ratio Histogram ax7 = fig.add_subplot( gs_main[ 0, 3 ] ) bins = np.logspace( 0, 3, 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): edgeRatio = self.validMetrics[ n ][ "edgeRatio" ] ax7.hist( edgeRatio[ edgeRatio < 1000 ], bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax7.set_xscale( 'log' ) ax7.axvline( 1, color='green', linestyle='--', linewidth=1.5, alpha=0.7 ) ax7.set_xlabel( 'Edge Length Ratio', fontweight='bold' ) ax7.set_ylabel( 'Count', fontweight='bold' ) ax7.set_title( 'Edge Length Ratio Distribution', fontweight='bold' ) ax7.legend() ax7.grid( True, alpha=0.3 ) # 8. Volume Histogram ax8 = fig.add_subplot( gs_main[ 0, 4 ] ) volMin = np.array( [ self.validMetrics[ n ][ "volume" ].min() for n, _ in enumerate( self.meshes, 1 ) ] ).min() volMax = np.array( [ self.validMetrics[ n ][ "volume" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ).max() bins = np.logspace( np.log10( volMin ), np.log10( volMax ), 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): volume = self.validMetrics[ n ][ "volume" ] ax8.hist( volume, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax8.set_xscale( 'log' ) ax8.set_xlabel( 'Volume', fontweight='bold' ) ax8.set_ylabel( 'Count', fontweight='bold' ) ax8.set_title( 'Volume Distribution', fontweight='bold' ) ax8.legend() ax8.grid( True, alpha=0.3 ) # ==================== ROW 3: STATISTICAL COMPARISON (BOX PLOTS) ==================== # 9. Shape Quality Box Plot ax9 = fig.add_subplot( gs_main[ 1, 0 ] ) sq = [ self.validMetrics[ n ][ "shapeQuality" ] for n, _ in enumerate( self.meshes, 1 ) ] bp1 = ax9.boxplot( sq, labels=lbl, patch_artist=True, showfliers=False ) # type: ignore[call-arg] ax9.set_ylabel( 'Shape Quality', fontweight='bold' ) ax9.set_title( 'Shape Quality Comparison', fontweight='bold' ) ax9.grid( True, alpha=0.3, axis='y' ) # 10. Aspect Ratio Box Plot ax10 = fig.add_subplot( gs_main[ 1, 1 ] ) ar = [ self.validMetrics[ n ][ "aspectRatio" ] for n, _ in enumerate( self.meshes, 1 ) ] bp2 = ax10.boxplot( ar, labels=lbl, patch_artist=True, showfliers=False ) # type: ignore[call-arg] ax10.set_yscale( 'log' ) ax10.set_ylabel( 'Aspect Ratio (log)', fontweight='bold' ) ax10.set_title( 'Aspect Ratio Comparison', fontweight='bold' ) ax10.grid( True, alpha=0.3, axis='y' ) # 11. Min Dihedral Box Plot ax11 = fig.add_subplot( gs_main[ 1, 2 ] ) minDihedral = [ self.validMetrics[ n ][ "minDihedral" ] for n, _ in enumerate( self.meshes, 1 ) ] bp3 = ax11.boxplot( minDihedral, labels=lbl, patch_artist=True, showfliers=False ) # type: ignore[call-arg] ax11.set_ylabel( 'Min Dihedral Angle (degrees)', fontweight='bold' ) ax11.set_title( 'Min Dihedral Comparison', fontweight='bold' ) ax11.grid( True, alpha=0.3, axis='y' ) # 12. Edge Ratio Box Plot ax12 = fig.add_subplot( gs_main[ 1, 3 ] ) edgeRatio = [ self.validMetrics[ n ][ "edgeRatio" ] for n, _ in enumerate( self.meshes, 1 ) ] bp4 = ax12.boxplot( edgeRatio, labels=lbl, patch_artist=True, showfliers=False ) # type: ignore[call-arg] ax12.set_yscale( 'log' ) ax12.set_ylabel( 'Edge Length Ratio (log)', fontweight='bold' ) ax12.set_title( 'Edge Ratio Comparison', fontweight='bold' ) ax12.grid( True, alpha=0.3, axis='y' ) # 13. Volume Box Plot ax13 = fig.add_subplot( gs_main[ 1, 4 ] ) vol = [ self.validMetrics[ n ][ "volume" ] for n, _ in enumerate( self.meshes, 1 ) ] bp5 = ax13.boxplot( vol, labels=lbl, patch_artist=True, showfliers=False ) # type: ignore[call-arg] ax13.set_yscale( 'log' ) ax13.set_ylabel( 'Volume (log)', fontweight='bold' ) ax13.set_title( 'Volume Comparison', fontweight='bold' ) ax13.grid( True, alpha=0.3, axis='y' ) for n, _ in enumerate( self.meshes, 1 ): bp1[ 'boxes' ][ n - 1 ].set_facecolor( color[ n - 1 ] ) bp1[ 'medians' ][ n - 1 ].set_color( "black" ) bp2[ 'boxes' ][ n - 1 ].set_facecolor( color[ n - 1 ] ) bp2[ 'medians' ][ n - 1 ].set_color( "black" ) bp3[ 'boxes' ][ n - 1 ].set_facecolor( color[ n - 1 ] ) bp3[ 'medians' ][ n - 1 ].set_color( "black" ) bp4[ 'boxes' ][ n - 1 ].set_facecolor( color[ n - 1 ] ) bp4[ 'medians' ][ n - 1 ].set_color( "black" ) bp5[ 'boxes' ][ n - 1 ].set_facecolor( color[ n - 1 ] ) bp5[ 'medians' ][ n - 1 ].set_color( "black" ) # ==================== ROW 4: CORRELATION ANALYSIS (SCATTER PLOTS) ==================== # 14. Shape Quality vs Aspect Ratio (duplicate for detail) ax14 = fig.add_subplot( gs_main[ 2, 0 ] ) for n, _ in enumerate( self.meshes, 1 ): idx = self.sample[ n ] aspectRatio = self.validMetrics[ n ][ 'aspectRatio' ] shapeQuality = self.validMetrics[ n ][ 'shapeQuality' ] mask1 = aspectRatio[ idx ] < arPlotLimit ax14.scatter( aspectRatio[ idx ][ mask1 ], shapeQuality[ idx ][ mask1 ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) ax14.set_xscale( 'log' ) ax14.set_xlabel( 'Aspect Ratio', fontweight='bold' ) ax14.set_ylabel( 'Shape Quality', fontweight='bold' ) ax14.set_title( 'Shape Quality vs Aspect Ratio', fontweight='bold' ) ax14.set_xlim( ( 1, arPlotLimit ) ) ax14.set_ylim( ( 0, 1.05 ) ) ax14.legend( loc='upper right', fontsize=7 ) ax14.grid( True, alpha=0.3 ) # 15. Aspect Ratio vs Flatness ax15 = fig.add_subplot( gs_main[ 2, 1 ] ) for n, _ in enumerate( self.meshes, 1 ): idx = self.sample[ n ] aspectRatio = self.validMetrics[ n ][ "aspectRatio" ] flatnessRatio = self.validMetrics[ n ][ 'flatnessRatio' ] mask1 = aspectRatio[ idx ] < arPlotLimit ax15.scatter( aspectRatio[ idx ][ mask1 ], flatnessRatio[ idx ][ mask1 ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) ax15.set_xscale( 'log' ) ax15.set_yscale( 'log' ) ax15.set_xlabel( 'Aspect Ratio', fontweight='bold' ) ax15.set_ylabel( 'Flatness Ratio', fontweight='bold' ) ax15.set_title( 'Aspect Ratio vs Flatness', fontweight='bold' ) ax15.set_xlim( ( 1, arPlotLimit ) ) ax15.legend( loc='upper right', fontsize=7 ) ax15.grid( True, alpha=0.3 ) # 16. Volume vs Aspect Ratio ax16 = fig.add_subplot( gs_main[ 2, 2 ] ) for n, _ in enumerate( self.meshes, 1 ): idx = self.sample[ n ] aspectRatio = self.validMetrics[ n ][ "aspectRatio" ] volume = self.validMetrics[ n ][ 'volume' ] mask1 = aspectRatio[ idx ] < arPlotLimit ax16.scatter( volume[ idx ][ mask1 ], aspectRatio[ idx ][ mask1 ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) ax16.set_xscale( 'log' ) ax16.set_yscale( 'log' ) ax16.set_xlabel( 'Volume', fontweight='bold' ) ax16.set_ylabel( 'Aspect Ratio', fontweight='bold' ) ax16.set_title( 'Volume vs Aspect Ratio', fontweight='bold' ) ax16.set_ylim( ( 1, arPlotLimit ) ) ax16.legend( loc='upper right', fontsize=7 ) ax16.grid( True, alpha=0.3 ) # 17. Volume vs Shape Quality ax17 = fig.add_subplot( gs_main[ 2, 3 ] ) for n, _ in enumerate( self.meshes, 1 ): idx = self.sample[ n ] volume = self.validMetrics[ n ][ 'volume' ] shapeQuality = self.validMetrics[ n ][ 'shapeQuality' ] ax17.scatter( volume[ idx ], shapeQuality[ idx ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) ax17.set_xscale( 'log' ) ax17.set_xlabel( 'Volume', fontweight='bold' ) ax17.set_ylabel( 'Shape Quality', fontweight='bold' ) ax17.set_title( 'Volume vs Shape Quality', fontweight='bold' ) ax17.legend( loc='upper right', fontsize=7 ) ax17.grid( True, alpha=0.3 ) # 18. Edge Ratio vs Volume ax18 = fig.add_subplot( gs_main[ 2, 4 ] ) for n, _ in enumerate( self.meshes, 1 ): idx = self.sample[ n ] volume = self.validMetrics[ n ][ 'volume' ] edgeRatio = self.validMetrics[ n ][ 'edgeRatio' ] ax18.scatter( volume[ idx ], edgeRatio[ idx ], alpha=0.4, s=5, color=color[ n - 1 ], label=f'Mesh {n}', edgecolors='none' ) ax18.axhline( y=1, color='green', linestyle='--', linewidth=1.5, alpha=0.7 ) ax18.set_xscale( 'log' ) ax18.set_yscale( 'log' ) ax18.set_xlabel( 'Volume', fontweight='bold' ) ax18.set_ylabel( 'Edge Length Ratio', fontweight='bold' ) ax18.set_title( 'Edge Ratio vs Volume', fontweight='bold' ) ax18.legend( loc='upper right', fontsize=7 ) ax18.grid( True, alpha=0.3 ) # ==================== ROW 5: DETAILED DIAGNOSTICS ==================== # 19. Min Edge Length Histogram ax19 = fig.add_subplot( gs_main[ 3, 0 ] ) edgeMinMin = np.array( [ self.validMetrics[ n ][ "minEdge" ].min() for n, _ in enumerate( self.meshes, 1 ) ] ).min() edgeMaxMin = np.array( [ self.validMetrics[ n ][ "minEdge" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ).min() bins = np.logspace( np.log10( edgeMinMin ), np.log10( edgeMaxMin ), 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): minEdge = self.validMetrics[ n ][ 'minEdge' ] ax19.hist( minEdge, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax19.axvline( np.median( minEdge ), color=color[ n - 1 ], linestyle=':', linewidth=2, alpha=0.8 ) ax19.set_xscale( 'log' ) ax19.set_xlabel( 'Minimum Edge Length', fontweight='bold' ) ax19.set_ylabel( 'Count', fontweight='bold' ) ax19.set_title( 'Min Edge Length Distribution', fontweight='bold' ) ax19.legend() ax19.grid( True, alpha=0.3 ) # 20. Max Edge Length Histogram ax20 = fig.add_subplot( gs_main[ 3, 1 ] ) edgeMaxMax = np.array( [ self.validMetrics[ n ][ "maxEdge" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ).max() edgeMinMax = np.array( [ self.validMetrics[ n ][ "maxEdge" ].min() for n, _ in enumerate( self.meshes, 1 ) ] ).min() bins = np.logspace( np.log10( edgeMinMax ), np.log10( edgeMaxMax ), 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): maxEdge = self.validMetrics[ n ][ "maxEdge" ] ax20.hist( maxEdge, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax20.axvline( np.median( maxEdge ), color=color[ n - 1 ], linestyle=':', linewidth=2, alpha=0.8 ) ax20.set_xscale( 'log' ) ax20.set_xlabel( 'Maximum Edge Length', fontweight='bold' ) ax20.set_ylabel( 'Count', fontweight='bold' ) ax20.set_title( 'Max Edge Length Distribution', fontweight='bold' ) ax20.legend() ax20.grid( True, alpha=0.3 ) # 21. Max Dihedral Histogram ax21 = fig.add_subplot( gs_main[ 3, 2 ] ) bins = np.linspace( 90, 180, 40 ).tolist() for n, _ in enumerate( self.meshes, 1 ): maxDihedral = self.validMetrics[ n ][ "maxDihedral" ] ax21.hist( maxDihedral, bins=bins, alpha=0.6, label=f'Mesh {n}', color=color[ n - 1 ], edgecolor='black', linewidth=0.5 ) ax21.axvline( 175, color='red', linestyle='--', linewidth=1.5, alpha=0.7 ) ax21.set_xlabel( 'Max Dihedral Angle (degrees)', fontweight='bold' ) ax21.set_ylabel( 'Count', fontweight='bold' ) ax21.set_title( 'Max Dihedral Angle Distribution', fontweight='bold' ) ax21.legend() ax21.grid( True, alpha=0.3 ) # 22. Dihedral Range Box Plot ax22 = fig.add_subplot( gs_main[ 3, 3 ] ) nmesh = len( self.meshes ) positions = np.delete( np.arange( 1, nmesh * 2 + 2 ), nmesh ) dih = [ self.validMetrics[ n ][ "minDihedral" ] for n, _ in enumerate( self.meshes, 1 ) ] + [ self.validMetrics[ n ][ "maxDihedral" ] for n, _ in enumerate( self.meshes, 1 ) ] lbl_boxplot = [ f'M{n}Min' for n, _ in enumerate( self.meshes, 1 ) ] + [ f'M{n}Max' for n, _ in enumerate( self.meshes, 1 ) ] boxplot_color = [ n for n, _ in enumerate( self.meshes, ) ] * 2 bp_dih = ax22.boxplot( dih, positions=positions, labels=lbl_boxplot, # type: ignore[call-arg] patch_artist=True, showfliers=False, widths=0.6 ) for m in range( len( self.meshes ) * 2 ): bp_dih[ 'boxes' ][ m ].set_facecolor( color[ boxplot_color[ m ] ] ) bp_dih[ 'medians' ][ m ].set_color( "black" ) ax22.axhline( 5, color='red', linestyle='--', linewidth=1, alpha=0.5, zorder=0 ) ax22.axhline( 175, color='red', linestyle='--', linewidth=1, alpha=0.5, zorder=0 ) ax22.axhline( 70.5, color='green', linestyle=':', linewidth=1, alpha=0.5, zorder=0 ) ax22.set_ylabel( 'Dihedral Angle (degrees)', fontweight='bold' ) ax22.set_title( 'Dihedral Angle Comparison', fontweight='bold' ) ax22.grid( True, alpha=0.3, axis='y' ) # 23. Shape Quality CDF ax23 = fig.add_subplot( gs_main[ 3, 4 ] ) for n, _ in enumerate( self.meshes, 1 ): shapeQuality = self.validMetrics[ n ][ "shapeQuality" ] sorted_sq1 = np.sort( shapeQuality ) cdf_sq1 = np.arange( 1, len( sorted_sq1 ) + 1 ) / len( sorted_sq1 ) * 100 ax23.plot( sorted_sq1, cdf_sq1, color=color[ n - 1 ], linewidth=2, label=f'Mesh {n}' ) ax23.axvline( 0.3, color='red', linestyle='--', linewidth=1, alpha=0.5 ) ax23.axvline( 0.7, color='green', linestyle='--', linewidth=1, alpha=0.5 ) ax23.axhline( 50, color='gray', linestyle='--', linewidth=1, alpha=0.5 ) ax23.set_xlabel( 'Shape Quality', fontweight='bold' ) ax23.set_ylabel( 'Cumulative %', fontweight='bold' ) ax23.set_title( 'Cumulative Distribution - Shape Quality', fontweight='bold' ) ax23.legend( loc='lower right' ) ax23.grid( True, alpha=0.3 ) # Save figure plt.savefig( self.filename, dpi=300, bbox_inches='tight', facecolor='white' ) self.logger.info( f"Dashboard saved successfully: {self.filename}" )
[docs] def setFilename( self: Self, filename: str ) -> None: """Set comparison dashboard output filename. Args: filename (str): Output filename. """ if filename != "None": self.filename = filename
def __loggerSection( self: Self, sectionName: str ) -> None: self.logger.info( "=" * 80 ) self.logger.info( sectionName ) self.logger.info( "=" * 80 ) def __orderMeshes( self: Self ) -> None: """Proposition of ordering as fonction of median quality score.""" self.__loggerSection( "ORDERING MESHES (from median quality score)" ) medianScore = { n: np.median( self.validMetrics[ n ][ "qualityScore" ] ) for n, _ in enumerate( self.meshes, 1 ) } sortedMeshes = sorted( medianScore.items(), key=lambda x: x[ 1 ], reverse=True ) self.sorted = sortedMeshes self.best = sortedMeshes[ 0 ][ 0 ] self.worst = sortedMeshes[ -1 ][ 0 ] self.logger.info( "Mesh order from median quality score:" ) top = [ f"Mesh {m[0]} ({m[1]:.2f})" for m in sortedMeshes ] toprint: str = ( " > " ).join( top ) self.logger.info( " [+] " + toprint + " [-]\n" )
[docs] def compareIssuesFromBest( self: Self ) -> None: """Compare issues values vs [BEST] mesh.""" highAspectRatioBest = self.issues[ self.best ][ "highAspectRatio" ] criticalMinDihedralBest = self.issues[ self.best ][ "criticalMinDihedral" ] lowShapeQualityBest = self.issues[ self.best ][ "lowShapeQuality" ] criticalComboBest = self.issues[ self.best ][ "criticalCombo" ] def getPercentChange( data: np.float64, ref: np.float64 ) -> np.float64: """Compute and return the percent change. Args: data (np.float64): Data to compare. ref (np.float64): Reference. Returns: np.float64: The percent change from reference. """ return ( data - ref ) / max( ref, 1 ) * 100 msg: str = f"Change from BEST [Mesh {self.best}]\n" msg += f" {'Mesh':20}" + ( "" ).join( [ f"{f'Mesh {n}':>16}" for n, _ in enumerate( self.meshes, 1 ) ] ) + "\n" highAspectRatio: list[ str ] = [ f"{getPercentChange( self.issues[ n ][ 'highAspectRatio' ], highAspectRatioBest ):>+15,.1f}%" if n != self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ] lowShapeQuality: list[ str ] = [ f"{getPercentChange( self.issues[ n ][ 'lowShapeQuality' ], lowShapeQualityBest ):>+15,.1f}%" if n != self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ] criticalMinDihedral: list[ str ] = [ f"{getPercentChange( self.issues[ n ][ 'criticalMinDihedral' ], criticalMinDihedralBest ):>+15,.1f}%" if criticalMinDihedralBest > 0 and n != self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ] criticalCombo: list[ str ] = [ f"{getPercentChange( self.issues[ n ][ 'criticalCombo' ], criticalComboBest ):>+15,.1f}%" if criticalComboBest > 0 and n != self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ] msg += f"{' AR > 100:':20}{('').join( highAspectRatio )}\n" msg += f"{' Quality < 0.3:':20}{('').join( lowShapeQuality )}\n" msg += f"{' MinDih < 5°:':20}{('').join( criticalMinDihedral )}\n" msg += f"{' CRITICAL combo:':20}{('').join( criticalCombo )}\n" self.logger.info( msg )
[docs] def setSampleForPlot( self: Self, data: npt.NDArray[ Any ], n: int ) -> None: """Set sampling for a given metric of mesh n. Args: data (npt.NDArray[Any]): Metric array to sample. n (int): Mesh id. """ sampleSize = min( 10000, len( data ) ) self.sample[ n ] = np.random.choice( len( data ), sampleSize, replace=False )