From 70886d9c9dd0295851b9859402fada4a3a1aeff4 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Wed, 28 Jan 2026 09:06:31 +0100
Subject: [PATCH 01/11] make filter class from existing script
---
.../pre_processing/TetQualityAnalysis.py | 1209 +++++++++++++++++
1 file changed, 1209 insertions(+)
create mode 100644 geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
new file mode 100644
index 00000000..05fbbbbe
--- /dev/null
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -0,0 +1,1209 @@
+# 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
+from geos.utils.Logger import ( Logger, getLogger )
+import vtk
+import numpy as np
+import sys
+import matplotlib
+
+from vtkmodules.vtkCommonDataModel import vtkDataSet
+from vtkmodules.util.numpy_support import vtk_to_numpy
+import matplotlib.pyplot as plt
+import matplotlib.gridspec as gridspec
+from matplotlib.patches import Rectangle
+import seaborn as sns
+
+__doc__ = """
+TetQualityAnalysis module is a filter that performs an an analysis of tetrahedras of one or several meshes and plot a summary.
+"""
+
+loggerName: str = "Tetrahedra Quality Analysis"
+
+class TetQualityAnalysis:
+
+ def __init__(
+ self: Self,
+ meshes: dict[str, vtkDataSet],
+ speHandler: bool = False
+ ) -> None:
+
+ self.meshes: dict[ str, vtkDataSet ] = meshes
+ # self.meshes = Set( )
+ self.analyzedMesh: dict[ Any ] = {}
+ self.issues: dict[ Any ] = {}
+ self.overallQualityScore: dict[ Any] = {}
+ self.validMetrics: dict[Any] = {}
+ self.sample = {}
+
+ # Logger.
+ self.logger: Logger
+ if not speHandler:
+ self.logger = getLogger( loggerName, True )
+ else:
+ self.logger = logging.getLogger( loggerName )
+ self.logger.setLevel( logging.INFO )
+ self.logger.propagate = False
+
+
+ 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.
+ """
+ self.handler = handler
+ if len( self.logger.handlers ) == 0:
+ self.logger.addHandler( handler )
+ else:
+ self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler'"
+ " to True during the filter initialization." )
+
+
+ def applyFilter( self: Self) -> None:
+ self.logger.info( f"Apply filter { self.logger.name }." )
+
+ self.__loggerSection( "MESH COMPARISON DASHBOARD" )
+
+ for n, (filename1, mesh1) in enumerate( self.meshes.items() ):
+ # print( n, filename1 )
+ coords1 = self.get_coordinates_double_precision(mesh1)
+ tet_ids1, tet_conn1 = self.extract_tet_connectivity(mesh1)
+ n_tets1 = len( tet_ids1 )
+
+ self.logger.info( f" Mesh {n} info: " )
+ self.logger.info( f" Name: {filename1}" )
+ self.logger.info( f" Total cells: {mesh1.GetNumberOfCells()}" )
+ self.logger.info( f" Tetrahedra: {n_tets1}" )
+ self.logger.info( f" Points: {mesh1.GetNumberOfPoints()}" )
+
+ # Analyze both meshes
+ print(f"\nAnalyzing Mesh {n} (double precision)...")
+ # metrics1 = self.analyze_all_tets_vectorized(n, coords1, tet_conn1)
+ self.analyze_all_tets_vectorized(n, coords1, tet_conn1)
+ metrics1 = self.analyzedMesh[ n ]
+ self.analyzedMesh[n]["tet"] = n_tets1
+
+ # Extract data with consistent filtering
+ ar_valid1 = np.isfinite(metrics1['aspect_ratio'])
+ rr_valid1 = np.isfinite(metrics1['radius_ratio'])
+
+ # Combined valid mask
+ valid1 = ar_valid1 & rr_valid1
+
+ ar1 = metrics1['aspect_ratio'][valid1]
+ rr1 = metrics1['radius_ratio'][valid1]
+ fr1 = metrics1['flatness_ratio'][valid1]
+ vol1 = metrics1['volumes'][valid1]
+ sq1 = metrics1['shape_quality'][valid1]
+
+ # Edge length data
+ min_edge1 = metrics1['min_edge'][valid1]
+ max_edge1 = metrics1['max_edge'][valid1]
+
+ # Edge length ratio
+ edge_ratio1 = max_edge1 / np.maximum(min_edge1, 1e-15)
+
+ # Dihedral angles
+ min_dih1 = metrics1['min_dihedral'][valid1]
+ max_dih1 = metrics1['max_dihedral'][valid1]
+ dih_range1 = metrics1['dihedral_range'][valid1]
+
+ score1 = compute_quality_score(ar1, sq1, edge_ratio1, min_dih1)
+
+ # Store all values
+ self.validMetrics[n] = { 'aspect_ratio' : ar1, 'radius_ratio': rr1, 'flatness_ratio': fr1, 'volume': vol1, 'shape_quality': sq1, 'min_edge': min_edge1, 'max_edge': max_edge1, 'edge_ratio': edge_ratio1, 'min_dihedral': min_dih1, 'max_dihedral': max_dih1, 'dihedral_range': dih_range1, 'quality_score': score1}
+
+ # # ==================== Print Distribution Statistics ====================
+
+ # Problem element counts
+
+ high_ar1 = np.sum(ar1 > 100)
+ low_sq1 = np.sum(sq1 < 0.3)
+ low_flat1 = np.sum(fr1 < 0.01)
+ high_edge_ratio1 = np.sum(edge_ratio1 > 10)
+ critical_dih1 = np.sum(min_dih1 < 5)
+ critical_max_dih1 = np.sum(max_dih1 > 175)
+ problem1 = np.sum((ar1 > 100) & (sq1 < 0.3))
+ critical_combo1 = np.sum((ar1 > 100) & (sq1 < 0.3) & (min_dih1 < 5))
+
+ self.issues[n] = { "high_aspect_ratio": high_ar1, "low_shape_quality": low_sq1, "low_flatness": low_flat1, "high_edge_ratio": high_edge_ratio1, "critical_dihedral": critical_dih1, "critical_max_dihedral": critical_max_dih1, "combined": problem1, "critical_combo": critical_combo1 }
+
+ # Overall quality scores
+
+ excellent1 = np.sum(score1 > 80) / len(score1) * 100
+ good1 = np.sum((score1 > 60) & (score1 <= 80)) / len(score1) * 100
+ fair1 = np.sum((score1 > 30) & (score1 <= 60)) / len(score1) * 100
+ poor1 = np.sum(score1 <= 30) / len(score1) * 100
+
+ self.overallQualityScore[n] = { 'excellent': excellent1, 'good': good1, 'fair': fair1, 'poor': poor1 }
+
+ extreme_ar1 = ar1 > 1e4
+ self.issues[ n ][ "extreme_aspect_ratio" ] = extreme_ar1
+
+ # Nearly degenerate elements
+ degenerate1 = (min_dih1 < 0.1) | (max_dih1 > 179.9)
+ self.issues[ n ][ "degenerate" ] = degenerate1
+
+ print("\n" + "="*80 + "\n")
+
+ # ==================== Create Dashboard ====================
+
+ # ==================== Print Distribution Statistics ====================
+ self.__orderMeshes()
+
+ self.printDistributionStatistics()
+
+ # Percentile information
+ self.printPercentileAnalysis()
+
+ self.printQualityIssueSummary()
+
+ self.printExtremeOutlierAnalysis()
+
+ self.printComparisonSummary()
+ self.computeDeltasFromBest()
+
+ self.createComparisonDashboard()
+
+
+ def get_coordinates_double_precision( self, mesh ):
+ points = mesh.GetPoints()
+ n_points = points.GetNumberOfPoints()
+
+ coords = np.zeros((n_points, 3), dtype=np.float64)
+ for i in range(n_points):
+ point = points.GetPoint(i)
+ coords[i] = [point[0], point[1], point[2]]
+
+ return coords
+
+ def extract_tet_connectivity( self, mesh ):
+ """Extract connectivity for all tetrahedra."""
+ n_cells = mesh.GetNumberOfCells()
+ tet_ids = []
+ tet_connectivity = []
+
+ for cell_id in range(n_cells):
+ if mesh.GetCellType(cell_id) == vtk.VTK_TETRA:
+ cell = mesh.GetCell(cell_id)
+ point_ids = cell.GetPointIds()
+ conn = [point_ids.GetId(i) for i in range(4)]
+ tet_ids.append(cell_id)
+ tet_connectivity.append(conn)
+
+ return np.array(tet_ids), np.array(tet_connectivity)
+
+
+
+
+
+ def analyze_all_tets_vectorized(self, n, coords, connectivity) -> dict:
+ """Vectorized analysis of all tetrahedra."""
+ n_tets = connectivity.shape[0]
+
+ # Get coordinates for all vertices
+ v0 = coords[connectivity[:, 0]]
+ v1 = coords[connectivity[:, 1]]
+ v2 = coords[connectivity[:, 2]]
+ v3 = coords[connectivity[:, 3]]
+
+ # Compute edges
+ e01 = v1 - v0
+ e02 = v2 - v0
+ e03 = v3 - v0
+ e12 = v2 - v1
+ e13 = v3 - v1
+ e23 = v3 - v2
+
+ # Edge lengths
+ l01 = np.linalg.norm(e01, axis=1)
+ l02 = np.linalg.norm(e02, axis=1)
+ l03 = np.linalg.norm(e03, axis=1)
+ l12 = np.linalg.norm(e12, axis=1)
+ l13 = np.linalg.norm(e13, axis=1)
+ l23 = np.linalg.norm(e23, axis=1)
+
+ all_edges = np.stack([l01, l02, l03, l12, l13, l23], axis=1)
+ min_edge = np.min(all_edges, axis=1)
+ max_edge = np.max(all_edges, axis=1)
+
+ # Volumes
+ cross_product = np.cross(e01, e02)
+ volumes = np.abs(np.sum(cross_product * e03, axis=1) / 6.0)
+
+ # Face areas
+ face_012 = 0.5 * np.linalg.norm(np.cross(e01, e02), axis=1)
+ face_013 = 0.5 * np.linalg.norm(np.cross(e01, e03), axis=1)
+ face_023 = 0.5 * np.linalg.norm(np.cross(e02, e03), axis=1)
+ face_123 = 0.5 * np.linalg.norm(np.cross(e12, e13), axis=1)
+
+ all_faces = np.stack([face_012, face_013, face_023, face_123], axis=1)
+ max_face_area = np.max(all_faces, axis=1)
+ total_surface_area = np.sum(all_faces, axis=1)
+
+ # Aspect ratio
+ min_altitude = 3.0 * volumes / np.maximum(max_face_area, 1e-15)
+ aspect_ratio = max_edge / np.maximum(min_altitude, 1e-15)
+ aspect_ratio[volumes < 1e-15] = np.inf
+
+ # Radius ratio
+ inradius = 3.0 * volumes / np.maximum(total_surface_area, 1e-15)
+ circumradius = (max_edge ** 3) / np.maximum(24.0 * volumes, 1e-15)
+ radius_ratio = circumradius / np.maximum(inradius, 1e-15)
+
+ # Flatness ratio
+ flatness_ratio = volumes / np.maximum(max_face_area * min_edge, 1e-15)
+
+ # Shape quality
+ sum_edge_sq = np.sum(all_edges ** 2, axis=1)
+ shape_quality = 12.0 * (3.0 * volumes) ** (2.0/3.0) / np.maximum(sum_edge_sq, 1e-15)
+ shape_quality[volumes < 1e-15] = 0.0
+
+ # Dihedral angles
+ def compute_dihedral_angle(normal1, normal2):
+ """Compute dihedral angle between two face normals."""
+ n1_norm = normal1 / np.maximum(np.linalg.norm(normal1, axis=1, keepdims=True), 1e-15)
+ n2_norm = normal2 / np.maximum(np.linalg.norm(normal2, axis=1, keepdims=True), 1e-15)
+ cos_angle = np.sum(n1_norm * n2_norm, axis=1)
+ cos_angle = np.clip(cos_angle, -1.0, 1.0)
+ angle = np.arccos(cos_angle) * 180.0 / np.pi
+ return angle
+
+ # Face normals
+ normal_012 = np.cross(e01, e02)
+ normal_013 = np.cross(e01, e03)
+ normal_023 = np.cross(e02, e03)
+ normal_123 = np.cross(e12, e13)
+
+ # Dihedral angles for each edge
+ dihedral_01 = compute_dihedral_angle(normal_012, normal_013)
+ dihedral_02 = compute_dihedral_angle(normal_012, normal_023)
+ dihedral_03 = compute_dihedral_angle(normal_013, normal_023)
+ dihedral_12 = compute_dihedral_angle(normal_012, normal_123)
+ dihedral_13 = compute_dihedral_angle(normal_013, normal_123)
+ dihedral_23 = compute_dihedral_angle(normal_023, normal_123)
+
+ all_dihedrals = np.stack([dihedral_01, dihedral_02, dihedral_03,
+ dihedral_12, dihedral_13, dihedral_23], axis=1)
+
+ min_dihedral = np.min(all_dihedrals, axis=1)
+ max_dihedral = np.max(all_dihedrals, axis=1)
+ dihedral_range = max_dihedral - min_dihedral
+
+ self.analyzedMesh[ n ] = {
+ 'volumes': volumes,
+ 'aspect_ratio': aspect_ratio,
+ 'radius_ratio': radius_ratio,
+ 'flatness_ratio': flatness_ratio,
+ 'shape_quality': shape_quality,
+ 'min_edge': min_edge,
+ 'max_edge': max_edge,
+ 'min_dihedral': min_dihedral,
+ 'max_dihedral': max_dihedral,
+ 'dihedral_range': dihedral_range
+ }
+
+ return {
+ 'volumes': volumes,
+ 'aspect_ratio': aspect_ratio,
+ 'radius_ratio': radius_ratio,
+ 'flatness_ratio': flatness_ratio,
+ 'shape_quality': shape_quality,
+ 'min_edge': min_edge,
+ 'max_edge': max_edge,
+ 'min_dihedral': min_dihedral,
+ 'max_dihedral': max_dihedral,
+ 'dihedral_range': dihedral_range
+ }
+
+ def printDistributionStatistics( self: Self, fmt='.2e'):
+ self.__loggerSection( "DISTRIBUTION STATISTICS (MIN / MEDIAN / MAX)" )
+
+ def print_metric_stats( metricName, dataName, fmt='.2e'):
+ """Helper function to print min/median/max for a metric."""
+ # self.logger.info(f"\n{metricName}:")
+ self.logger.info(f"{metricName}:")
+ for n, _ in enumerate( self.meshes.items() ):
+ data = self.validMetrics[ n ][ dataName ]
+ self.logger.info(f" Mesh {n}: Min={data.min():{fmt}} Median={np.median(data):{fmt}} Max={data.max():{fmt}}")
+
+
+ print_metric_stats( "Aspect Ratio", 'aspect_ratio' )
+ print_metric_stats( "Radius Ratio", 'radius_ratio' )
+ print_metric_stats( "Flatness Ratio", 'flatness_ratio' )
+ print_metric_stats( "Shape Quality", 'shape_quality', fmt='.4f' )
+ print_metric_stats( "Volume", 'volume' )
+ print_metric_stats( "Min Edge Length", 'min_edge' )
+ print_metric_stats( "Max Edge Length", 'max_edge' )
+ print_metric_stats( "Edge Length Ratio", 'edge_ratio', fmt='.2f' )
+ print_metric_stats( "Min Dihedral Angle (degrees)", 'min_dihedral', fmt='.2f' )
+ print_metric_stats( "Max Dihedral Angle (degrees)", 'max_dihedral', fmt='.2f' )
+ print_metric_stats( "Dihedral Range (degrees)", 'dihedral_range',fmt='.2f' )
+ print_metric_stats( "Overall Quality Score (0-100)", 'quality_score', fmt='.2f' )
+
+
+ def printPercentileAnalysis( self: Self, fmt='.2f' ):
+ self.__loggerSection( "PERCENTILE ANALYSIS (25th / 75th / 90th / 99th)" )
+
+ def print_percentiles(metricName, dataName, fmt='.2f'):
+ """Helper function to print percentiles."""
+ self.logger.info(f"{metricName}:")
+ # self.logger.info(f"\n{metricName}:")
+ for n, _ in enumerate( self.meshes.items() ):
+ data = self.validMetrics[ n ][ dataName ]
+ p1 = np.percentile(data, [25, 75, 90, 99])
+ self.logger.info(f" Mesh {n}: 25th={p1[0]:{fmt}} 75th={p1[1]:{fmt}} 90th={p1[2]:{fmt}} 99th={p1[3]:{fmt}}")
+
+ print_percentiles( "Aspect Ratio", 'aspect_ratio' )
+ print_percentiles( "Shape Quality", 'shape_quality' )
+ print_percentiles( "Edge Length Ratio", 'edge_ratio' )
+ print_percentiles( "Min Dihedral Angle (degrees)", 'min_dihedral' )
+ print_percentiles( "Overall Quality Score", 'quality_score' )
+
+
+ def printQualityIssueSummary( self: Self ):
+ self.__loggerSection( "QUALITY ISSUE SUMMARY" )
+
+ def print_issue(meshID, metricName, issueDataName, dataName ):
+ pb = self.issues[ meshID ][ issueDataName ]
+ m = len( self.validMetrics[ meshID ][ dataName ] )
+ fmt = '.2f'
+ self.logger.info(f" {metricName:20}: {pb:,} ({(pb/m*100):{fmt}}%)")
+
+ for n, _ in enumerate( self.meshes.items() ):
+ # self.logger.info( f"\nMesh {n} Issues:" )
+ self.logger.info( f"Mesh {n} Issues:" )
+ print_issue( n, "Aspect Ratio > 100", "high_aspect_ratio", 'aspect_ratio' )
+ print_issue( n, "Shape Quality < 0.3", "low_shape_quality", 'shape_quality' )
+ print_issue( n, "Flatness < 0.01", "low_flatness", "flatness_ratio" )
+ print_issue( n, "Edge Ratio > 10", "high_edge_ratio", "edge_ratio" )
+ print_issue( n, "Min Dihedral < 5°", "critical_dihedral", "min_dihedral" )
+ print_issue( n, "Max Dihedral > 175°", "critical_max_dihedral",'max_dihedral' )
+ print_issue( n, "Combined (AR>100 & Q<0.3)", "combined" ,'aspect_ratio' )
+ print_issue( n, "CRITICAL (AR>100 & Q<0.3 & MinDih<5°", "critical_combo", 'aspect_ratio' )
+
+ self.compareIssuesFromBest()
+
+ self.printOverallQualityScore()
+
+
+ def printOverallQualityScore( self: Self ):
+ # self.logger.info( f"\nOverall Quality Score Distribution:" )
+ self.logger.info( f"Overall Quality Score Distribution:" )
+ for n, _ in enumerate( self.meshes.items() ):
+ qualityScore = self.overallQualityScore[ n ]
+ self.logger.info(f" Mesh {n}: Excellent (>80): {qualityScore[ 'excellent' ]:.1f}% Good (60-80): {qualityScore[ 'good' ]:.1f}% Fair (30-60): {qualityScore[ 'fair' ]:.1f}% Poor (≤30): {qualityScore[ 'poor' ]:.1f}%")
+
+ def printExtremeOutlierAnalysis( self: Self ):
+ #TODO : info in warning if bad ?
+ self.__loggerSection( "EXTREME OUTLIER ANALYSIS" )
+
+ # self.logger.warning( f"\n🚨 Elements with Aspect Ratio > 10,000:" )
+ self.logger.warning( f"🚨 Elements with Aspect Ratio > 10,000:" )
+ for n, _ in enumerate( self.meshes ):
+ extreme_ar = self.issues[n][ 'extreme_aspect_ratio' ]
+ data = self.analyzedMesh[n]
+ ar1 = data[ 'aspect_ratio' ]
+ self.logger.info(f" Mesh {n}: {np.sum(extreme_ar):,} elements ({np.sum(extreme_ar)/len(ar1)*100:.3f}%)")
+
+ if np.sum(extreme_ar) > 0:
+ vol1 = data[ "volume" ]
+ min_dih1 = data[ "min_dihedral" ]
+ sq1 = data[ "shape_quality" ]
+ self.logger.info(f" Worst AR: {ar1[extreme_ar].max():.2e}")
+ self.logger.info(f" Avg volume: {vol1[extreme_ar].mean():.2e}")
+ self.logger.info(f" Min dihedral: {min_dih1[extreme_ar].min():.2f}° - {min_dih1[extreme_ar].mean():.2f}° (avg)")
+ self.logger.info(f" Shape quality: {sq1[extreme_ar].min():.4f} - {sq1[extreme_ar].mean():.4f} (avg)")
+
+ if np.sum(extreme_ar) > 10:
+ # self.logger.warning(f"\n💡 Recommendation: Investigate/remove {np.sum(extreme_ar):,} extreme elements in Mesh {n}")
+ self.logger.warning(f"💡 Recommendation: Investigate/remove {np.sum(extreme_ar):,} extreme elements in Mesh {n}")
+ self.logger.warning(f" These are likely artifacts from mesh generation or geometry issues.")
+
+ # Nearly degenerate elements
+ # self.logger.warning( f"\n🚨 Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):")
+ self.logger.warning( f"🚨 Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):")
+ for n, _ in enumerate( self.meshes ):
+ degenerate = self.issues[ n ][ "degenerate" ]
+ data = self.validMetrics[ n ][ "min_dihedral" ]
+ self.logger.warning( f" Mesh {n}: {np.sum(degenerate):,} elements ({np.sum(degenerate)/len(data)*100:.3f}%)")
+
+
+ def printComparisonSummary( self: Self):
+ self.__loggerSection( "COMPARISON SUMMARY" )
+
+ for n, _ in enumerate( self.meshes ):
+ name = f"Mesh {n}"
+ if n == self.best:
+ name += " [BEST]"
+ elif n == self.worst:
+ name += " [LEAST GOOD]"
+
+ ar1_med = np.median( self.validMetrics[n]["aspect_ratio"] )
+ sq1_med = np.median( self.validMetrics[n]["shape_quality"] )
+ vol1_med = np.median(self.validMetrics[n]["volume"] )
+ min_edge1_med = np.median(self.validMetrics[n]["min_edge"] )
+ max_edge1_med = np.median(self.validMetrics[n]["max_edge"] )
+ edge_ratio1_med = np.median(self.validMetrics[n]["edge_ratio"] )
+ min_dih1_med = np.median( self.validMetrics[n]["min_dihedral"] )
+ max_dih1_med = np.median( self.validMetrics[n]["max_dihedral"] )
+ score1_med = np.median( self.validMetrics[n]["quality_score"] )
+
+ self.logger.info(f"{name}")
+ self.logger.info(f" Tetrahedra: {self.analyzedMesh[n]['tet']:,}")
+ self.logger.info(f" Median Aspect Ratio: {ar1_med:.2f}")
+ self.logger.info(f" Median Shape Quality: {sq1_med:.4f}")
+ self.logger.info(f" Median Volume: {vol1_med:.2e}")
+ self.logger.info(f" Median Min Edge: {min_edge1_med:.2e}")
+ self.logger.info(f" Median Max Edge: {max_edge1_med:.2e}")
+ self.logger.info(f" Median Edge Ratio: {edge_ratio1_med:.2f}")
+ self.logger.info(f" Median Min Dihedral: {min_dih1_med:.1f}°")
+ self.logger.info(f" Median Max Dihedral: {max_dih1_med:.1f}°")
+ self.logger.info(f" Median Quality Score: {score1_med:.1f}/100")
+
+ self.logger.info("\n" + "="*80)
+
+
+
+ def computeDeltasFromBest( self: Self ):
+ self.logger.info( f"Best mesh : Mesh {self.best}")
+ self.deltas = {}
+
+ n_tets_best = self.analyzedMesh[self.best][ 'tet']
+ ar_med_best = np.median( self.validMetrics[self.best]["aspect_ratio"] )
+ sq_med_best = np.median(self.validMetrics[self.best]["shape_quality"])
+ vol_med_best = np.median(self.validMetrics[self.best]["volume"])
+ min_edge_med_best = np.median(self.validMetrics[self.best]["min_edge"])
+ max_edge_med_best = np.median(self.validMetrics[self.best]["max_edge"])
+ edge_ratio_med_best = np.median(self.validMetrics[self.best]["edge_ratio"])
+
+ for n, _ in enumerate( self.meshes ):
+ n_tets = self.analyzedMesh[n][ 'tet']
+ ar_med = np.median( self.validMetrics[n]["aspect_ratio"] )
+ sq_med = np.median(self.validMetrics[n]["shape_quality"])
+ vol_med = np.median(self.validMetrics[n]["volume"])
+ min_edge_med = np.median(self.validMetrics[n]["min_edge"])
+ max_edge_med = np.median(self.validMetrics[n]["max_edge"])
+ edge_ratio_med = np.median(self.validMetrics[n]["edge_ratio"])
+
+
+ delta_tets = ((n_tets - n_tets_best) / n_tets_best * 100) if n_tets_best > 0 else 0
+ delta_ar = ((ar_med - ar_med_best) / ar_med_best * 100) if ar_med_best > 0 else 0
+ delta_sq = ((sq_med - sq_med_best) / sq_med_best * 100) if sq_med_best > 0 else 0
+ delta_vol = ((vol_med - vol_med_best) / vol_med_best * 100) if vol_med_best > 0 else 0
+ delta_min_edge = ((min_edge_med - min_edge_med_best) / min_edge_med_best * 100) if min_edge_med_best > 0 else 0
+ delta_max_edge = ((max_edge_med - max_edge_med_best) / max_edge_med_best * 100) if max_edge_med_best > 0 else 0
+ delta_edge_ratio = ((edge_ratio_med - edge_ratio_med_best) / edge_ratio_med_best * 100) if edge_ratio_med_best > 0 else 0
+ self.deltas[ n ] = { "tetrahedra": delta_tets, "aspect_ratio": delta_ar, "shape_quality": delta_sq, "volume": delta_vol, "min_edge": delta_min_edge, "max_edge": delta_max_edge, "edge_ratio": delta_edge_ratio }
+
+
+ dtets = [ f"{self.deltas[ n ][ 'tetrahedra' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ dar = [ f"{self.deltas[ n ][ 'aspect_ratio' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ dsq = [ f"{self.deltas[ n ][ 'shape_quality' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ dvol = [ f"{self.deltas[ n ][ 'volume' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ d_min_edge = [ f"{self.deltas[ n ][ 'min_edge' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ d_max_edge = [ f"{self.deltas[ n ][ 'max_edge' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ d_edge_ratio = [ f"{self.deltas[ n ][ 'edge_ratio' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
+ names = [ f"{f'Mesh {n}':16}" for n, _ in self.sorted[1:]]
+
+ self.logger.info(f"Changes vs Mesh {self.best} [BEST]:")
+ self.logger.info(f"{' Mesh:':25}{('').join( names )}")
+ self.logger.info(f"{' Tetrahedra:':25} {('').join(dtets)}%")
+ self.logger.info(f"{' Aspect Ratio:':25}{('').join( dar)}%")
+ self.logger.info(f"{' Shape Quality:':25}{('').join( dsq)}%")
+ self.logger.info(f"{' Volume:':25}{('').join( dvol)}%")
+ self.logger.info(f"{' Min Edge Length:':25}{('').join( d_min_edge)}%")
+ self.logger.info(f"{' Max Edge Length:':25}{('').join( d_max_edge)}%")
+ self.logger.info(f"{' Edge Length Ratio:':25}{('').join( d_edge_ratio)}%")
+
+
+
+ def createComparisonDashboard( self: Self ):
+ lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes ) ]
+ # Determine smart plot limits
+
+ ar_99 = []
+ for n, _ in enumerate( self.meshes ):
+ ar_99.append( np.percentile( self.validMetrics[n]["aspect_ratio"], 99 ) )
+
+ ar_99_max = np.max( np.array( ar_99 ) )
+
+ # ar_99_1 = np.percentile(ar1, 99)
+ # # ar_99_2 = np.percentile(ar2, 99)
+ # # ar_99_max = max(ar_99_1, ar_99_2)
+ # ar_99_max = ar_99_1
+
+ if ar_99_max < 10:
+ ar_plot_limit = 100
+ elif ar_99_max < 100:
+ ar_plot_limit = 1000
+ else:
+ ar_plot_limit = 10000
+
+ # print(f"Using AR plot limit: {ar_plot_limit} (99th percentiles: M1={ar_99_1:.1f}, M2={ar_99_2:.1f})")
+ # print(f"Using AR plot limit: {ar_plot_limit} (99th percentiles: M1={ar_99_1:.1f}")
+
+ # Set style
+ sns.set_style("whitegrid")
+ 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.96, 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.82, bottom=0.05, hspace=0.35, wspace=0.30)
+
+ # Title
+ # mesh1_name = filename1.split('/')[-1]
+ # mesh2_name = filename2.split('/')[-1]
+ suptitle = 'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n'
+ for n, _ in enumerate( self.meshes ):
+ # suptitle += f'Mesh {n}: {n_tets1:,} tets\t'
+ suptitle += f'Mesh {n}: tets\t'
+ fig.suptitle( suptitle,
+ fontsize=16, fontweight='bold', y=0.99)
+ # fig.suptitle(f'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n' +
+ # f'Mesh 1: {mesh1_name} ({n_tets1:,} tets)',
+ # # f'Mesh 1: {mesh1_name} ({n_tets1:,} tets) vs Mesh 2: {mesh2_name} ({n_tets2:,} tets)',
+ # fontsize=16, fontweight='bold', y=0.99)
+
+ # Color scheme
+ # color = matplotlib.colormaps[ 'tab20' ]
+ # def color[n]:
+ # nn = np.linspace(0,20)
+ # return matplotlib.colormaps[ 'tab20' ][nn]
+ color = matplotlib.pyplot.cm.tab10( np.arange(20))
+
+ # ==================== ROW 1: EXECUTIVE SUMMARY ====================
+
+ # 1. Overall Quality Score Distribution
+ ax1 = fig.add_subplot(gs_row1[0, 0])
+ bins = np.linspace(0, 100, 40)
+ for n, _ in enumerate( self.meshes ):
+ score1 = self.validMetrics[n][ 'quality_score' ]
+ ax1.hist(score1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], edgecolor='black', linewidth=0.5)
+ ax1.axvline(np.median(score1), color=color[n], 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 median lines
+ # ax1.axvline(np.median(score1), color=color1, linestyle='--', linewidth=2.5, alpha=0.9)
+ # ax1.axvline(np.median(score2), color=color2, linestyle='--', linewidth=2.5, alpha=0.9)
+
+ # Add summary text #### ONLY BEST AND WORST MESH?
+ # TODO
+ # ax1.text(0.98, 0.98,
+ # f'Median Score:\nM1: {np.median(score1):.1f}\n\n' +
+ # f'Excellent (>80):\nM1: {excellent1:.1f}%',
+ # # f'Median Score:\nM1: {np.median(score1):.1f}\nM2: {np.median(score2):.1f}\n\n' +
+ # # f'Excellent (>80):\nM1: {excellent1:.1f}%\nM2: {excellent2:.1f}%',
+ # transform=ax1.transAxes, va='top', ha='right',
+ # bbox=dict(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 ):
+ ar1 = self.validMetrics[n]["aspect_ratio"]
+ sq1 = self.validMetrics[n]["shape_quality"]
+ self.setSampleForPlot( ar1, n )
+
+ idx = self.sample[n]
+
+ mask1_plot = ar1[idx] < ar_plot_limit
+
+ ax2.scatter(ar1[idx][mask1_plot], sq1[idx][mask1_plot], alpha=0.4, s=5,
+ color=color[n], 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
+ problem_zone = Rectangle((100, 0), ar_plot_limit-100, 0.3,
+ alpha=0.2, facecolor='red', edgecolor='none', zorder=0)
+ ax2.add_patch(problem_zone)
+
+ # Count ALL elements
+ problem1_all = np.sum((ar1 > 100) & (sq1 < 0.3))
+ extreme1 = np.sum(ar1 > ar_plot_limit)
+
+ # Problem annotation
+ #TODO
+ # ax2.text(0.98, 0.02,
+ # f'PROBLEM ELEMENTS\n(AR>100 & Q<0.3):\n\n' +
+ # f'M1: {problem1_all:,}\n\n',
+ # # f'Change: {((problem2_all-problem1_all)/max(problem1_all,1)*100):+.1f}%',
+ # # f'M1: {problem1_all:,}\nM2: {problem2_all:,}\n\n' +
+ # transform=ax2.transAxes, va='bottom', ha='right',
+ # bbox=dict(boxstyle='round', facecolor='#ffcccc', alpha=0.95,
+ # edgecolor='darkred', linewidth=2),
+ # fontsize=9, fontweight='bold')
+
+ # # Outlier warning
+ # if extreme1 + extreme2 > 100:
+ # ax2.text(0.02, 0.98,
+ # f'⚠️ AR > {ar_plot_limit}\n(not shown):\n' +
+ # f'M1: {extreme1:,}\nM2: {extreme2:,}',
+ # transform=ax2.transAxes, va='top', ha='left',
+ # bbox=dict(boxstyle='round', facecolor='#ffe6cc', alpha=0.9),
+ # fontsize=7)
+
+ 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, ar_plot_limit])
+ 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')
+
+ summary_stats = []
+ summary_stats.append(['CRITICAL ISSUE', 'WORST', 'BEST', 'Change'])
+ summary_stats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
+
+ critical_combo1 = self.issues[ self.best ][ "critical_combo" ]
+ critical_combo2 = self.issues[ self.worst ][ "critical_combo" ]
+ ar1 = self.validMetrics[self.best][ "aspect_ratio" ]
+ ar2 = self.validMetrics[self.worst][ "aspect_ratio" ]
+
+ high_ar1 = self.issues[ self.best ][ "high_aspect_ratio" ]
+ high_ar2 = self.issues[ self.worst ][ "high_aspect_ratio" ]
+
+ low_sq1 = self.issues[self.best ][ "low_shape_quality" ]
+ low_sq2 = self.issues[self.worst][ "low_shape_quality" ]
+
+ critical_dih1 = self.issues[self.best]["critical_dihedral"]
+ critical_dih2 = self.issues[self.worst]["critical_dihedral"]
+
+ critical_max_dih1 = self.issues[self.best][ "critical_max_dihedral" ]
+ critical_max_dih2 = self.issues[self.worst][ "critical_max_dihedral" ]
+
+ high_edge_ratio1 = self.issues[self.best][ "high_edge_ratio" ]
+ high_edge_ratio2 = self.issues[self.worst][ "high_edge_ratio" ]
+
+
+ summary_stats.append(['CRITICAL Combo', f'{critical_combo1:,}', f'{critical_combo2:,}',
+ f'{((critical_combo2-critical_combo1)/max(critical_combo1,1)*100):+.1f}%' if critical_combo1 > 0 else 'N/A'])
+ summary_stats.append(['(AR>100 & Q<0.3', f'({critical_combo1/len(ar1)*100:.2f}%)',
+ f'({critical_combo2/len(ar2)*100:.2f}%)', ''])
+ summary_stats.append([' & MinDih<5°)', '', '', ''])
+
+ summary_stats.append(['', '', '', ''])
+ summary_stats.append(['AR > 100', f'{high_ar1:,}', f'{high_ar2:,}',
+ f'{((high_ar2-high_ar1)/max(high_ar1,1)*100):+.1f}%'])
+
+ summary_stats.append(['Quality < 0.3', f'{low_sq1:,}', f'{low_sq2:,}',
+ f'{((low_sq2-low_sq1)/max(low_sq1,1)*100):+.1f}%'])
+
+ summary_stats.append(['MinDih < 5°', f'{critical_dih1:,}', f'{critical_dih2:,}',
+ f'{((critical_dih2-critical_dih1)/max(critical_dih1,1)*100):+.1f}%' if critical_dih1 > 0 else 'N/A'])
+
+ summary_stats.append(['MaxDih > 175°', f'{critical_max_dih1:,}', f'{critical_max_dih2:,}',
+ f'{((critical_max_dih2-critical_max_dih1)/max(critical_max_dih1,1)*100):+.1f}%' if critical_max_dih1 > 0 else 'N/A'])
+
+ summary_stats.append(['Edge Ratio > 10', f'{high_edge_ratio1:,}', f'{high_edge_ratio2:,}',
+ f'{((high_edge_ratio2-high_edge_ratio1)/max(high_edge_ratio1,1)*100):+.1f}%'])
+
+ summary_stats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
+ summary_stats.append(['Quality Grade', '', '', ''])
+ excellent1 = self.overallQualityScore[self.best]["excellent"]
+ excellent2 = self.overallQualityScore[self.worst]["excellent"]
+ good1 = self.overallQualityScore[self.best]["good"]
+ good2 = self.overallQualityScore[self.worst]["good"]
+ poor1 = self.overallQualityScore[self.best]["poor"]
+ poor2 = self.overallQualityScore[self.worst]["poor"]
+
+
+ summary_stats.append([' Excellent (>80)', f'{excellent1:.1f}%', f'{excellent2:.1f}%',
+ f'{excellent2-excellent1:+.1f}%'])
+ summary_stats.append([' Good (60-80)', f'{good1:.1f}%', f'{good2:.1f}%',
+ f'{good2-good1:+.1f}%'])
+ summary_stats.append([' Poor (≤30)', f'{poor1:.1f}%', f'{poor2:.1f}%',
+ f'{poor2-poor1:+.1f}%'])
+
+ table = ax3.table(cellText=summary_stats, cellLoc='left',
+ bbox=[0, 0, 1, 1], 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='white', 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(summary_stats):
+ change_text = summary_stats[row][3]
+ if '%' in change_text and change_text != 'N/A':
+ try:
+ val = float(change_text.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')
+ except:
+ pass
+
+ 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)
+ for n, _ in enumerate( self.meshes ):
+ sq1 = self.validMetrics[n][ "shape_quality" ]
+ ax4.hist(sq1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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])
+ # bins = np.logspace(0, np.log10(min(ar_plot_limit, ar1.max(), ar2.max())), 40)
+ ar_max = np.array( [ self.validMetrics[n]["aspect_ratio"].max() for n, _ in enumerate( self.meshes )] )
+
+ bins = np.logspace(0, np.log10(min(ar_plot_limit, ar_max.max())), 40)
+ for n, _ in enumerate( self.meshes ):
+ ar1 = self.validMetrics[n]['aspect_ratio']
+ ax5.hist(ar1[ar1 < ar_plot_limit], bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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)
+ for n, _ in enumerate( self.meshes ):
+ min_dih1 = self.validMetrics[ n ]["min_dihedral"]
+ ax6.hist(min_dih1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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)
+ for n, _ in enumerate( self.meshes ):
+ edge_ratio1 = self.validMetrics[ n ][ "edge_ratio" ]
+ ax7.hist(edge_ratio1[edge_ratio1 < 1000], bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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])
+ vol_min = np.array( [ self.validMetrics[n]["volume"].min() for n, _ in enumerate( self.meshes )] ).min()
+ vol_max = np.array( [ self.validMetrics[n]["volume"].max() for n, _ in enumerate( self.meshes )] ).max()
+
+
+ bins = np.logspace(np.log10(vol_min), np.log10(vol_max), 40)
+ for n, _ in enumerate( self.meshes ):
+ vol1 = self.validMetrics[n][ "volume" ]
+ ax8.hist(vol1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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][ "shape_quality" ] for n, _ in enumerate( self.meshes ) ]
+ bp1 = ax9.boxplot( sq, labels=lbl,
+ patch_artist=True, showfliers=False)
+ 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][ "aspect_ratio" ] for n, _ in enumerate( self.meshes ) ]
+ bp2 = ax10.boxplot( ar, labels=lbl,
+ patch_artist=True, showfliers=False)
+ 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])
+ min_dih1 = [ self.validMetrics[n][ "min_dihedral" ] for n, _ in enumerate( self.meshes ) ]
+ bp3 = ax11.boxplot( min_dih1, labels=lbl,
+ patch_artist=True, showfliers=False)
+ 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])
+ edge_ratio = [ self.validMetrics[n][ "edge_ratio" ] for n, _ in enumerate( self.meshes ) ]
+ bp4 = ax12.boxplot(edge_ratio, labels=lbl,
+ patch_artist=True, showfliers=False)
+ 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 ) ]
+ bp5 = ax13.boxplot( vol, labels=lbl,
+ patch_artist=True, showfliers=False)
+ 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 ):
+ bp1['boxes'][n].set_facecolor(color[n])
+ bp1['medians'][n].set_color("black")
+ bp2['boxes'][n].set_facecolor(color[n])
+ bp2['medians'][n].set_color("black")
+ bp3['boxes'][n].set_facecolor(color[n])
+ bp3['medians'][n].set_color("black")
+ bp4['boxes'][n].set_facecolor(color[n])
+ bp4['medians'][n].set_color("black")
+ bp5['boxes'][n].set_facecolor(color[n])
+ bp5['medians'][n].set_color("black")
+
+ # ==================== ROW 4: CORRELATION ANALYSIS (SCATTER PLOTS) ====================
+
+ # Use existing idx1, idx2 from executive summary
+
+ # 14. Shape Quality vs Aspect Ratio (duplicate for detail)
+ ax14 = fig.add_subplot(gs_main[2, 0])
+ for n, _ in enumerate( self.meshes ):
+ idx = self.sample[n]
+ ar1 = self.validMetrics[n][ 'aspect_ratio']
+ sq1 = self.validMetrics[n][ 'shape_quality']
+ mask1 = ar1[idx] < ar_plot_limit
+ ax14.scatter(ar1[idx][mask1], sq1[idx][mask1], alpha=0.4, s=5,
+ color=color[n], 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, ar_plot_limit])
+ 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 ):
+ idx = self.sample[n]
+ ar1 = self.validMetrics[n]["aspect_ratio"]
+ fr1 = self.validMetrics[n][ 'flatness_ratio' ]
+ mask1 = ar1[idx] < ar_plot_limit
+ ax15.scatter(ar1[idx][mask1], fr1[idx][mask1], alpha=0.4, s=5,
+ color=color[n], 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, ar_plot_limit])
+ 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 ):
+ idx = self.sample[n]
+ ar1 = self.validMetrics[n]["aspect_ratio"]
+ vol1 = self.validMetrics[n][ 'volume' ]
+ mask1 = ar1[idx] < ar_plot_limit
+ ax16.scatter(vol1[idx][mask1], ar1[idx][mask1], alpha=0.4, s=5,
+ color=color[n], 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, ar_plot_limit])
+ 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 ):
+ idx = self.sample[n]
+ vol1 = self.validMetrics[n][ 'volume' ]
+ sq1 = self.validMetrics[n][ 'shape_quality']
+ ax17.scatter(vol1[idx], sq1[idx], alpha=0.4, s=5,
+ color=color[n], 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 ):
+ idx = self.sample[n]
+ vol1 = self.validMetrics[n][ 'volume' ]
+ edge_ratio = self.validMetrics[n][ 'edge_ratio' ]
+ ax18.scatter(vol1[idx], edge_ratio[idx], alpha=0.4, s=5,
+ color=color[n], 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])
+ edge_min = np.array( [ self.validMetrics[n]["min_edge"].min() for n,_ in enumerate( self.meshes ) ] ).min()
+ edge_max_min = np.array( [ self.validMetrics[n]["min_edge"].max() for n,_ in enumerate( self.meshes ) ] ).min()
+
+ bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
+
+ for n, _ in enumerate( self.meshes ):
+ # edge_min = min(min_edge1.min(), min_edge2.min())
+ # edge_max_min = max(min_edge1.max(), min_edge2.max())
+ # bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
+ min_edge = self.validMetrics[n]['min_edge']
+ ax19.hist(min_edge, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], edgecolor='black', linewidth=0.5)
+ ax19.axvline(np.median(min_edge), color=color[n], 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])
+ edge_max = np.array( [ self.validMetrics[n]["max_edge"].max() for n,_ in enumerate( self.meshes ) ] ).max()
+ edge_min_max = np.array( [ self.validMetrics[n]["max_edge"].min() for n,_ in enumerate( self.meshes ) ] ).min()
+
+ # edge_max = max(max_edge1.max(), max_edge2.max())
+ # edge_min_max = min(max_edge1.min(), max_edge2.min())
+ bins = np.logspace(np.log10(edge_min_max), np.log10(edge_max), 40)
+ for n, _ in enumerate( self.meshes ):
+ max_edge1 = self.validMetrics[n]["max_edge"]
+ ax20.hist(max_edge1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], edgecolor='black', linewidth=0.5)
+ ax20.axvline(np.median(max_edge1), color=color[n], 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)
+ for n, _ in enumerate( self.meshes ):
+ max_dih1 = self.validMetrics[n][ "max_dihedral" ]
+ ax21.hist(max_dih1, bins=bins, alpha=0.6, label=f'Mesh {n}',
+ color=color[n], 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]["min_dihedral"] for n, _ in enumerate( self.meshes )] + [ self.validMetrics[n]["max_dihedral"] for n, _ in enumerate( self.meshes )]
+ lbl_boxplot = [ f'M{n}Min' for n, _ in enumerate( self.meshes )] + [ f'M{n}Max' for n, _ in enumerate( self.meshes )]
+ boxplot_color = [ n for n, _ in enumerate( self.meshes ) ] + [ n for n, _ in enumerate( self.meshes ) ]
+ bp_dih = ax22.boxplot(dih,
+ positions=positions,
+ labels=lbl_boxplot,
+ 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] ])
+
+ 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 ):
+ sq1 = self.validMetrics[n][ "shape_quality"]
+ sorted_sq1 = np.sort(sq1)
+ cdf_sq1 = np.arange(1, len(sorted_sq1) + 1) / len(sorted_sq1) * 100
+ ax23.plot(sorted_sq1, cdf_sq1, color=color[n], 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
+ output_png = 'mesh_comparison.png'
+ print(f"\nSaving dashboard to: {output_png}")
+ plt.savefig(output_png, dpi=300, bbox_inches='tight', facecolor='white')
+ print(f"Dashboard saved successfully!")
+
+
+
+
+ def __loggerSection( self: Self, sectionName: str ):
+ # self.logger.info("\n" + "="*80)
+ self.logger.info("="*80)
+ self.logger.info( sectionName )
+ self.logger.info("="*80)
+
+
+ def __orderMeshes( self: Self ):
+ """Proposition of ordering as fonction of median quality score"""
+ self.logger.info( "Ordering the meshes" )
+ median_score = { n : np.median( self.validMetrics[n]["quality_score"] ) for n, _ in enumerate (self.meshes) }
+
+ sorted_meshes = sorted( median_score.items(), key=lambda x:x[1], reverse = True )
+ self.sorted = sorted_meshes
+ self.best = sorted_meshes[ 0 ][0]
+ self.worst = sorted_meshes[ -1 ][0]
+
+ self.logger.info( f"Best Mesh: Mesh {self.best} vs worst Mesh [{self.worst}]")
+
+ self.logger.info( f"Mesh order from median quality score:" )
+ top = [ f"Mesh {m[0]} ({m[1]:.2f})" for m in sorted_meshes ]
+ toprint = (" > ").join( top )
+ self.logger.info( " [+] " + toprint + " [-]" )
+
+ def compareIssuesFromBest( self: Self ):
+ high_ar1 = self.issues[ self.best ][ "high_aspect_ratio" ]
+ critical_dih1 = self.issues[ self.best ]["critical_dihedral"]
+ low_sq1 = self.issues[ self.best ]["low_shape_quality"]
+ critical_combo1 = self.issues[ self.best ]["critical_combo"]
+
+ def getPercentChangeFromBest( data, ref ):
+ return (data - ref) / max( ref, 1)*100
+
+ self.logger.info (f"Change from BEST [Mesh {self.best}]" )
+ high_ar = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'high_aspect_ratio' ], high_ar1 ):+15,.1f}%" for n, _ in self.sorted ]
+ low_sq = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'low_shape_quality' ], low_sq1 ):+15,.1f}%" for n, _ in self.sorted ]
+ critical_dih = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'critical_dihedral' ], critical_dih1 ):+15,.1f}%" if critical_dih1 > 0 else f"{'N/A':16}" for n, _ in self.sorted]
+ critical_combo = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'critical_combo' ], critical_combo1 ):+15,.1f}%" if critical_combo1 > 0 else f"{'N/A':16}" for n, _ in self.sorted]
+
+ self.logger.info( f"{' AR > 100:':25}{('').join( high_ar )}" )
+ self.logger.info( f"{' Quality < 0.3:':25}{('').join( low_sq )}" )
+ self.logger.info( f"{' MinDih < 5°:':25}{('').join( critical_dih )}" )
+ self.logger.info( f"{' CRITICAL combo:':25}{('').join( critical_combo )}" )
+
+
+ def setSampleForPlot( self: Self, data, n ):
+ n_sample = min(10000, len( data ))
+ self.sample[n] = np.random.choice(len(data), n_sample, replace=False)
+
+# Combined quality score
+def compute_quality_score(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
+ """Compute combined quality score (0-100)."""
+ ar_norm = np.clip(1.0 / (aspectRatio / 1.73), 0, 1)
+ sq_norm = shapeQuality
+ er_norm = np.clip(1.0 / edgeRatio, 0, 1)
+ dih_norm = np.clip(minDihedralAngle / 60.0, 0, 1)
+ score = (0.3 * ar_norm + 0.4 * sq_norm + 0.2 * er_norm + 0.1 * dih_norm) * 100
+ return score
+
+
From e1bb281f99571d4c8f0d291b4f06aa7362fefcc8 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Fri, 30 Jan 2026 15:14:19 +0100
Subject: [PATCH 02/11] log display
---
.../pre_processing/TetQualityAnalysis.py | 963 +++++++++---------
1 file changed, 475 insertions(+), 488 deletions(-)
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index 05fbbbbe..2681f1da 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -16,7 +16,6 @@
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Rectangle
-import seaborn as sns
__doc__ = """
TetQualityAnalysis module is a filter that performs an an analysis of tetrahedras of one or several meshes and plot a summary.
@@ -33,11 +32,11 @@ def __init__(
) -> None:
self.meshes: dict[ str, vtkDataSet ] = meshes
- # self.meshes = Set( )
self.analyzedMesh: dict[ Any ] = {}
self.issues: dict[ Any ] = {}
- self.overallQualityScore: dict[ Any] = {}
- self.validMetrics: dict[Any] = {}
+ self.qualityScore: dict[ Any] = {}
+ self.validMetrics: dict[int, dict[str, np.float64]] = {}
+ self.medians: dict[ int, dict[str, np.float64 ]] = {}
self.sample = {}
# Logger.
@@ -72,95 +71,103 @@ def applyFilter( self: Self) -> None:
self.__loggerSection( "MESH COMPARISON DASHBOARD" )
- for n, (filename1, mesh1) in enumerate( self.meshes.items() ):
- # print( n, filename1 )
- coords1 = self.get_coordinates_double_precision(mesh1)
- tet_ids1, tet_conn1 = self.extract_tet_connectivity(mesh1)
- n_tets1 = len( tet_ids1 )
+ for n, (nfilename, mesh) in enumerate( self.meshes.items(), 1 ):
+ # print( n, nfilename )
+ coords = self.getCoordinatesDoublePrecision(mesh)
+ tetrahedraIds, tetrahedraConnectivity = self.extractTetConnectivity(mesh)
+ ntets = len( tetrahedraIds )
- self.logger.info( f" Mesh {n} info: " )
- self.logger.info( f" Name: {filename1}" )
- self.logger.info( f" Total cells: {mesh1.GetNumberOfCells()}" )
- self.logger.info( f" Tetrahedra: {n_tets1}" )
- self.logger.info( f" Points: {mesh1.GetNumberOfPoints()}" )
+ 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()}" +
+ f"\n" + "-"*80 + "\n" )
# Analyze both meshes
- print(f"\nAnalyzing Mesh {n} (double precision)...")
- # metrics1 = self.analyze_all_tets_vectorized(n, coords1, tet_conn1)
- self.analyze_all_tets_vectorized(n, coords1, tet_conn1)
- metrics1 = self.analyzedMesh[ n ]
- self.analyzedMesh[n]["tet"] = n_tets1
+ self.analyzeAllTets(n, coords, tetrahedraConnectivity)
+ metrics = self.analyzedMesh[ n ]
+ self.analyzedMesh[n]["tet"] = ntets
# Extract data with consistent filtering
- ar_valid1 = np.isfinite(metrics1['aspect_ratio'])
- rr_valid1 = np.isfinite(metrics1['radius_ratio'])
+ validAspectRatio = np.isfinite(metrics['aspectRatio'])
+ validRadiusRatio = np.isfinite(metrics['radiusRatio'])
# Combined valid mask
- valid1 = ar_valid1 & rr_valid1
+ validMask = validAspectRatio & validRadiusRatio
- ar1 = metrics1['aspect_ratio'][valid1]
- rr1 = metrics1['radius_ratio'][valid1]
- fr1 = metrics1['flatness_ratio'][valid1]
- vol1 = metrics1['volumes'][valid1]
- sq1 = metrics1['shape_quality'][valid1]
+ aspectRatio = metrics['aspectRatio'][validMask]
+ radiusRatio = metrics['radiusRatio'][validMask]
+ flatnessRatio = metrics['flatnessRatio'][validMask]
+ volume = metrics['volumes'][validMask]
+ shapeQuality = metrics['shapeQuality'][validMask]
# Edge length data
- min_edge1 = metrics1['min_edge'][valid1]
- max_edge1 = metrics1['max_edge'][valid1]
+ minEdge = metrics['minEdge'][validMask]
+ maxEdge = metrics['maxEdge'][validMask]
# Edge length ratio
- edge_ratio1 = max_edge1 / np.maximum(min_edge1, 1e-15)
+ edgeRatio = maxEdge / np.maximum(minEdge, 1e-15)
# Dihedral angles
- min_dih1 = metrics1['min_dihedral'][valid1]
- max_dih1 = metrics1['max_dihedral'][valid1]
- dih_range1 = metrics1['dihedral_range'][valid1]
+ minDihedral = metrics['minDihedral'][validMask]
+ maxDihedral = metrics['maxDihedral'][validMask]
+ dihedralRange = metrics['dihedral_range'][validMask]
- score1 = compute_quality_score(ar1, sq1, edge_ratio1, min_dih1)
+ qualityScore = compute_quality_score(aspectRatio, shapeQuality, edgeRatio, minDihedral)
# Store all values
- self.validMetrics[n] = { 'aspect_ratio' : ar1, 'radius_ratio': rr1, 'flatness_ratio': fr1, 'volume': vol1, 'shape_quality': sq1, 'min_edge': min_edge1, 'max_edge': max_edge1, 'edge_ratio': edge_ratio1, 'min_dihedral': min_dih1, 'max_dihedral': max_dih1, 'dihedral_range': dih_range1, 'quality_score': score1}
+ self.validMetrics[n] = { 'aspectRatio' : aspectRatio, 'radiusRatio': radiusRatio, 'flatnessRatio': flatnessRatio, 'volume': volume, 'shapeQuality': shapeQuality, 'minEdge': minEdge, 'maxEdge': maxEdge, 'edgeRatio': edgeRatio, 'minDihedral': minDihedral, 'maxDihedral': maxDihedral, 'dihedral_range': dihedralRange, 'qualityScore': qualityScore}
# # ==================== Print Distribution Statistics ====================
# Problem element counts
- high_ar1 = np.sum(ar1 > 100)
- low_sq1 = np.sum(sq1 < 0.3)
- low_flat1 = np.sum(fr1 < 0.01)
- high_edge_ratio1 = np.sum(edge_ratio1 > 10)
- critical_dih1 = np.sum(min_dih1 < 5)
- critical_max_dih1 = np.sum(max_dih1 > 175)
- problem1 = np.sum((ar1 > 100) & (sq1 < 0.3))
- critical_combo1 = np.sum((ar1 > 100) & (sq1 < 0.3) & (min_dih1 < 5))
+ 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] = { "high_aspect_ratio": high_ar1, "low_shape_quality": low_sq1, "low_flatness": low_flat1, "high_edge_ratio": high_edge_ratio1, "critical_dihedral": critical_dih1, "critical_max_dihedral": critical_max_dih1, "combined": problem1, "critical_combo": critical_combo1 }
+ self.issues[n] = { "highAspectRatio": highAspectRatio, "lowShapeQuality": lowShapeQuality, "lowFlatness": lowFlatness, "highEdgeRatio": highEdgeRatio, "criticalMinDihedral": criticalMinDihedral, "criticalMaxDihedral": criticalMaxDihedral, "combined": combined, "criticalCombo": criticalCombo }
# Overall quality scores
- excellent1 = np.sum(score1 > 80) / len(score1) * 100
- good1 = np.sum((score1 > 60) & (score1 <= 80)) / len(score1) * 100
- fair1 = np.sum((score1 > 30) & (score1 <= 60)) / len(score1) * 100
- poor1 = np.sum(score1 <= 30) / len(score1) * 100
+ 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.overallQualityScore[n] = { 'excellent': excellent1, 'good': good1, 'fair': fair1, 'poor': poor1 }
+ self.qualityScore[n] = { 'excellent': excellent, 'good': good, 'fair': fair, 'poor': poor }
- extreme_ar1 = ar1 > 1e4
- self.issues[ n ][ "extreme_aspect_ratio" ] = extreme_ar1
+ extremeAspectRatio = aspectRatio > 1e4
+ self.issues[ n ][ "extremeAspectRatio" ] = extremeAspectRatio
# Nearly degenerate elements
- degenerate1 = (min_dih1 < 0.1) | (max_dih1 > 179.9)
- self.issues[ n ][ "degenerate" ] = degenerate1
+ 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 ====================
- print("\n" + "="*80 + "\n")
-
- # ==================== Create Dashboard ====================
+ self.printDistributionStatistics()
- # ==================== Print Distribution Statistics ====================
self.__orderMeshes()
- self.printDistributionStatistics()
-
# Percentile information
self.printPercentileAnalysis()
@@ -169,45 +176,42 @@ def applyFilter( self: Self) -> None:
self.printExtremeOutlierAnalysis()
self.printComparisonSummary()
- self.computeDeltasFromBest()
+ self.computeDeltasFromBest()
self.createComparisonDashboard()
- def get_coordinates_double_precision( self, mesh ):
+ def getCoordinatesDoublePrecision( self, mesh ):
points = mesh.GetPoints()
- n_points = points.GetNumberOfPoints()
+ npoints = points.GetNumberOfPoints()
- coords = np.zeros((n_points, 3), dtype=np.float64)
- for i in range(n_points):
+ coords = np.zeros((npoints, 3), dtype=np.float64)
+ for i in range(npoints):
point = points.GetPoint(i)
coords[i] = [point[0], point[1], point[2]]
return coords
- def extract_tet_connectivity( self, mesh ):
+ def extractTetConnectivity( self, mesh ):
"""Extract connectivity for all tetrahedra."""
- n_cells = mesh.GetNumberOfCells()
- tet_ids = []
- tet_connectivity = []
+ ncells = mesh.GetNumberOfCells()
+ tetrahedraIds = []
+ tetrahedraConnectivity = []
- for cell_id in range(n_cells):
- if mesh.GetCellType(cell_id) == vtk.VTK_TETRA:
- cell = mesh.GetCell(cell_id)
+ for cellID in range(ncells):
+ if mesh.GetCellType(cellID) == vtk.VTK_TETRA:
+ cell = mesh.GetCell(cellID)
point_ids = cell.GetPointIds()
conn = [point_ids.GetId(i) for i in range(4)]
- tet_ids.append(cell_id)
- tet_connectivity.append(conn)
-
- return np.array(tet_ids), np.array(tet_connectivity)
+ tetrahedraIds.append(cellID)
+ tetrahedraConnectivity.append(conn)
+ return np.array(tetrahedraIds), np.array(tetrahedraConnectivity)
-
-
- def analyze_all_tets_vectorized(self, n, coords, connectivity) -> dict:
+ def analyzeAllTets(self, n, coords, connectivity) -> dict:
"""Vectorized analysis of all tetrahedra."""
- n_tets = connectivity.shape[0]
+ ntets = connectivity.shape[0]
# Get coordinates for all vertices
v0 = coords[connectivity[:, 0]]
@@ -231,39 +235,39 @@ def analyze_all_tets_vectorized(self, n, coords, connectivity) -> dict:
l13 = np.linalg.norm(e13, axis=1)
l23 = np.linalg.norm(e23, axis=1)
- all_edges = np.stack([l01, l02, l03, l12, l13, l23], axis=1)
- min_edge = np.min(all_edges, axis=1)
- max_edge = np.max(all_edges, axis=1)
+ allEdges = np.stack([l01, l02, l03, l12, l13, l23], axis=1)
+ minEdge = np.min(allEdges, axis=1)
+ maxEdge = np.max(allEdges, axis=1)
# Volumes
cross_product = np.cross(e01, e02)
volumes = np.abs(np.sum(cross_product * e03, axis=1) / 6.0)
# Face areas
- face_012 = 0.5 * np.linalg.norm(np.cross(e01, e02), axis=1)
- face_013 = 0.5 * np.linalg.norm(np.cross(e01, e03), axis=1)
- face_023 = 0.5 * np.linalg.norm(np.cross(e02, e03), axis=1)
- face_123 = 0.5 * np.linalg.norm(np.cross(e12, e13), axis=1)
+ face012 = 0.5 * np.linalg.norm(np.cross(e01, e02), axis=1)
+ face013 = 0.5 * np.linalg.norm(np.cross(e01, e03), axis=1)
+ face023 = 0.5 * np.linalg.norm(np.cross(e02, e03), axis=1)
+ face123 = 0.5 * np.linalg.norm(np.cross(e12, e13), axis=1)
- all_faces = np.stack([face_012, face_013, face_023, face_123], axis=1)
- max_face_area = np.max(all_faces, axis=1)
- total_surface_area = np.sum(all_faces, axis=1)
+ allFaces = np.stack([face012, face013, face023, face123], axis=1)
+ maxFaceArea = np.max(allFaces, axis=1)
+ totalSurfaceArea = np.sum(allFaces, axis=1)
# Aspect ratio
- min_altitude = 3.0 * volumes / np.maximum(max_face_area, 1e-15)
- aspect_ratio = max_edge / np.maximum(min_altitude, 1e-15)
- aspect_ratio[volumes < 1e-15] = np.inf
+ minAltitude = 3.0 * volumes / np.maximum(maxFaceArea, 1e-15)
+ aspectRatio = maxEdge / np.maximum(minAltitude, 1e-15)
+ aspectRatio[volumes < 1e-15] = np.inf
# Radius ratio
- inradius = 3.0 * volumes / np.maximum(total_surface_area, 1e-15)
- circumradius = (max_edge ** 3) / np.maximum(24.0 * volumes, 1e-15)
+ inradius = 3.0 * volumes / np.maximum(totalSurfaceArea, 1e-15)
+ circumradius = (maxEdge ** 3) / np.maximum(24.0 * volumes, 1e-15)
radius_ratio = circumradius / np.maximum(inradius, 1e-15)
# Flatness ratio
- flatness_ratio = volumes / np.maximum(max_face_area * min_edge, 1e-15)
+ flatness_ratio = volumes / np.maximum(maxFaceArea * minEdge, 1e-15)
# Shape quality
- sum_edge_sq = np.sum(all_edges ** 2, axis=1)
+ sum_edge_sq = np.sum(allEdges ** 2, axis=1)
shape_quality = 12.0 * (3.0 * volumes) ** (2.0/3.0) / np.maximum(sum_edge_sq, 1e-15)
shape_quality[volumes < 1e-15] = 0.0
@@ -300,95 +304,91 @@ def compute_dihedral_angle(normal1, normal2):
self.analyzedMesh[ n ] = {
'volumes': volumes,
- 'aspect_ratio': aspect_ratio,
- 'radius_ratio': radius_ratio,
- 'flatness_ratio': flatness_ratio,
- 'shape_quality': shape_quality,
- 'min_edge': min_edge,
- 'max_edge': max_edge,
- 'min_dihedral': min_dihedral,
- 'max_dihedral': max_dihedral,
+ 'aspectRatio': aspectRatio,
+ 'radiusRatio': radius_ratio,
+ 'flatnessRatio': flatness_ratio,
+ 'shapeQuality': shape_quality,
+ 'minEdge': minEdge,
+ 'maxEdge': maxEdge,
+ 'minDihedral': min_dihedral,
+ 'maxDihedral': max_dihedral,
'dihedral_range': dihedral_range
}
return {
'volumes': volumes,
- 'aspect_ratio': aspect_ratio,
- 'radius_ratio': radius_ratio,
- 'flatness_ratio': flatness_ratio,
- 'shape_quality': shape_quality,
- 'min_edge': min_edge,
- 'max_edge': max_edge,
- 'min_dihedral': min_dihedral,
- 'max_dihedral': max_dihedral,
+ 'aspectRatio': aspectRatio,
+ 'radiusRatio': radius_ratio,
+ 'flatnessRatio': flatness_ratio,
+ 'shapeQuality': shape_quality,
+ 'minEdge': minEdge,
+ 'maxEdge': maxEdge,
+ 'minDihedral': min_dihedral,
+ 'maxDihedral': max_dihedral,
'dihedral_range': dihedral_range
}
def printDistributionStatistics( self: Self, fmt='.2e'):
self.__loggerSection( "DISTRIBUTION STATISTICS (MIN / MEDIAN / MAX)" )
- def print_metric_stats( metricName, dataName, fmt='.2e'):
+ def printMetricStats( metricName, dataName, fmt='.2e'):
"""Helper function to print min/median/max for a metric."""
- # self.logger.info(f"\n{metricName}:")
- self.logger.info(f"{metricName}:")
- for n, _ in enumerate( self.meshes.items() ):
+ msg = f"{metricName}:\n"
+ for n, _ in enumerate( self.meshes.items(), 1 ):
data = self.validMetrics[ n ][ dataName ]
- self.logger.info(f" Mesh {n}: Min={data.min():{fmt}} Median={np.median(data):{fmt}} Max={data.max():{fmt}}")
+ 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 )
- print_metric_stats( "Aspect Ratio", 'aspect_ratio' )
- print_metric_stats( "Radius Ratio", 'radius_ratio' )
- print_metric_stats( "Flatness Ratio", 'flatness_ratio' )
- print_metric_stats( "Shape Quality", 'shape_quality', fmt='.4f' )
- print_metric_stats( "Volume", 'volume' )
- print_metric_stats( "Min Edge Length", 'min_edge' )
- print_metric_stats( "Max Edge Length", 'max_edge' )
- print_metric_stats( "Edge Length Ratio", 'edge_ratio', fmt='.2f' )
- print_metric_stats( "Min Dihedral Angle (degrees)", 'min_dihedral', fmt='.2f' )
- print_metric_stats( "Max Dihedral Angle (degrees)", 'max_dihedral', fmt='.2f' )
- print_metric_stats( "Dihedral Range (degrees)", 'dihedral_range',fmt='.2f' )
- print_metric_stats( "Overall Quality Score (0-100)", 'quality_score', fmt='.2f' )
+ 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)", 'dihedral_range',fmt='.2f' )
+ printMetricStats( "Overall Quality Score (0-100)", 'qualityScore', fmt='.2f' )
def printPercentileAnalysis( self: Self, fmt='.2f' ):
self.__loggerSection( "PERCENTILE ANALYSIS (25th / 75th / 90th / 99th)" )
- def print_percentiles(metricName, dataName, fmt='.2f'):
- """Helper function to print percentiles."""
- self.logger.info(f"{metricName}:")
- # self.logger.info(f"\n{metricName}:")
- for n, _ in enumerate( self.meshes.items() ):
- data = self.validMetrics[ n ][ dataName ]
+ 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])
- self.logger.info(f" Mesh {n}: 25th={p1[0]:{fmt}} 75th={p1[1]:{fmt}} 90th={p1[2]:{fmt}} 99th={p1[3]:{fmt}}")
-
- print_percentiles( "Aspect Ratio", 'aspect_ratio' )
- print_percentiles( "Shape Quality", 'shape_quality' )
- print_percentiles( "Edge Length Ratio", 'edge_ratio' )
- print_percentiles( "Min Dihedral Angle (degrees)", 'min_dihedral' )
- print_percentiles( "Overall Quality Score", 'quality_score' )
+ 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 )
- def printQualityIssueSummary( self: Self ):
+ def printQualityIssueSummary( self: Self ) -> None:
self.__loggerSection( "QUALITY ISSUE SUMMARY" )
- def print_issue(meshID, metricName, issueDataName, dataName ):
- pb = self.issues[ meshID ][ issueDataName ]
- m = len( self.validMetrics[ meshID ][ dataName ] )
- fmt = '.2f'
- self.logger.info(f" {metricName:20}: {pb:,} ({(pb/m*100):{fmt}}%)")
-
- for n, _ in enumerate( self.meshes.items() ):
- # self.logger.info( f"\nMesh {n} Issues:" )
- self.logger.info( f"Mesh {n} Issues:" )
- print_issue( n, "Aspect Ratio > 100", "high_aspect_ratio", 'aspect_ratio' )
- print_issue( n, "Shape Quality < 0.3", "low_shape_quality", 'shape_quality' )
- print_issue( n, "Flatness < 0.01", "low_flatness", "flatness_ratio" )
- print_issue( n, "Edge Ratio > 10", "high_edge_ratio", "edge_ratio" )
- print_issue( n, "Min Dihedral < 5°", "critical_dihedral", "min_dihedral" )
- print_issue( n, "Max Dihedral > 175°", "critical_max_dihedral",'max_dihedral' )
- print_issue( n, "Combined (AR>100 & Q<0.3)", "combined" ,'aspect_ratio' )
- print_issue( n, "CRITICAL (AR>100 & Q<0.3 & MinDih<5°", "critical_combo", 'aspect_ratio' )
+ 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 ] )
+ percent = 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()
@@ -396,80 +396,81 @@ def print_issue(meshID, metricName, issueDataName, dataName ):
def printOverallQualityScore( self: Self ):
- # self.logger.info( f"\nOverall Quality Score Distribution:" )
- self.logger.info( f"Overall Quality Score Distribution:" )
- for n, _ in enumerate( self.meshes.items() ):
- qualityScore = self.overallQualityScore[ n ]
- self.logger.info(f" Mesh {n}: Excellent (>80): {qualityScore[ 'excellent' ]:.1f}% Good (60-80): {qualityScore[ 'good' ]:.1f}% Fair (30-60): {qualityScore[ 'fair' ]:.1f}% Poor (≤30): {qualityScore[ 'poor' ]:.1f}%")
+ msg = f"Overall Quality Score Distribution:\n"
+ msg += f" {f'Mesh n':10}{f'Excellent (>80)':20}{f'Good (60-80)':17}{f'Fair (30-60)':15}{f'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 )
def printExtremeOutlierAnalysis( self: Self ):
- #TODO : info in warning if bad ?
self.__loggerSection( "EXTREME OUTLIER ANALYSIS" )
# self.logger.warning( f"\n🚨 Elements with Aspect Ratio > 10,000:" )
- self.logger.warning( f"🚨 Elements with Aspect Ratio > 10,000:" )
- for n, _ in enumerate( self.meshes ):
- extreme_ar = self.issues[n][ 'extreme_aspect_ratio' ]
+ msg = f"Elements with Aspect Ratio > 10,000:\n"
+ msg2 = ""
+ w = False
+ w2 = False
+ for n, _ in enumerate( self.meshes, 1 ):
+ extremeAspectRatio = self.issues[n][ 'extremeAspectRatio' ]
data = self.analyzedMesh[n]
- ar1 = data[ 'aspect_ratio' ]
- self.logger.info(f" Mesh {n}: {np.sum(extreme_ar):,} elements ({np.sum(extreme_ar)/len(ar1)*100:.3f}%)")
-
- if np.sum(extreme_ar) > 0:
- vol1 = data[ "volume" ]
- min_dih1 = data[ "min_dihedral" ]
- sq1 = data[ "shape_quality" ]
- self.logger.info(f" Worst AR: {ar1[extreme_ar].max():.2e}")
- self.logger.info(f" Avg volume: {vol1[extreme_ar].mean():.2e}")
- self.logger.info(f" Min dihedral: {min_dih1[extreme_ar].min():.2f}° - {min_dih1[extreme_ar].mean():.2f}° (avg)")
- self.logger.info(f" Shape quality: {sq1[extreme_ar].min():.4f} - {sq1[extreme_ar].mean():.4f} (avg)")
-
- if np.sum(extreme_ar) > 10:
- # self.logger.warning(f"\n💡 Recommendation: Investigate/remove {np.sum(extreme_ar):,} extreme elements in Mesh {n}")
- self.logger.warning(f"💡 Recommendation: Investigate/remove {np.sum(extreme_ar):,} extreme elements in Mesh {n}")
- self.logger.warning(f" These are likely artifacts from mesh generation or geometry issues.")
+ 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
- # self.logger.warning( f"\n🚨 Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):")
- self.logger.warning( f"🚨 Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):")
- for n, _ in enumerate( self.meshes ):
+ degMsg = f"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 ][ "min_dihedral" ]
- self.logger.warning( f" Mesh {n}: {np.sum(degenerate):,} elements ({np.sum(degenerate)/len(data)*100:.3f}%)")
+ 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" )
def printComparisonSummary( self: Self):
self.__loggerSection( "COMPARISON SUMMARY" )
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
name = f"Mesh {n}"
if n == self.best:
name += " [BEST]"
elif n == self.worst:
name += " [LEAST GOOD]"
- ar1_med = np.median( self.validMetrics[n]["aspect_ratio"] )
- sq1_med = np.median( self.validMetrics[n]["shape_quality"] )
- vol1_med = np.median(self.validMetrics[n]["volume"] )
- min_edge1_med = np.median(self.validMetrics[n]["min_edge"] )
- max_edge1_med = np.median(self.validMetrics[n]["max_edge"] )
- edge_ratio1_med = np.median(self.validMetrics[n]["edge_ratio"] )
- min_dih1_med = np.median( self.validMetrics[n]["min_dihedral"] )
- max_dih1_med = np.median( self.validMetrics[n]["max_dihedral"] )
- score1_med = np.median( self.validMetrics[n]["quality_score"] )
+ msg = f"{name}\n"
- self.logger.info(f"{name}")
- self.logger.info(f" Tetrahedra: {self.analyzedMesh[n]['tet']:,}")
- self.logger.info(f" Median Aspect Ratio: {ar1_med:.2f}")
- self.logger.info(f" Median Shape Quality: {sq1_med:.4f}")
- self.logger.info(f" Median Volume: {vol1_med:.2e}")
- self.logger.info(f" Median Min Edge: {min_edge1_med:.2e}")
- self.logger.info(f" Median Max Edge: {max_edge1_med:.2e}")
- self.logger.info(f" Median Edge Ratio: {edge_ratio1_med:.2f}")
- self.logger.info(f" Median Min Dihedral: {min_dih1_med:.1f}°")
- self.logger.info(f" Median Max Dihedral: {max_dih1_med:.1f}°")
- self.logger.info(f" Median Quality Score: {score1_med:.1f}/100")
-
- self.logger.info("\n" + "="*80)
+ msg += f" Tetrahedra: {self.analyzedMesh[n]['tet']:,}\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"
@@ -477,66 +478,64 @@ def computeDeltasFromBest( self: Self ):
self.logger.info( f"Best mesh : Mesh {self.best}")
self.deltas = {}
- n_tets_best = self.analyzedMesh[self.best][ 'tet']
- ar_med_best = np.median( self.validMetrics[self.best]["aspect_ratio"] )
- sq_med_best = np.median(self.validMetrics[self.best]["shape_quality"])
- vol_med_best = np.median(self.validMetrics[self.best]["volume"])
- min_edge_med_best = np.median(self.validMetrics[self.best]["min_edge"])
- max_edge_med_best = np.median(self.validMetrics[self.best]["max_edge"])
- edge_ratio_med_best = np.median(self.validMetrics[self.best]["edge_ratio"])
-
- for n, _ in enumerate( self.meshes ):
- n_tets = self.analyzedMesh[n][ 'tet']
- ar_med = np.median( self.validMetrics[n]["aspect_ratio"] )
- sq_med = np.median(self.validMetrics[n]["shape_quality"])
- vol_med = np.median(self.validMetrics[n]["volume"])
- min_edge_med = np.median(self.validMetrics[n]["min_edge"])
- max_edge_med = np.median(self.validMetrics[n]["max_edge"])
- edge_ratio_med = np.median(self.validMetrics[n]["edge_ratio"])
-
-
- delta_tets = ((n_tets - n_tets_best) / n_tets_best * 100) if n_tets_best > 0 else 0
- delta_ar = ((ar_med - ar_med_best) / ar_med_best * 100) if ar_med_best > 0 else 0
- delta_sq = ((sq_med - sq_med_best) / sq_med_best * 100) if sq_med_best > 0 else 0
- delta_vol = ((vol_med - vol_med_best) / vol_med_best * 100) if vol_med_best > 0 else 0
- delta_min_edge = ((min_edge_med - min_edge_med_best) / min_edge_med_best * 100) if min_edge_med_best > 0 else 0
- delta_max_edge = ((max_edge_med - max_edge_med_best) / max_edge_med_best * 100) if max_edge_med_best > 0 else 0
- delta_edge_ratio = ((edge_ratio_med - edge_ratio_med_best) / edge_ratio_med_best * 100) if edge_ratio_med_best > 0 else 0
- self.deltas[ n ] = { "tetrahedra": delta_tets, "aspect_ratio": delta_ar, "shape_quality": delta_sq, "volume": delta_vol, "min_edge": delta_min_edge, "max_edge": delta_max_edge, "edge_ratio": delta_edge_ratio }
-
-
- dtets = [ f"{self.deltas[ n ][ 'tetrahedra' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- dar = [ f"{self.deltas[ n ][ 'aspect_ratio' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- dsq = [ f"{self.deltas[ n ][ 'shape_quality' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- dvol = [ f"{self.deltas[ n ][ 'volume' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- d_min_edge = [ f"{self.deltas[ n ][ 'min_edge' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- d_max_edge = [ f"{self.deltas[ n ][ 'max_edge' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- d_edge_ratio = [ f"{self.deltas[ n ][ 'edge_ratio' ]:+15,.1f}%" for n, _ in self.sorted[1:] ]
- names = [ f"{f'Mesh {n}':16}" for n, _ in self.sorted[1:]]
-
- self.logger.info(f"Changes vs Mesh {self.best} [BEST]:")
- self.logger.info(f"{' Mesh:':25}{('').join( names )}")
- self.logger.info(f"{' Tetrahedra:':25} {('').join(dtets)}%")
- self.logger.info(f"{' Aspect Ratio:':25}{('').join( dar)}%")
- self.logger.info(f"{' Shape Quality:':25}{('').join( dsq)}%")
- self.logger.info(f"{' Volume:':25}{('').join( dvol)}%")
- self.logger.info(f"{' Min Edge Length:':25}{('').join( d_min_edge)}%")
- self.logger.info(f"{' Max Edge Length:':25}{('').join( d_max_edge)}%")
- self.logger.info(f"{' Edge Length Ratio:':25}{('').join( d_edge_ratio)}%")
-
+ ntetsBest = self.analyzedMesh[self.best][ 'tet']
+ aspectRatioBest = self.medians[self.best]["aspectRatio"]
+ shapeQualityBest = self.medians[self.best]["shapeQuality"]
+ volumeBest = self.medians[self.best]["volume"]
+ minEdgeBest = self.medians[self.best]["minEdge"]
+ maxEdgeBest = self.medians[self.best]["maxEdge"]
+ edgeRatioBest = self.medians[self.best]["edgeRatio"]
+
+ for n, _ in enumerate( self.meshes, 1 ):
+ self.deltas[n] = {}
+ ntets = self.analyzedMesh[n][ 'tet']
+
+ 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"])
+
+ self.deltas[n]["tetrahedra"] = ( ( self.analyzedMesh[n]["tet"] - self.analyzedMesh[self.best]["tet"]) / self.analyzedMesh[self.best]["tet"] * 100 ) if self.analyzedMesh[self.best]["tet"] > 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
+
+
+ dtets = [ f"{self.deltas[ n ][ 'tetrahedra' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ dar = [ f"{self.deltas[ n ][ 'aspectRatio' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ dsq = [ f"{self.deltas[ n ][ 'shapeQuality' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ dvol = [ f"{self.deltas[ n ][ 'volume' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ d_min_edge = [ f"{self.deltas[ n ][ 'minEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ d_maxEdge = [ f"{self.deltas[ n ][ 'maxEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ d_edge_ratio = [ 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(dtets)}\n" +
+ f"{' Aspect Ratio:':<20}{('').join( dar)}\n" +
+ f"{' Shape Quality:':<20}{('').join( dsq)}\n" +
+ f"{' Volume:':<20}{('').join( dvol)}\n" +
+ f"{' Min Edge Length:':<20}{('').join( d_min_edge)}\n" +
+ f"{' Max Edge Length:':<20}{('').join( d_maxEdge)}\n" +
+ f"{' Edge Length Ratio:':<20}{('').join( d_edge_ratio)}\n"
+ )
def createComparisonDashboard( self: Self ):
- lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes ) ]
+ lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes, 1 ) ]
# Determine smart plot limits
ar_99 = []
- for n, _ in enumerate( self.meshes ):
- ar_99.append( np.percentile( self.validMetrics[n]["aspect_ratio"], 99 ) )
+ for n, _ in enumerate( self.meshes, 1 ):
+ ar_99.append( np.percentile( self.validMetrics[n]["aspectRatio"], 99 ) )
ar_99_max = np.max( np.array( ar_99 ) )
- # ar_99_1 = np.percentile(ar1, 99)
+ # ar_99_1 = np.percentile(aspectRatio, 99)
# # ar_99_2 = np.percentile(ar2, 99)
# # ar_99_max = max(ar_99_1, ar_99_2)
# ar_99_max = ar_99_1
@@ -552,7 +551,6 @@ def createComparisonDashboard( self: Self ):
# print(f"Using AR plot limit: {ar_plot_limit} (99th percentiles: M1={ar_99_1:.1f}")
# Set style
- sns.set_style("whitegrid")
plt.rcParams['figure.facecolor'] = 'white'
plt.rcParams.update({
'font.size': 9,
@@ -575,24 +573,14 @@ def createComparisonDashboard( self: Self ):
top=0.82, bottom=0.05, hspace=0.35, wspace=0.30)
# Title
- # mesh1_name = filename1.split('/')[-1]
- # mesh2_name = filename2.split('/')[-1]
suptitle = 'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n'
- for n, _ in enumerate( self.meshes ):
- # suptitle += f'Mesh {n}: {n_tets1:,} tets\t'
- suptitle += f'Mesh {n}: tets\t'
+ for n, _ in enumerate( self.meshes, 1 ):
+ # suptitle += f'Mesh {n}: {ntets:,} tets\t'
+ suptitle += f'Mesh {n}: {self.analyzedMesh[n]["tet"]} tets - '
fig.suptitle( suptitle,
fontsize=16, fontweight='bold', y=0.99)
- # fig.suptitle(f'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n' +
- # f'Mesh 1: {mesh1_name} ({n_tets1:,} tets)',
- # # f'Mesh 1: {mesh1_name} ({n_tets1:,} tets) vs Mesh 2: {mesh2_name} ({n_tets2:,} tets)',
- # fontsize=16, fontweight='bold', y=0.99)
# Color scheme
- # color = matplotlib.colormaps[ 'tab20' ]
- # def color[n]:
- # nn = np.linspace(0,20)
- # return matplotlib.colormaps[ 'tab20' ][nn]
color = matplotlib.pyplot.cm.tab10( np.arange(20))
# ==================== ROW 1: EXECUTIVE SUMMARY ====================
@@ -600,11 +588,11 @@ def createComparisonDashboard( self: Self ):
# 1. Overall Quality Score Distribution
ax1 = fig.add_subplot(gs_row1[0, 0])
bins = np.linspace(0, 100, 40)
- for n, _ in enumerate( self.meshes ):
- score1 = self.validMetrics[n][ 'quality_score' ]
- ax1.hist(score1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
- ax1.axvline(np.median(score1), color=color[n], linestyle='--', linewidth=2.5, alpha=0.9)
+ 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)
@@ -612,20 +600,15 @@ def createComparisonDashboard( self: Self ):
ax1.axvspan(60, 80, alpha=0.15, color='lightgreen', zorder=0)
ax1.axvspan(80, 100, alpha=0.15, color='darkgreen', zorder=0)
- # Add median lines
- # ax1.axvline(np.median(score1), color=color1, linestyle='--', linewidth=2.5, alpha=0.9)
- # ax1.axvline(np.median(score2), color=color2, linestyle='--', linewidth=2.5, alpha=0.9)
-
# Add summary text #### ONLY BEST AND WORST MESH?
- # TODO
- # ax1.text(0.98, 0.98,
- # f'Median Score:\nM1: {np.median(score1):.1f}\n\n' +
- # f'Excellent (>80):\nM1: {excellent1:.1f}%',
- # # f'Median Score:\nM1: {np.median(score1):.1f}\nM2: {np.median(score2):.1f}\n\n' +
- # # f'Excellent (>80):\nM1: {excellent1:.1f}%\nM2: {excellent2:.1f}%',
- # transform=ax1.transAxes, va='top', ha='right',
- # bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.9),
- # fontsize=9, fontweight='bold')
+ ax1.text(0.98, 0.98,
+ 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=dict(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')
@@ -644,17 +627,17 @@ def createComparisonDashboard( self: Self ):
ax2 = fig.add_subplot(gs_row1[0, 1])
# Create sample for plotting
- for n, _ in enumerate( self.meshes ):
- ar1 = self.validMetrics[n]["aspect_ratio"]
- sq1 = self.validMetrics[n]["shape_quality"]
- self.setSampleForPlot( ar1, n )
+ 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]
- mask1_plot = ar1[idx] < ar_plot_limit
+ mask1_plot = aspectRatio[idx] < ar_plot_limit
- ax2.scatter(ar1[idx][mask1_plot], sq1[idx][mask1_plot], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ ax2.scatter(aspectRatio[idx][mask1_plot], shapeQuality[idx][mask1_plot], 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,
@@ -670,22 +653,22 @@ def createComparisonDashboard( self: Self ):
ax2.add_patch(problem_zone)
# Count ALL elements
- problem1_all = np.sum((ar1 > 100) & (sq1 < 0.3))
- extreme1 = np.sum(ar1 > ar_plot_limit)
+ problem1_all = np.sum((aspectRatio > 100) & (shapeQuality < 0.3))
+ extreme1 = np.sum(aspectRatio > ar_plot_limit)
# Problem annotation
#TODO
- # ax2.text(0.98, 0.02,
- # f'PROBLEM ELEMENTS\n(AR>100 & Q<0.3):\n\n' +
- # f'M1: {problem1_all:,}\n\n',
- # # f'Change: {((problem2_all-problem1_all)/max(problem1_all,1)*100):+.1f}%',
- # # f'M1: {problem1_all:,}\nM2: {problem2_all:,}\n\n' +
- # transform=ax2.transAxes, va='bottom', ha='right',
- # bbox=dict(boxstyle='round', facecolor='#ffcccc', alpha=0.95,
- # edgecolor='darkred', linewidth=2),
- # fontsize=9, fontweight='bold')
-
- # # Outlier warning
+ 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,
+ f'PROBLEM ELEMENTS\n(AR>100 & Q<0.3):\n\n' +
+ annotateIssues,
+ transform=ax2.transAxes, va='bottom', ha='right',
+ bbox=dict(boxstyle='round', facecolor='#ffcccc', alpha=0.95,
+ edgecolor='darkred', linewidth=2),
+ fontsize=8, fontweight='bold')
+
+ # Outlier warning
# if extreme1 + extreme2 > 100:
# ax2.text(0.02, 0.98,
# f'⚠️ AR > {ar_plot_limit}\n(not shown):\n' +
@@ -708,71 +691,72 @@ def createComparisonDashboard( self: Self ):
ax3 = fig.add_subplot(gs_row1[0, 2])
ax3.axis('off')
- summary_stats = []
- summary_stats.append(['CRITICAL ISSUE', 'WORST', 'BEST', 'Change'])
- summary_stats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
+ summaryStats = []
+ summaryStats.append(['CRITICAL ISSUE', 'WORST', 'BEST', 'Change'])
+ summaryStats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
- critical_combo1 = self.issues[ self.best ][ "critical_combo" ]
- critical_combo2 = self.issues[ self.worst ][ "critical_combo" ]
- ar1 = self.validMetrics[self.best][ "aspect_ratio" ]
- ar2 = self.validMetrics[self.worst][ "aspect_ratio" ]
+ criticalCombo = self.issues[ self.best ][ "criticalCombo" ]
+ critical_combo2 = self.issues[ self.worst ][ "criticalCombo" ]
- high_ar1 = self.issues[ self.best ][ "high_aspect_ratio" ]
- high_ar2 = self.issues[ self.worst ][ "high_aspect_ratio" ]
+ aspectRatio = self.validMetrics[self.best][ "aspectRatio" ]
+ ar2 = self.validMetrics[self.worst][ "aspectRatio" ]
- low_sq1 = self.issues[self.best ][ "low_shape_quality" ]
- low_sq2 = self.issues[self.worst][ "low_shape_quality" ]
+ highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ]
+ high_ar2 = self.issues[ self.worst ][ "highAspectRatio" ]
- critical_dih1 = self.issues[self.best]["critical_dihedral"]
- critical_dih2 = self.issues[self.worst]["critical_dihedral"]
+ lowShapeQuality = self.issues[self.best ][ "lowShapeQuality" ]
+ low_sq2 = self.issues[self.worst][ "lowShapeQuality" ]
- critical_max_dih1 = self.issues[self.best][ "critical_max_dihedral" ]
- critical_max_dih2 = self.issues[self.worst][ "critical_max_dihedral" ]
+ criticalMinDihedral = self.issues[self.best]["criticalMinDihedral"]
+ critical_dih2 = self.issues[self.worst]["criticalMinDihedral"]
- high_edge_ratio1 = self.issues[self.best][ "high_edge_ratio" ]
- high_edge_ratio2 = self.issues[self.worst][ "high_edge_ratio" ]
+ criticalMaxDihedral = self.issues[self.best][ "criticalMaxDihedral" ]
+ critical_max_dih2 = self.issues[self.worst][ "criticalMaxDihedral" ]
+ highEdgeRatio = self.issues[self.best][ "highEdgeRatio" ]
+ highEdgeRatio2 = self.issues[self.worst][ "highEdgeRatio" ]
- summary_stats.append(['CRITICAL Combo', f'{critical_combo1:,}', f'{critical_combo2:,}',
- f'{((critical_combo2-critical_combo1)/max(critical_combo1,1)*100):+.1f}%' if critical_combo1 > 0 else 'N/A'])
- summary_stats.append(['(AR>100 & Q<0.3', f'({critical_combo1/len(ar1)*100:.2f}%)',
+
+ summaryStats.append(['CRITICAL Combo', f'{criticalCombo:,}', f'{critical_combo2:,}',
+ f'{((critical_combo2-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'({critical_combo2/len(ar2)*100:.2f}%)', ''])
- summary_stats.append([' & MinDih<5°)', '', '', ''])
+ summaryStats.append([' & MinDih<5°)', '', '', ''])
- summary_stats.append(['', '', '', ''])
- summary_stats.append(['AR > 100', f'{high_ar1:,}', f'{high_ar2:,}',
- f'{((high_ar2-high_ar1)/max(high_ar1,1)*100):+.1f}%'])
+ summaryStats.append(['', '', '', ''])
+ summaryStats.append(['AR > 100', f'{highAspectRatio:,}', f'{high_ar2:,}',
+ f'{((high_ar2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%'])
- summary_stats.append(['Quality < 0.3', f'{low_sq1:,}', f'{low_sq2:,}',
- f'{((low_sq2-low_sq1)/max(low_sq1,1)*100):+.1f}%'])
+ summaryStats.append(['Quality < 0.3', f'{lowShapeQuality:,}', f'{low_sq2:,}',
+ f'{((low_sq2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%'])
- summary_stats.append(['MinDih < 5°', f'{critical_dih1:,}', f'{critical_dih2:,}',
- f'{((critical_dih2-critical_dih1)/max(critical_dih1,1)*100):+.1f}%' if critical_dih1 > 0 else 'N/A'])
+ summaryStats.append(['MinDih < 5°', f'{criticalMinDihedral:,}', f'{critical_dih2:,}',
+ f'{((critical_dih2-criticalMinDihedral)/max(criticalMinDihedral,1)*100):+.1f}%' if criticalMinDihedral > 0 else 'N/A'])
- summary_stats.append(['MaxDih > 175°', f'{critical_max_dih1:,}', f'{critical_max_dih2:,}',
- f'{((critical_max_dih2-critical_max_dih1)/max(critical_max_dih1,1)*100):+.1f}%' if critical_max_dih1 > 0 else 'N/A'])
+ summaryStats.append(['MaxDih > 175°', f'{criticalMaxDihedral:,}', f'{critical_max_dih2:,}',
+ f'{((critical_max_dih2-criticalMaxDihedral)/max(criticalMaxDihedral,1)*100):+.1f}%' if criticalMaxDihedral > 0 else 'N/A'])
- summary_stats.append(['Edge Ratio > 10', f'{high_edge_ratio1:,}', f'{high_edge_ratio2:,}',
- f'{((high_edge_ratio2-high_edge_ratio1)/max(high_edge_ratio1,1)*100):+.1f}%'])
+ summaryStats.append(['Edge Ratio > 10', f'{highEdgeRatio:,}', f'{highEdgeRatio2:,}',
+ f'{((highEdgeRatio2-highEdgeRatio)/max(highEdgeRatio,1)*100):+.1f}%'])
- summary_stats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
- summary_stats.append(['Quality Grade', '', '', ''])
- excellent1 = self.overallQualityScore[self.best]["excellent"]
- excellent2 = self.overallQualityScore[self.worst]["excellent"]
- good1 = self.overallQualityScore[self.best]["good"]
- good2 = self.overallQualityScore[self.worst]["good"]
- poor1 = self.overallQualityScore[self.best]["poor"]
- poor2 = self.overallQualityScore[self.worst]["poor"]
+ 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"]
- summary_stats.append([' Excellent (>80)', f'{excellent1:.1f}%', f'{excellent2:.1f}%',
- f'{excellent2-excellent1:+.1f}%'])
- summary_stats.append([' Good (60-80)', f'{good1:.1f}%', f'{good2:.1f}%',
- f'{good2-good1:+.1f}%'])
- summary_stats.append([' Poor (≤30)', f'{poor1:.1f}%', f'{poor2:.1f}%',
- f'{poor2-poor1:+.1f}%'])
+ 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=summary_stats, cellLoc='left',
+ table = ax3.table(cellText=summaryStats, cellLoc='left',
bbox=[0, 0, 1, 1], edges='open')
table.auto_set_font_size(False)
table.set_fontsize(8)
@@ -789,8 +773,8 @@ def createComparisonDashboard( self: Self ):
# Color code changes
for row in [2, 6, 7, 8, 9, 10, 13, 14, 15]:
- if row < len(summary_stats):
- change_text = summary_stats[row][3]
+ if row < len(summaryStats):
+ change_text = summaryStats[row][3]
if '%' in change_text and change_text != 'N/A':
try:
val = float(change_text.replace('%', '').replace('+', ''))
@@ -815,10 +799,10 @@ def createComparisonDashboard( self: Self ):
# 4. Shape Quality Histogram
ax4 = fig.add_subplot(gs_main[0, 0])
bins = np.linspace(0, 1, 40)
- for n, _ in enumerate( self.meshes ):
- sq1 = self.validMetrics[n][ "shape_quality" ]
- ax4.hist(sq1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ 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')
@@ -827,14 +811,14 @@ def createComparisonDashboard( self: Self ):
# 5. Aspect Ratio Histogram
ax5 = fig.add_subplot(gs_main[0, 1])
- # bins = np.logspace(0, np.log10(min(ar_plot_limit, ar1.max(), ar2.max())), 40)
- ar_max = np.array( [ self.validMetrics[n]["aspect_ratio"].max() for n, _ in enumerate( self.meshes )] )
+ # bins = np.logspace(0, np.log10(min(ar_plot_limit, aspectRatio.max(), ar2.max())), 40)
+ ar_max = np.array( [ self.validMetrics[n]["aspectRatio"].max() for n, _ in enumerate( self.meshes, 1 )] )
bins = np.logspace(0, np.log10(min(ar_plot_limit, ar_max.max())), 40)
- for n, _ in enumerate( self.meshes ):
- ar1 = self.validMetrics[n]['aspect_ratio']
- ax5.hist(ar1[ar1 < ar_plot_limit], bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ for n, _ in enumerate( self.meshes, 1 ):
+ aspectRatio = self.validMetrics[n]['aspectRatio']
+ ax5.hist(aspectRatio[aspectRatio < ar_plot_limit], 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')
@@ -845,10 +829,10 @@ def createComparisonDashboard( self: Self ):
# 6. Min Dihedral Histogram
ax6 = fig.add_subplot(gs_main[0, 2])
bins = np.linspace(0, 90, 40)
- for n, _ in enumerate( self.meshes ):
- min_dih1 = self.validMetrics[ n ]["min_dihedral"]
- ax6.hist(min_dih1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ 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')
@@ -859,10 +843,10 @@ def createComparisonDashboard( self: Self ):
# 7. Edge Ratio Histogram
ax7 = fig.add_subplot(gs_main[0, 3])
bins = np.logspace(0, 3, 40)
- for n, _ in enumerate( self.meshes ):
- edge_ratio1 = self.validMetrics[ n ][ "edge_ratio" ]
- ax7.hist(edge_ratio1[edge_ratio1 < 1000], bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ 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')
@@ -873,15 +857,15 @@ def createComparisonDashboard( self: Self ):
# 8. Volume Histogram
ax8 = fig.add_subplot(gs_main[0, 4])
- vol_min = np.array( [ self.validMetrics[n]["volume"].min() for n, _ in enumerate( self.meshes )] ).min()
- vol_max = np.array( [ self.validMetrics[n]["volume"].max() for n, _ in enumerate( self.meshes )] ).max()
+ vol_min = np.array( [ self.validMetrics[n]["volume"].min() for n, _ in enumerate( self.meshes, 1 )] ).min()
+ vol_max = np.array( [ self.validMetrics[n]["volume"].max() for n, _ in enumerate( self.meshes, 1 )] ).max()
bins = np.logspace(np.log10(vol_min), np.log10(vol_max), 40)
- for n, _ in enumerate( self.meshes ):
- vol1 = self.validMetrics[n][ "volume" ]
- ax8.hist(vol1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ 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')
@@ -893,7 +877,7 @@ def createComparisonDashboard( self: Self ):
# 9. Shape Quality Box Plot
ax9 = fig.add_subplot(gs_main[1, 0])
- sq = [ self.validMetrics[n][ "shape_quality" ] for n, _ in enumerate( self.meshes ) ]
+ sq = [ self.validMetrics[n][ "shapeQuality" ] for n, _ in enumerate( self.meshes, 1 ) ]
bp1 = ax9.boxplot( sq, labels=lbl,
patch_artist=True, showfliers=False)
ax9.set_ylabel('Shape Quality', fontweight='bold')
@@ -902,7 +886,7 @@ def createComparisonDashboard( self: Self ):
# 10. Aspect Ratio Box Plot
ax10 = fig.add_subplot(gs_main[1, 1])
- ar = [ self.validMetrics[n][ "aspect_ratio" ] for n, _ in enumerate( self.meshes ) ]
+ ar = [ self.validMetrics[n][ "aspectRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
bp2 = ax10.boxplot( ar, labels=lbl,
patch_artist=True, showfliers=False)
ax10.set_yscale('log')
@@ -912,8 +896,8 @@ def createComparisonDashboard( self: Self ):
# 11. Min Dihedral Box Plot
ax11 = fig.add_subplot(gs_main[1, 2])
- min_dih1 = [ self.validMetrics[n][ "min_dihedral" ] for n, _ in enumerate( self.meshes ) ]
- bp3 = ax11.boxplot( min_dih1, labels=lbl,
+ minDihedral = [ self.validMetrics[n][ "minDihedral" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp3 = ax11.boxplot( minDihedral, labels=lbl,
patch_artist=True, showfliers=False)
ax11.set_ylabel('Min Dihedral Angle (degrees)', fontweight='bold')
ax11.set_title('Min Dihedral Comparison', fontweight='bold')
@@ -921,7 +905,7 @@ def createComparisonDashboard( self: Self ):
# 12. Edge Ratio Box Plot
ax12 = fig.add_subplot(gs_main[1, 3])
- edge_ratio = [ self.validMetrics[n][ "edge_ratio" ] for n, _ in enumerate( self.meshes ) ]
+ edge_ratio = [ self.validMetrics[n][ "edgeRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
bp4 = ax12.boxplot(edge_ratio, labels=lbl,
patch_artist=True, showfliers=False)
ax12.set_yscale('log')
@@ -931,7 +915,7 @@ def createComparisonDashboard( self: Self ):
# 13. Volume Box Plot
ax13 = fig.add_subplot(gs_main[1, 4])
- vol = [ self.validMetrics[n][ "volume" ] for n, _ in enumerate( self.meshes ) ]
+ vol = [ self.validMetrics[n][ "volume" ] for n, _ in enumerate( self.meshes, 1 ) ]
bp5 = ax13.boxplot( vol, labels=lbl,
patch_artist=True, showfliers=False)
ax13.set_yscale('log')
@@ -939,17 +923,17 @@ def createComparisonDashboard( self: Self ):
ax13.set_title('Volume Comparison', fontweight='bold')
ax13.grid(True, alpha=0.3, axis='y')
- for n, _ in enumerate( self.meshes ):
- bp1['boxes'][n].set_facecolor(color[n])
- bp1['medians'][n].set_color("black")
- bp2['boxes'][n].set_facecolor(color[n])
- bp2['medians'][n].set_color("black")
- bp3['boxes'][n].set_facecolor(color[n])
- bp3['medians'][n].set_color("black")
- bp4['boxes'][n].set_facecolor(color[n])
- bp4['medians'][n].set_color("black")
- bp5['boxes'][n].set_facecolor(color[n])
- bp5['medians'][n].set_color("black")
+ 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) ====================
@@ -957,13 +941,13 @@ def createComparisonDashboard( self: Self ):
# 14. Shape Quality vs Aspect Ratio (duplicate for detail)
ax14 = fig.add_subplot(gs_main[2, 0])
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[n]
- ar1 = self.validMetrics[n][ 'aspect_ratio']
- sq1 = self.validMetrics[n][ 'shape_quality']
- mask1 = ar1[idx] < ar_plot_limit
- ax14.scatter(ar1[idx][mask1], sq1[idx][mask1], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ aspectRatio = self.validMetrics[n][ 'aspectRatio']
+ shapeQuality = self.validMetrics[n][ 'shapeQuality']
+ mask1 = aspectRatio[idx] < ar_plot_limit
+ 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')
@@ -975,13 +959,13 @@ def createComparisonDashboard( self: Self ):
# 15. Aspect Ratio vs Flatness
ax15 = fig.add_subplot(gs_main[2, 1])
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[n]
- ar1 = self.validMetrics[n]["aspect_ratio"]
- fr1 = self.validMetrics[n][ 'flatness_ratio' ]
- mask1 = ar1[idx] < ar_plot_limit
- ax15.scatter(ar1[idx][mask1], fr1[idx][mask1], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ aspectRatio = self.validMetrics[n]["aspectRatio"]
+ flatnessRatio = self.validMetrics[n][ 'flatnessRatio' ]
+ mask1 = aspectRatio[idx] < ar_plot_limit
+ 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')
@@ -993,13 +977,13 @@ def createComparisonDashboard( self: Self ):
# 16. Volume vs Aspect Ratio
ax16 = fig.add_subplot(gs_main[2, 2])
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[n]
- ar1 = self.validMetrics[n]["aspect_ratio"]
- vol1 = self.validMetrics[n][ 'volume' ]
- mask1 = ar1[idx] < ar_plot_limit
- ax16.scatter(vol1[idx][mask1], ar1[idx][mask1], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ aspectRatio = self.validMetrics[n]["aspectRatio"]
+ volume = self.validMetrics[n][ 'volume' ]
+ mask1 = aspectRatio[idx] < ar_plot_limit
+ 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')
@@ -1011,12 +995,12 @@ def createComparisonDashboard( self: Self ):
# 17. Volume vs Shape Quality
ax17 = fig.add_subplot(gs_main[2, 3])
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[n]
- vol1 = self.validMetrics[n][ 'volume' ]
- sq1 = self.validMetrics[n][ 'shape_quality']
- ax17.scatter(vol1[idx], sq1[idx], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ 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')
@@ -1026,12 +1010,12 @@ def createComparisonDashboard( self: Self ):
# 18. Edge Ratio vs Volume
ax18 = fig.add_subplot(gs_main[2, 4])
- for n, _ in enumerate( self.meshes ):
+ for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[n]
- vol1 = self.validMetrics[n][ 'volume' ]
- edge_ratio = self.validMetrics[n][ 'edge_ratio' ]
- ax18.scatter(vol1[idx], edge_ratio[idx], alpha=0.4, s=5,
- color=color[n], label=f'Mesh {n}', edgecolors='none')
+ volume = self.validMetrics[n][ 'volume' ]
+ edge_ratio = self.validMetrics[n][ 'edgeRatio' ]
+ ax18.scatter(volume[idx], edge_ratio[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')
@@ -1045,19 +1029,19 @@ def createComparisonDashboard( self: Self ):
# 19. Min Edge Length Histogram
ax19 = fig.add_subplot(gs_main[3, 0])
- edge_min = np.array( [ self.validMetrics[n]["min_edge"].min() for n,_ in enumerate( self.meshes ) ] ).min()
- edge_max_min = np.array( [ self.validMetrics[n]["min_edge"].max() for n,_ in enumerate( self.meshes ) ] ).min()
+ edge_min = np.array( [ self.validMetrics[n]["minEdge"].min() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
+ edge_max_min = np.array( [ self.validMetrics[n]["minEdge"].max() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
- for n, _ in enumerate( self.meshes ):
- # edge_min = min(min_edge1.min(), min_edge2.min())
- # edge_max_min = max(min_edge1.max(), min_edge2.max())
+ for n, _ in enumerate( self.meshes, 1 ):
+ # edge_min = min(minEdge.min(), min_edge2.min())
+ # edge_max_min = max(minEdge.max(), min_edge2.max())
# bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
- min_edge = self.validMetrics[n]['min_edge']
- ax19.hist(min_edge, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
- ax19.axvline(np.median(min_edge), color=color[n], linestyle=':', linewidth=2, alpha=0.8)
+ 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')
@@ -1068,17 +1052,17 @@ def createComparisonDashboard( self: Self ):
# 20. Max Edge Length Histogram
ax20 = fig.add_subplot(gs_main[3, 1])
- edge_max = np.array( [ self.validMetrics[n]["max_edge"].max() for n,_ in enumerate( self.meshes ) ] ).max()
- edge_min_max = np.array( [ self.validMetrics[n]["max_edge"].min() for n,_ in enumerate( self.meshes ) ] ).min()
+ edge_max = np.array( [ self.validMetrics[n]["maxEdge"].max() for n,_ in enumerate( self.meshes, 1 ) ] ).max()
+ edge_min_max = np.array( [ self.validMetrics[n]["maxEdge"].min() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
- # edge_max = max(max_edge1.max(), max_edge2.max())
- # edge_min_max = min(max_edge1.min(), max_edge2.min())
+ # edge_max = max(maxEdge.max(), max_edge2.max())
+ # edge_min_max = min(maxEdge.min(), max_edge2.min())
bins = np.logspace(np.log10(edge_min_max), np.log10(edge_max), 40)
- for n, _ in enumerate( self.meshes ):
- max_edge1 = self.validMetrics[n]["max_edge"]
- ax20.hist(max_edge1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
- ax20.axvline(np.median(max_edge1), color=color[n], linestyle=':', linewidth=2, alpha=0.8)
+ 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')
@@ -1089,10 +1073,10 @@ def createComparisonDashboard( self: Self ):
# 21. Max Dihedral Histogram
ax21 = fig.add_subplot(gs_main[3, 2])
bins = np.linspace(90, 180, 40)
- for n, _ in enumerate( self.meshes ):
- max_dih1 = self.validMetrics[n][ "max_dihedral" ]
- ax21.hist(max_dih1, bins=bins, alpha=0.6, label=f'Mesh {n}',
- color=color[n], edgecolor='black', linewidth=0.5)
+ 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')
@@ -1104,15 +1088,16 @@ def createComparisonDashboard( self: Self ):
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]["min_dihedral"] for n, _ in enumerate( self.meshes )] + [ self.validMetrics[n]["max_dihedral"] for n, _ in enumerate( self.meshes )]
- lbl_boxplot = [ f'M{n}Min' for n, _ in enumerate( self.meshes )] + [ f'M{n}Max' for n, _ in enumerate( self.meshes )]
- boxplot_color = [ n for n, _ in enumerate( self.meshes ) ] + [ n for n, _ in enumerate( self.meshes ) ]
+ 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, 1 ) ] + [ n for n, _ in enumerate( self.meshes, ) ]
bp_dih = ax22.boxplot(dih,
positions=positions,
labels=lbl_boxplot,
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)
@@ -1123,11 +1108,11 @@ def createComparisonDashboard( self: Self ):
# 23. Shape Quality CDF
ax23 = fig.add_subplot(gs_main[3, 4])
- for n, _ in enumerate( self.meshes ):
- sq1 = self.validMetrics[n][ "shape_quality"]
- sorted_sq1 = np.sort(sq1)
+ 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], linewidth=2, label=f'Mesh {n}')
+ 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)
@@ -1139,7 +1124,7 @@ def createComparisonDashboard( self: Self ):
ax23.grid(True, alpha=0.3)
# Save figure
- output_png = 'mesh_comparison.png'
+ output_png = '/data/pau901/SIM_CS/04_WORKSPACE/USERS/PalomaMartinez/geosPythonPackages/TESST/mesh_comparison.png'
print(f"\nSaving dashboard to: {output_png}")
plt.savefig(output_png, dpi=300, bbox_inches='tight', facecolor='white')
print(f"Dashboard saved successfully!")
@@ -1148,7 +1133,6 @@ def createComparisonDashboard( self: Self ):
def __loggerSection( self: Self, sectionName: str ):
- # self.logger.info("\n" + "="*80)
self.logger.info("="*80)
self.logger.info( sectionName )
self.logger.info("="*80)
@@ -1156,54 +1140,57 @@ def __loggerSection( self: Self, sectionName: str ):
def __orderMeshes( self: Self ):
"""Proposition of ordering as fonction of median quality score"""
- self.logger.info( "Ordering the meshes" )
- median_score = { n : np.median( self.validMetrics[n]["quality_score"] ) for n, _ in enumerate (self.meshes) }
+ self.__loggerSection( "ORDERING MESHES (from median quality score)")
+ median_score = { n : np.median( self.validMetrics[n]["qualityScore"] ) for n, _ in enumerate (self.meshes, 1) }
sorted_meshes = sorted( median_score.items(), key=lambda x:x[1], reverse = True )
self.sorted = sorted_meshes
self.best = sorted_meshes[ 0 ][0]
self.worst = sorted_meshes[ -1 ][0]
- self.logger.info( f"Best Mesh: Mesh {self.best} vs worst Mesh [{self.worst}]")
-
self.logger.info( f"Mesh order from median quality score:" )
top = [ f"Mesh {m[0]} ({m[1]:.2f})" for m in sorted_meshes ]
toprint = (" > ").join( top )
- self.logger.info( " [+] " + toprint + " [-]" )
+ self.logger.info( " [+] " + toprint + " [-]\n" )
+
def compareIssuesFromBest( self: Self ):
- high_ar1 = self.issues[ self.best ][ "high_aspect_ratio" ]
- critical_dih1 = self.issues[ self.best ]["critical_dihedral"]
- low_sq1 = self.issues[ self.best ]["low_shape_quality"]
- critical_combo1 = self.issues[ self.best ]["critical_combo"]
+ highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ]
+ criticalMinDihedral = self.issues[ self.best ]["criticalMinDihedral"]
+ lowShapeQuality = self.issues[ self.best ]["lowShapeQuality"]
+ criticalCombo = self.issues[ self.best ]["criticalCombo"]
def getPercentChangeFromBest( data, ref ):
return (data - ref) / max( ref, 1)*100
- self.logger.info (f"Change from BEST [Mesh {self.best}]" )
- high_ar = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'high_aspect_ratio' ], high_ar1 ):+15,.1f}%" for n, _ in self.sorted ]
- low_sq = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'low_shape_quality' ], low_sq1 ):+15,.1f}%" for n, _ in self.sorted ]
- critical_dih = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'critical_dihedral' ], critical_dih1 ):+15,.1f}%" if critical_dih1 > 0 else f"{'N/A':16}" for n, _ in self.sorted]
- critical_combo = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'critical_combo' ], critical_combo1 ):+15,.1f}%" if critical_combo1 > 0 else f"{'N/A':16}" for n, _ in self.sorted]
+ msg = f"Change from BEST [Mesh {self.best}]\n"
+ msg += f" {f'Mesh':20}"+ ("").join([f"{f'Mesh {n}':>16}" for n, _ in enumerate( self.meshes, 1 )]) + "\n"
+ highAspectRatio = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'highAspectRatio' ], highAspectRatio ):>+15,.1f}%" if n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ]
+ lowShapeQuality = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'lowShapeQuality' ], lowShapeQuality ):>+15,.1f}%" if n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ]
+ criticalMinDihedral = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'criticalMinDihedral' ], criticalMinDihedral ):>+15,.1f}%" if criticalMinDihedral > 0 and n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 )]
+ criticalCombo = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'criticalCombo' ], criticalCombo ):>+15,.1f}%" if criticalCombo > 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( f"{' AR > 100:':25}{('').join( high_ar )}" )
- self.logger.info( f"{' Quality < 0.3:':25}{('').join( low_sq )}" )
- self.logger.info( f"{' MinDih < 5°:':25}{('').join( critical_dih )}" )
- self.logger.info( f"{' CRITICAL combo:':25}{('').join( critical_combo )}" )
+ self.logger.info( msg )
def setSampleForPlot( self: Self, data, n ):
n_sample = min(10000, len( data ))
self.sample[n] = np.random.choice(len(data), n_sample, replace=False)
+
# Combined quality score
def compute_quality_score(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
"""Compute combined quality score (0-100)."""
- ar_norm = np.clip(1.0 / (aspectRatio / 1.73), 0, 1)
- sq_norm = shapeQuality
- er_norm = np.clip(1.0 / edgeRatio, 0, 1)
- dih_norm = np.clip(minDihedralAngle / 60.0, 0, 1)
- score = (0.3 * ar_norm + 0.4 * sq_norm + 0.2 * er_norm + 0.1 * dih_norm) * 100
+ aspectRatioNorm = np.clip(1.0 / (aspectRatio / 1.73), 0, 1)
+ shapeQualityNorm = shapeQuality
+ edgeRatioNorm = np.clip(1.0 / edgeRatio, 0, 1)
+ dihedralMinNorm = np.clip(minDihedralAngle / 60.0, 0, 1)
+ score = (0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm) * 100
return score
From 845eb21c310308e87f6cc2d840526ef8f6ad6471 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Fri, 30 Jan 2026 15:32:14 +0100
Subject: [PATCH 03/11] Adding docstring
---
.../pre_processing/TetQualityAnalysis.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index 2681f1da..a07f1378 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -114,7 +114,7 @@ def applyFilter( self: Self) -> None:
maxDihedral = metrics['maxDihedral'][validMask]
dihedralRange = metrics['dihedral_range'][validMask]
- qualityScore = compute_quality_score(aspectRatio, shapeQuality, edgeRatio, minDihedral)
+ 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, 'dihedral_range': dihedralRange, 'qualityScore': qualityScore}
@@ -1184,8 +1184,18 @@ def setSampleForPlot( self: Self, data, n ):
# Combined quality score
-def compute_quality_score(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
- """Compute combined quality score (0-100)."""
+def computeQualityScore(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
+ """Compute combined quality score (0-100).
+
+ Args:
+ aspectRatio(npt.NDArray[np.float64]): Aspect ratio
+ shapeQuality(npt.NDArray[np.float64]): Shape quality
+ edgeRatio(npt.NDArray[np.float64]): Edge ratio
+ minDihedralAngle(npt.NDArray[np.float64]): Minimal edge ratio
+
+ Returns:
+ np.float64: Quality score
+ """
aspectRatioNorm = np.clip(1.0 / (aspectRatio / 1.73), 0, 1)
shapeQualityNorm = shapeQuality
edgeRatioNorm = np.clip(1.0 / edgeRatio, 0, 1)
From 4af6efd0d72d533df700364c97155ff11084c197 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Fri, 30 Jan 2026 17:51:43 +0100
Subject: [PATCH 04/11] Typing/linting
---
.../pre_processing/TetQualityAnalysis.py | 1631 +++++++++--------
1 file changed, 905 insertions(+), 726 deletions(-)
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index a07f1378..2a87a11a 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -4,15 +4,11 @@
import logging
import numpy as np
import numpy.typing as npt
-from typing_extensions import Self
+from typing_extensions import Self, Any #, Union
from geos.utils.Logger import ( Logger, getLogger )
import vtk
-import numpy as np
-import sys
-import matplotlib
from vtkmodules.vtkCommonDataModel import vtkDataSet
-from vtkmodules.util.numpy_support import vtk_to_numpy
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Rectangle
@@ -23,21 +19,26 @@
loggerName: str = "Tetrahedra Quality Analysis"
+
class TetQualityAnalysis:
- def __init__(
- self: Self,
- meshes: dict[str, vtkDataSet],
- speHandler: bool = False
- ) -> None:
+ 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[ Any ] = {}
- self.issues: dict[ Any ] = {}
- self.qualityScore: dict[ Any] = {}
- self.validMetrics: dict[int, dict[str, np.float64]] = {}
- self.medians: dict[ int, dict[str, np.float64 ]] = {}
- self.sample = {}
+ 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
@@ -48,7 +49,6 @@ def __init__(
self.logger.setLevel( logging.INFO )
self.logger.propagate = False
-
def setLoggerHandler( self: Self, handler: logging.Handler ) -> None:
"""Set a specific handler for the filter logger.
@@ -65,101 +65,120 @@ def setLoggerHandler( self: Self, handler: logging.Handler ) -> None:
self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler'"
" to True during the filter initialization." )
-
- def applyFilter( self: Self) -> None:
+ 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 ):
+ for n, ( nfilename, mesh ) in enumerate( self.meshes.items(), 1 ):
# print( n, nfilename )
- coords = self.getCoordinatesDoublePrecision(mesh)
- tetrahedraIds, tetrahedraConnectivity = self.extractTetConnectivity(mesh)
+ coords = self.getCoordinatesDoublePrecision( mesh )
+ tetrahedraIds, tetrahedraConnectivity = self.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()}" +
- f"\n" + "-"*80 + "\n" )
+ 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.analyzeAllTets(n, coords, tetrahedraConnectivity)
+ self.analyzeAllTets( n, coords, tetrahedraConnectivity )
metrics = self.analyzedMesh[ n ]
- self.analyzedMesh[n]["tet"] = ntets
+ self.tets[ n ] = ntets
# Extract data with consistent filtering
- validAspectRatio = np.isfinite(metrics['aspectRatio'])
- validRadiusRatio = np.isfinite(metrics['radiusRatio'])
+ 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]
+ 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]
+ minEdge = metrics[ 'minEdge' ][ validMask ]
+ maxEdge = metrics[ 'maxEdge' ][ validMask ]
# Edge length ratio
- edgeRatio = maxEdge / np.maximum(minEdge, 1e-15)
+ edgeRatio = maxEdge / np.maximum( minEdge, 1e-15 )
# Dihedral angles
- minDihedral = metrics['minDihedral'][validMask]
- maxDihedral = metrics['maxDihedral'][validMask]
- dihedralRange = metrics['dihedral_range'][validMask]
+ minDihedral = metrics[ 'minDihedral' ][ validMask ]
+ maxDihedral = metrics[ 'maxDihedral' ][ validMask ]
+ dihedralRange = metrics[ 'dihedralRange' ][ validMask ]
- qualityScore = computeQualityScore(aspectRatio, shapeQuality, edgeRatio, minDihedral)
+ 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, 'dihedral_range': dihedralRange, 'qualityScore': qualityScore}
+ 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 }
+ 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
+ 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 }
+ 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)
+ 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"]),
+ 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 ====================
@@ -175,49 +194,71 @@ def applyFilter( self: Self) -> None:
self.printExtremeOutlierAnalysis()
- self.printComparisonSummary()
+ self.printSummary()
self.computeDeltasFromBest()
self.createComparisonDashboard()
+ def getCoordinatesDoublePrecision( self, mesh: vtkDataSet ) -> npt.NDArray[ np.float64 ]:
+ """Get coordinates with double precision.
+
+ Args:
+ mesh (vtkDataSet): Input mesh.
+
+ Returns:
+ npt.NDArray[np.float64]: Coordinates
- def getCoordinatesDoublePrecision( self, mesh ):
+ """
points = mesh.GetPoints()
npoints = points.GetNumberOfPoints()
- coords = np.zeros((npoints, 3), dtype=np.float64)
- for i in range(npoints):
- point = points.GetPoint(i)
- coords[i] = [point[0], point[1], point[2]]
+ coords = np.zeros( ( npoints, 3 ), dtype=np.float64 )
+ for i in range( npoints ):
+ point = points.GetPoint( i )
+ coords[ i ] = [ point[ 0 ], point[ 1 ], point[ 2 ] ]
return coords
- def extractTetConnectivity( self, mesh ):
- """Extract connectivity for all tetrahedra."""
+ def extractTetConnectivity( self: Self,
+ mesh: vtkDataSet ) -> tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ] ]:
+ """Extract connectivity for all tetrahedra.
+
+ Args:
+ mesh (vtkDataSet): Mesh to analyze.
+
+ Returns:
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
+ Cell IDS corresponding to tetrahedra,
+ Connectivity of these cells
+ """
ncells = mesh.GetNumberOfCells()
tetrahedraIds = []
tetrahedraConnectivity = []
- for cellID in range(ncells):
- if mesh.GetCellType(cellID) == vtk.VTK_TETRA:
- cell = mesh.GetCell(cellID)
+ for cellID in range( ncells ):
+ if mesh.GetCellType( cellID ) == vtk.VTK_TETRA:
+ cell = mesh.GetCell( cellID )
point_ids = cell.GetPointIds()
- conn = [point_ids.GetId(i) for i in range(4)]
- tetrahedraIds.append(cellID)
- tetrahedraConnectivity.append(conn)
-
- return np.array(tetrahedraIds), np.array(tetrahedraConnectivity)
+ conn = [ point_ids.GetId( i ) for i in range( 4 ) ]
+ tetrahedraIds.append( cellID )
+ tetrahedraConnectivity.append( conn )
+ return np.array( tetrahedraIds ), np.array( tetrahedraConnectivity )
- def analyzeAllTets(self, n, coords, connectivity) -> dict:
- """Vectorized analysis of all tetrahedra."""
- ntets = connectivity.shape[0]
+ def analyzeAllTets( self, n: int, coords: npt.NDArray[ np.float64 ],
+ connectivity: npt.NDArray[ np.float64 ] ) -> None:
+ """Vectorized analysis of all tetrahedra.
+ Args:
+ n (int): Mesh id.
+ coords (npt.NDArray[np.float64]): Tetrahedra coordinates.
+ connectivity (npt.NDArray[np.float64]): Connectivity.
+ """
# Get coordinates for all vertices
- v0 = coords[connectivity[:, 0]]
- v1 = coords[connectivity[:, 1]]
- v2 = coords[connectivity[:, 2]]
- v3 = coords[connectivity[:, 3]]
+ v0 = coords[ connectivity[ :, 0 ] ]
+ v1 = coords[ connectivity[ :, 1 ] ]
+ v2 = coords[ connectivity[ :, 2 ] ]
+ v3 = coords[ connectivity[ :, 3 ] ]
# Compute edges
e01 = v1 - v0
@@ -228,117 +269,121 @@ def analyzeAllTets(self, n, coords, connectivity) -> dict:
e23 = v3 - v2
# Edge lengths
- l01 = np.linalg.norm(e01, axis=1)
- l02 = np.linalg.norm(e02, axis=1)
- l03 = np.linalg.norm(e03, axis=1)
- l12 = np.linalg.norm(e12, axis=1)
- l13 = np.linalg.norm(e13, axis=1)
- l23 = np.linalg.norm(e23, axis=1)
+ l01 = np.linalg.norm( e01, axis=1 )
+ l02 = np.linalg.norm( e02, axis=1 )
+ l03 = np.linalg.norm( e03, axis=1 )
+ l12 = np.linalg.norm( e12, axis=1 )
+ l13 = np.linalg.norm( e13, axis=1 )
+ l23 = np.linalg.norm( e23, axis=1 )
- allEdges = np.stack([l01, l02, l03, l12, l13, l23], axis=1)
- minEdge = np.min(allEdges, axis=1)
- maxEdge = np.max(allEdges, axis=1)
+ allEdges = np.stack( [ l01, l02, l03, l12, l13, l23 ], axis=1 )
+ minEdge = np.min( allEdges, axis=1 )
+ maxEdge = np.max( allEdges, axis=1 )
# Volumes
- cross_product = np.cross(e01, e02)
- volumes = np.abs(np.sum(cross_product * e03, axis=1) / 6.0)
+ cross_product = np.cross( e01, e02 )
+ volumes = np.abs( np.sum( cross_product * e03, axis=1 ) / 6.0 )
# Face areas
- face012 = 0.5 * np.linalg.norm(np.cross(e01, e02), axis=1)
- face013 = 0.5 * np.linalg.norm(np.cross(e01, e03), axis=1)
- face023 = 0.5 * np.linalg.norm(np.cross(e02, e03), axis=1)
- face123 = 0.5 * np.linalg.norm(np.cross(e12, e13), axis=1)
+ face012 = 0.5 * np.linalg.norm( np.cross( e01, e02 ), axis=1 )
+ face013 = 0.5 * np.linalg.norm( np.cross( e01, e03 ), axis=1 )
+ face023 = 0.5 * np.linalg.norm( np.cross( e02, e03 ), axis=1 )
+ face123 = 0.5 * np.linalg.norm( np.cross( e12, e13 ), axis=1 )
- allFaces = np.stack([face012, face013, face023, face123], axis=1)
- maxFaceArea = np.max(allFaces, axis=1)
- totalSurfaceArea = np.sum(allFaces, axis=1)
+ allFaces = np.stack( [ face012, face013, face023, face123 ], axis=1 )
+ maxFaceArea = np.max( allFaces, axis=1 )
+ totalSurfaceArea = np.sum( allFaces, axis=1 )
# Aspect ratio
- minAltitude = 3.0 * volumes / np.maximum(maxFaceArea, 1e-15)
- aspectRatio = maxEdge / np.maximum(minAltitude, 1e-15)
- aspectRatio[volumes < 1e-15] = np.inf
+ minAltitude = 3.0 * volumes / np.maximum( maxFaceArea, 1e-15 )
+ aspectRatio = maxEdge / np.maximum( minAltitude, 1e-15 )
+ aspectRatio[ volumes < 1e-15 ] = np.inf
# Radius ratio
- inradius = 3.0 * volumes / np.maximum(totalSurfaceArea, 1e-15)
- circumradius = (maxEdge ** 3) / np.maximum(24.0 * volumes, 1e-15)
- radius_ratio = circumradius / np.maximum(inradius, 1e-15)
+ inradius = 3.0 * volumes / np.maximum( totalSurfaceArea, 1e-15 )
+ circumradius = ( maxEdge**3 ) / np.maximum( 24.0 * volumes, 1e-15 )
+ radiusRatio = circumradius / np.maximum( inradius, 1e-15 )
# Flatness ratio
- flatness_ratio = volumes / np.maximum(maxFaceArea * minEdge, 1e-15)
+ flatnessRatio = volumes / np.maximum( maxFaceArea * minEdge, 1e-15 )
# Shape quality
- sum_edge_sq = np.sum(allEdges ** 2, axis=1)
- shape_quality = 12.0 * (3.0 * volumes) ** (2.0/3.0) / np.maximum(sum_edge_sq, 1e-15)
- shape_quality[volumes < 1e-15] = 0.0
+ sumEdgeShapeQuality = np.sum( allEdges**2, axis=1 )
+ shapeQuality = 12.0 * ( 3.0 * volumes )**( 2.0 / 3.0 ) / np.maximum( sumEdgeShapeQuality, 1e-15 )
+ shapeQuality[ volumes < 1e-15 ] = 0.0
# Dihedral angles
- def compute_dihedral_angle(normal1, normal2):
- """Compute dihedral angle between two face normals."""
- n1_norm = normal1 / np.maximum(np.linalg.norm(normal1, axis=1, keepdims=True), 1e-15)
- n2_norm = normal2 / np.maximum(np.linalg.norm(normal2, axis=1, keepdims=True), 1e-15)
- cos_angle = np.sum(n1_norm * n2_norm, axis=1)
- cos_angle = np.clip(cos_angle, -1.0, 1.0)
- angle = np.arccos(cos_angle) * 180.0 / np.pi
+ def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
+ normal2: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
+ """Compute dihedral angle between two face normals.
+
+ Args:
+ normal1 (npt.NDArray[np.float64]): Normal vector 1
+ normal2 (npt.NDArray[np.float64]): Normal vector 2
+
+ Returns:
+ npt.NDArray[ np.float64 ]: Dihedral angle
+
+ """
+ n1_norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
+ n2_norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
+ cos_angle = np.sum( n1_norm * n2_norm, axis=1 )
+ cos_angle = np.clip( cos_angle, -1.0, 1.0 )
+ angle = np.arccos( cos_angle ) * 180.0 / np.pi
return angle
# Face normals
- normal_012 = np.cross(e01, e02)
- normal_013 = np.cross(e01, e03)
- normal_023 = np.cross(e02, e03)
- normal_123 = np.cross(e12, e13)
+ normal012: npt.NDArray[ np.float64 ] = np.cross( e01, e02 )
+ normal013: npt.NDArray[ np.float64 ] = np.cross( e01, e03 )
+ normal023: npt.NDArray[ np.float64 ] = np.cross( e02, e03 )
+ normal123: npt.NDArray[ np.float64 ] = np.cross( e12, e13 )
# Dihedral angles for each edge
- dihedral_01 = compute_dihedral_angle(normal_012, normal_013)
- dihedral_02 = compute_dihedral_angle(normal_012, normal_023)
- dihedral_03 = compute_dihedral_angle(normal_013, normal_023)
- dihedral_12 = compute_dihedral_angle(normal_012, normal_123)
- dihedral_13 = compute_dihedral_angle(normal_013, normal_123)
- dihedral_23 = compute_dihedral_angle(normal_023, normal_123)
+ dihedral01 = computeDihedralAngle( normal012, normal013 )
+ dihedral02 = computeDihedralAngle( normal012, normal023 )
+ dihedral03 = computeDihedralAngle( normal013, normal023 )
+ dihedral12 = computeDihedralAngle( normal012, normal123 )
+ dihedral13 = computeDihedralAngle( normal013, normal123 )
+ dihedral23 = computeDihedralAngle( normal023, normal123 )
- all_dihedrals = np.stack([dihedral_01, dihedral_02, dihedral_03,
- dihedral_12, dihedral_13, dihedral_23], axis=1)
+ allDihedrals = np.stack( [ dihedral01, dihedral02, dihedral03, dihedral12, dihedral13, dihedral23 ], axis=1 )
- min_dihedral = np.min(all_dihedrals, axis=1)
- max_dihedral = np.max(all_dihedrals, axis=1)
- dihedral_range = max_dihedral - min_dihedral
+ minDihedral = np.min( allDihedrals, axis=1 )
+ maxDihedral = np.max( allDihedrals, axis=1 )
+ dihedralRange = maxDihedral - minDihedral
self.analyzedMesh[ n ] = {
'volumes': volumes,
'aspectRatio': aspectRatio,
- 'radiusRatio': radius_ratio,
- 'flatnessRatio': flatness_ratio,
- 'shapeQuality': shape_quality,
+ 'radiusRatio': radiusRatio,
+ 'flatnessRatio': flatnessRatio,
+ 'shapeQuality': shapeQuality,
'minEdge': minEdge,
'maxEdge': maxEdge,
- 'minDihedral': min_dihedral,
- 'maxDihedral': max_dihedral,
- 'dihedral_range': dihedral_range
+ 'minDihedral': minDihedral,
+ 'maxDihedral': maxDihedral,
+ 'dihedralRange': dihedralRange
}
- return {
- 'volumes': volumes,
- 'aspectRatio': aspectRatio,
- 'radiusRatio': radius_ratio,
- 'flatnessRatio': flatness_ratio,
- 'shapeQuality': shape_quality,
- 'minEdge': minEdge,
- 'maxEdge': maxEdge,
- 'minDihedral': min_dihedral,
- 'maxDihedral': max_dihedral,
- 'dihedral_range': dihedral_range
- }
-
- def printDistributionStatistics( self: Self, fmt='.2e'):
+ def printDistributionStatistics( self: Self ) -> None:
+ """Print the distribution statistics for various metrics."""
self.__loggerSection( "DISTRIBUTION STATISTICS (MIN / MEDIAN / MAX)" )
- def printMetricStats( metricName, dataName, fmt='.2e'):
- """Helper function to print min/median/max for a metric."""
+ 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 ][ dataName ]
+ 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 )
+ self.logger.info( msg )
printMetricStats( "Aspect Ratio", 'aspectRatio' )
printMetricStats( "Radius Ratio", 'radiusRatio' )
@@ -350,25 +395,32 @@ def printMetricStats( metricName, dataName, fmt='.2e'):
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)", 'dihedral_range',fmt='.2f' )
+ printMetricStats( "Dihedral Range (degrees)", 'dihedralRange', fmt='.2f' )
printMetricStats( "Overall Quality Score (0-100)", 'qualityScore', fmt='.2f' )
+ def printPercentileAnalysis( self: Self, fmt: str = '.2f' ) -> None:
+ """Print percentile analysis.
- def printPercentileAnalysis( self: Self, fmt='.2f' ):
+ 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' ) ]):
+ 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])
+ 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 )
+ self.logger.info( msg )
def printQualityIssueSummary( self: Self ) -> None:
+ """Print the quality issues."""
self.__loggerSection( "QUALITY ISSUE SUMMARY" )
fmt = '.2f'
@@ -376,48 +428,54 @@ def printQualityIssueSummary( self: Self ) -> None:
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' )
- ]):
+ 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 ] )
- percent = pb/m*100
+ 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()
-
- def printOverallQualityScore( self: Self ):
- msg = f"Overall Quality Score Distribution:\n"
- msg += f" {f'Mesh n':10}{f'Excellent (>80)':20}{f'Good (60-80)':17}{f'Fair (30-60)':15}{f'Poor (≤30)':15}\n"
+ 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 )
- def printExtremeOutlierAnalysis( self: Self ):
+ def printExtremeOutlierAnalysis( self: Self ) -> None:
+ """Print the extreme outlier analysis results."""
self.__loggerSection( "EXTREME OUTLIER ANALYSIS" )
- # self.logger.warning( f"\n🚨 Elements with Aspect Ratio > 10,000:" )
- msg = f"Elements with Aspect Ratio > 10,000:\n"
+ 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]
+ extremeAspectRatio = self.issues[ n ][ 'extremeAspectRatio' ]
+ data = self.analyzedMesh[ n ]
aspectRatio = data[ 'aspectRatio' ]
- if np.sum(extremeAspectRatio) > 0:
+ 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" ]
@@ -429,27 +487,28 @@ def printExtremeOutlierAnalysis( self: Self ):
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:
+ 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 )
+ if w2:
+ self.logger.warning( msg2 )
# Nearly degenerate elements
- degMsg = f"Nearly Degenerate Elements (dihedral < 0.1° or > 179.9°):\n"
+ 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:
+ 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" )
-
- def printComparisonSummary( self: Self):
+ def printSummary( self: Self ) -> None:
+ """Print the summary."""
self.__loggerSection( "COMPARISON SUMMARY" )
for n, _ in enumerate( self.meshes, 1 ):
@@ -460,8 +519,7 @@ def printComparisonSummary( self: Self):
name += " [LEAST GOOD]"
msg = f"{name}\n"
-
- msg += f" Tetrahedra: {self.analyzedMesh[n]['tet']:,}\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"
@@ -472,74 +530,72 @@ def printComparisonSummary( self: Self):
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 )
-
- def computeDeltasFromBest( self: Self ):
- self.logger.info( f"Best mesh : Mesh {self.best}")
- self.deltas = {}
-
- ntetsBest = self.analyzedMesh[self.best][ 'tet']
- aspectRatioBest = self.medians[self.best]["aspectRatio"]
- shapeQualityBest = self.medians[self.best]["shapeQuality"]
- volumeBest = self.medians[self.best]["volume"]
- minEdgeBest = self.medians[self.best]["minEdge"]
- maxEdgeBest = self.medians[self.best]["maxEdge"]
- edgeRatioBest = self.medians[self.best]["edgeRatio"]
+ 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] = {}
- ntets = self.analyzedMesh[n][ 'tet']
-
- 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"])
+ self.deltas[ n ] = {}
- self.deltas[n]["tetrahedra"] = ( ( self.analyzedMesh[n]["tet"] - self.analyzedMesh[self.best]["tet"]) / self.analyzedMesh[self.best]["tet"] * 100 ) if self.analyzedMesh[self.best]["tet"] > 0 else 0
+ 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
-
-
- dtets = [ f"{self.deltas[ n ][ 'tetrahedra' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- dar = [ f"{self.deltas[ n ][ 'aspectRatio' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- dsq = [ f"{self.deltas[ n ][ 'shapeQuality' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- dvol = [ f"{self.deltas[ n ][ 'volume' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- d_min_edge = [ f"{self.deltas[ n ][ 'minEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- d_maxEdge = [ f"{self.deltas[ n ][ 'maxEdge' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
- d_edge_ratio = [ f"{self.deltas[ n ][ 'edgeRatio' ]:>+12,.1f}%" if n != self.best else "" for n, _ in enumerate( self.meshes, 1 ) ]
+ value = self.medians[ n ][ metric ]
+ valueBest = self.medians[ self.best ][ metric ]
+ self.deltas[ n ][ metric ] = ( ( value - valueBest ) / valueBest * 100 ) if valueBest > 0 else 0
+
+ dtets = [
+ f"{self.deltas[ n ][ 'tetrahedra' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ dar = [
+ f"{self.deltas[ n ][ 'aspectRatio' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ dsq = [
+ f"{self.deltas[ n ][ 'shapeQuality' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ dvol = [
+ f"{self.deltas[ n ][ 'volume' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ d_min_edge = [
+ f"{self.deltas[ n ][ 'minEdge' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ d_maxEdge = [
+ f"{self.deltas[ n ][ 'maxEdge' ]:>+12,.1f}%" if n != self.best else ""
+ for n, _ in enumerate( self.meshes, 1 )
+ ]
+ d_edge_ratio = [
+ 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(dtets)}\n" +
- f"{' Aspect Ratio:':<20}{('').join( dar)}\n" +
- f"{' Shape Quality:':<20}{('').join( dsq)}\n" +
- f"{' Volume:':<20}{('').join( dvol)}\n" +
- f"{' Min Edge Length:':<20}{('').join( d_min_edge)}\n" +
- f"{' Max Edge Length:':<20}{('').join( d_maxEdge)}\n" +
- f"{' Edge Length Ratio:':<20}{('').join( d_edge_ratio)}\n"
- )
+ self.logger.info( f"Changes vs BEST [Mesh {self.best}]:\n" + f"{' Mesh:':<20}{('').join( names )}\n" +
+ f"{' Tetrahedra:':<20}{('').join(dtets)}\n" +
+ f"{' Aspect Ratio:':<20}{('').join( dar)}\n" +
+ f"{' Shape Quality:':<20}{('').join( dsq)}\n" + f"{' Volume:':<20}{('').join( dvol)}\n" +
+ f"{' Min Edge Length:':<20}{('').join( d_min_edge)}\n" +
+ f"{' Max Edge Length:':<20}{('').join( d_maxEdge)}\n" +
+ f"{' Edge Length Ratio:':<20}{('').join( d_edge_ratio)}\n" )
-
- def createComparisonDashboard( self: Self ):
+ def createComparisonDashboard( self: Self ) -> None:
+ """Create the comparison dashboard."""
lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes, 1 ) ]
# Determine smart plot limits
ar_99 = []
for n, _ in enumerate( self.meshes, 1 ):
- ar_99.append( np.percentile( self.validMetrics[n]["aspectRatio"], 99 ) )
+ ar_99.append( np.percentile( self.validMetrics[ n ][ "aspectRatio" ], 99 ) )
ar_99_max = np.max( np.array( ar_99 ) )
- # ar_99_1 = np.percentile(aspectRatio, 99)
- # # ar_99_2 = np.percentile(ar2, 99)
- # # ar_99_max = max(ar_99_1, ar_99_2)
- # ar_99_max = ar_99_1
-
if ar_99_max < 10:
ar_plot_limit = 100
elif ar_99_max < 100:
@@ -547,628 +603,745 @@ def createComparisonDashboard( self: Self ):
else:
ar_plot_limit = 10000
- # print(f"Using AR plot limit: {ar_plot_limit} (99th percentiles: M1={ar_99_1:.1f}, M2={ar_99_2:.1f})")
- # print(f"Using AR plot limit: {ar_plot_limit} (99th percentiles: M1={ar_99_1:.1f}")
-
# Set style
- plt.rcParams['figure.facecolor'] = 'white'
- plt.rcParams.update({
+ 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))
+ 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.96, bottom=0.84, wspace=0.20)
+ gs_row1 = gridspec.GridSpec( 1, 3, figure=fig, left=0.05, right=0.95, top=0.96, 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.82, bottom=0.05, hspace=0.35, wspace=0.30)
+ gs_main = gridspec.GridSpec( 4,
+ 5,
+ figure=fig,
+ left=0.05,
+ right=0.95,
+ top=0.82,
+ bottom=0.05,
+ hspace=0.35,
+ wspace=0.30 )
# Title
suptitle = 'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n'
for n, _ in enumerate( self.meshes, 1 ):
# suptitle += f'Mesh {n}: {ntets:,} tets\t'
- suptitle += f'Mesh {n}: {self.analyzedMesh[n]["tet"]} tets - '
- fig.suptitle( suptitle,
- fontsize=16, fontweight='bold', y=0.99)
+ suptitle += f'Mesh {n}: {self.tets[n]:<15} tets'
+ fig.suptitle( suptitle, fontsize=16, fontweight='bold', y=0.99 )
# Color scheme
- color = matplotlib.pyplot.cm.tab10( np.arange(20))
+ 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)
+ 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)
+ 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)
+ 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.98,
- 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=dict(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)
+ ax1.text(
+ 0.98,
+ 0.98,
+ 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')
+ 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])
+ 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"]
+ aspectRatio = self.validMetrics[ n ][ "aspectRatio" ]
+ shapeQuality = self.validMetrics[ n ][ "shapeQuality" ]
self.setSampleForPlot( aspectRatio, n )
- idx = self.sample[n]
+ idx = self.sample[ n ]
- mask1_plot = aspectRatio[idx] < ar_plot_limit
+ mask1_plot = aspectRatio[ idx ] < ar_plot_limit
- ax2.scatter(aspectRatio[idx][mask1_plot], shapeQuality[idx][mask1_plot], alpha=0.4, s=5,
- color=color[n-1], label=f'Mesh {n}', edgecolors='none')
+ ax2.scatter( aspectRatio[ idx ][ mask1_plot ],
+ shapeQuality[ idx ][ mask1_plot ],
+ 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)
+ 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
- problem_zone = Rectangle((100, 0), ar_plot_limit-100, 0.3,
- alpha=0.2, facecolor='red', edgecolor='none', zorder=0)
- ax2.add_patch(problem_zone)
+ problem_zone = Rectangle( ( 100, 0 ),
+ ar_plot_limit - 100,
+ 0.3,
+ alpha=0.2,
+ facecolor='red',
+ edgecolor='none',
+ zorder=0 )
+ ax2.add_patch( problem_zone )
# Count ALL elements
- problem1_all = np.sum((aspectRatio > 100) & (shapeQuality < 0.3))
- extreme1 = np.sum(aspectRatio > ar_plot_limit)
+ np.sum( ( aspectRatio > 100 ) & ( shapeQuality < 0.3 ) )
+ np.sum( aspectRatio > ar_plot_limit )
# Problem annotation
- #TODO
- 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,
- f'PROBLEM ELEMENTS\n(AR>100 & Q<0.3):\n\n' +
- annotateIssues,
- transform=ax2.transAxes, va='bottom', ha='right',
- bbox=dict(boxstyle='round', facecolor='#ffcccc', alpha=0.95,
- edgecolor='darkred', linewidth=2),
- fontsize=8, fontweight='bold')
-
- # Outlier warning
- # if extreme1 + extreme2 > 100:
- # ax2.text(0.02, 0.98,
- # f'⚠️ AR > {ar_plot_limit}\n(not shown):\n' +
- # f'M1: {extreme1:,}\nM2: {extreme2:,}',
- # transform=ax2.transAxes, va='top', ha='left',
- # bbox=dict(boxstyle='round', facecolor='#ffe6cc', alpha=0.9),
- # fontsize=7)
-
- 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, ar_plot_limit])
- ax2.set_ylim([0, 1.05])
- ax2.legend(loc='upper right', fontsize=7, framealpha=0.95)
- ax2.grid(True, alpha=0.3)
+ 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, ar_plot_limit ) )
+ 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')
+ ax3 = fig.add_subplot( gs_row1[ 0, 2 ] )
+ ax3.axis( 'off' )
summaryStats = []
- summaryStats.append(['CRITICAL ISSUE', 'WORST', 'BEST', 'Change'])
- summaryStats.append(['─' * 18, '─' * 10, '─' * 10, '─' * 10])
+ summaryStats.append( [ 'CRITICAL ISSUE', 'WORST', 'BEST', 'Change' ] )
+ summaryStats.append( [ '─' * 18, '─' * 10, '─' * 10, '─' * 10 ] )
criticalCombo = self.issues[ self.best ][ "criticalCombo" ]
critical_combo2 = self.issues[ self.worst ][ "criticalCombo" ]
- aspectRatio = self.validMetrics[self.best][ "aspectRatio" ]
- ar2 = self.validMetrics[self.worst][ "aspectRatio" ]
+ aspectRatio = self.validMetrics[ self.best ][ "aspectRatio" ]
+ ar2 = self.validMetrics[ self.worst ][ "aspectRatio" ]
highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ]
high_ar2 = self.issues[ self.worst ][ "highAspectRatio" ]
- lowShapeQuality = self.issues[self.best ][ "lowShapeQuality" ]
- low_sq2 = self.issues[self.worst][ "lowShapeQuality" ]
-
- criticalMinDihedral = self.issues[self.best]["criticalMinDihedral"]
- critical_dih2 = self.issues[self.worst]["criticalMinDihedral"]
-
- criticalMaxDihedral = self.issues[self.best][ "criticalMaxDihedral" ]
- critical_max_dih2 = self.issues[self.worst][ "criticalMaxDihedral" ]
-
- highEdgeRatio = self.issues[self.best][ "highEdgeRatio" ]
- highEdgeRatio2 = self.issues[self.worst][ "highEdgeRatio" ]
-
-
- summaryStats.append(['CRITICAL Combo', f'{criticalCombo:,}', f'{critical_combo2:,}',
- f'{((critical_combo2-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'({critical_combo2/len(ar2)*100:.2f}%)', ''])
- summaryStats.append([' & MinDih<5°)', '', '', ''])
-
- summaryStats.append(['', '', '', ''])
- summaryStats.append(['AR > 100', f'{highAspectRatio:,}', f'{high_ar2:,}',
- f'{((high_ar2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%'])
-
- summaryStats.append(['Quality < 0.3', f'{lowShapeQuality:,}', f'{low_sq2:,}',
- f'{((low_sq2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%'])
-
- summaryStats.append(['MinDih < 5°', f'{criticalMinDihedral:,}', f'{critical_dih2:,}',
- f'{((critical_dih2-criticalMinDihedral)/max(criticalMinDihedral,1)*100):+.1f}%' if criticalMinDihedral > 0 else 'N/A'])
-
- summaryStats.append(['MaxDih > 175°', f'{criticalMaxDihedral:,}', f'{critical_max_dih2:,}',
- f'{((critical_max_dih2-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], edges='open')
- table.auto_set_font_size(False)
- table.set_fontsize(8)
+ lowShapeQuality = self.issues[ self.best ][ "lowShapeQuality" ]
+ low_sq2 = self.issues[ self.worst ][ "lowShapeQuality" ]
+
+ criticalMinDihedral = self.issues[ self.best ][ "criticalMinDihedral" ]
+ critical_dih2 = self.issues[ self.worst ][ "criticalMinDihedral" ]
+
+ criticalMaxDihedral = self.issues[ self.best ][ "criticalMaxDihedral" ]
+ critical_max_dih2 = self.issues[ self.worst ][ "criticalMaxDihedral" ]
+
+ highEdgeRatio = self.issues[ self.best ][ "highEdgeRatio" ]
+ highEdgeRatio2 = self.issues[ self.worst ][ "highEdgeRatio" ]
+
+ summaryStats.append( [
+ 'CRITICAL Combo', f'{criticalCombo:,}', f'{critical_combo2:,}',
+ f'{((critical_combo2-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'({critical_combo2/len(ar2)*100:.2f}%)',
+ ''
+ ] )
+ summaryStats.append( [ ' & MinDih<5°)', '', '', '' ] )
+
+ summaryStats.append( [ '', '', '', '' ] )
+ summaryStats.append( [
+ 'AR > 100', f'{highAspectRatio:,}', f'{high_ar2:,}',
+ f'{((high_ar2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%'
+ ] )
+
+ summaryStats.append( [
+ 'Quality < 0.3', f'{lowShapeQuality:,}', f'{low_sq2:,}',
+ f'{((low_sq2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%'
+ ] )
+
+ summaryStats.append( [
+ 'MinDih < 5°', f'{criticalMinDihedral:,}', f'{critical_dih2:,}',
+ f'{((critical_dih2-criticalMinDihedral)/max(criticalMinDihedral,1)*100):+.1f}%'
+ if criticalMinDihedral > 0 else 'N/A'
+ ] )
+
+ summaryStats.append( [
+ 'MaxDih > 175°', f'{criticalMaxDihedral:,}', f'{critical_max_dih2:,}',
+ f'{((critical_max_dih2-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='white', fontsize=9)
+ for i in range( 4 ):
+ table[ ( 0, i ) ].set_facecolor( '#34495e' )
+ table[ ( 0, i ) ].set_text_props( weight='bold', color='white', 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)
+ 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):
- change_text = summaryStats[row][3]
+ for row in [ 2, 6, 7, 8, 9, 10, 13, 14, 15 ]:
+ if row < len( summaryStats ):
+ change_text = summaryStats[ row ][ 3 ]
if '%' in change_text and change_text != 'N/A':
- try:
- val = float(change_text.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')
- except:
- pass
-
- ax3.set_title('CRITICAL ISSUES SUMMARY', fontsize=12, fontweight='bold',
- color='darkgreen', pad=10)
+ val = float( change_text.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)
+ 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')
+ 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)
+ ax4.grid( True, alpha=0.3 )
# 5. Aspect Ratio Histogram
- ax5 = fig.add_subplot(gs_main[0, 1])
- # bins = np.logspace(0, np.log10(min(ar_plot_limit, aspectRatio.max(), ar2.max())), 40)
- ar_max = np.array( [ self.validMetrics[n]["aspectRatio"].max() for n, _ in enumerate( self.meshes, 1 )] )
+ ax5 = fig.add_subplot( gs_main[ 0, 1 ] )
+ ar_max = np.array( [ self.validMetrics[ n ][ "aspectRatio" ].max() for n, _ in enumerate( self.meshes, 1 ) ] )
- bins = np.logspace(0, np.log10(min(ar_plot_limit, ar_max.max())), 40)
+ bins = np.logspace( 0, np.log10( min( ar_plot_limit, ar_max.max() ) ), 40 ).tolist()
for n, _ in enumerate( self.meshes, 1 ):
- aspectRatio = self.validMetrics[n]['aspectRatio']
- ax5.hist(aspectRatio[aspectRatio < ar_plot_limit], 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')
+ aspectRatio = self.validMetrics[ n ][ 'aspectRatio' ]
+ ax5.hist( aspectRatio[ aspectRatio < ar_plot_limit ],
+ 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)
+ ax5.grid( True, alpha=0.3 )
# 6. Min Dihedral Histogram
- ax6 = fig.add_subplot(gs_main[0, 2])
- bins = np.linspace(0, 90, 40)
+ 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')
+ 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)
+ ax6.grid( True, alpha=0.3 )
# 7. Edge Ratio Histogram
- ax7 = fig.add_subplot(gs_main[0, 3])
- bins = np.logspace(0, 3, 40)
+ 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.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)
+ ax7.grid( True, alpha=0.3 )
# 8. Volume Histogram
- ax8 = fig.add_subplot(gs_main[0, 4])
- vol_min = np.array( [ self.validMetrics[n]["volume"].min() for n, _ in enumerate( self.meshes, 1 )] ).min()
- vol_max = np.array( [ self.validMetrics[n]["volume"].max() for n, _ in enumerate( self.meshes, 1 )] ).max()
-
+ ax8 = fig.add_subplot( gs_main[ 0, 4 ] )
+ vol_min = np.array( [ self.validMetrics[ n ][ "volume" ].min() for n, _ in enumerate( self.meshes, 1 ) ] ).min()
+ vol_max = np.array( [ self.validMetrics[ n ][ "volume" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ).max()
- bins = np.logspace(np.log10(vol_min), np.log10(vol_max), 40)
+ bins = np.logspace( np.log10( vol_min ), np.log10( vol_max ), 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')
+ 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)
+ 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)
- ax9.set_ylabel('Shape Quality', fontweight='bold')
- ax9.set_title('Shape Quality Comparison', fontweight='bold')
- ax9.grid(True, alpha=0.3, axis='y')
+ ax9 = fig.add_subplot( gs_main[ 1, 0 ] )
+ sq = [ self.validMetrics[ n ][ "shapeQuality" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp1 = ax9.boxplot( sq, label=lbl, patch_artist=True, showfliers=False )
+ 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)
- 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')
+ ax10 = fig.add_subplot( gs_main[ 1, 1 ] )
+ ar = [ self.validMetrics[ n ][ "aspectRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp2 = ax10.boxplot( ar, label=lbl, patch_artist=True, showfliers=False )
+ 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)
- 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')
+ ax11 = fig.add_subplot( gs_main[ 1, 2 ] )
+ minDihedral = [ self.validMetrics[ n ][ "minDihedral" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp3 = ax11.boxplot( minDihedral, label=lbl, patch_artist=True, showfliers=False )
+ 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])
- edge_ratio = [ self.validMetrics[n][ "edgeRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
- bp4 = ax12.boxplot(edge_ratio, labels=lbl,
- patch_artist=True, showfliers=False)
- 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')
+ ax12 = fig.add_subplot( gs_main[ 1, 3 ] )
+ edge_ratio = [ self.validMetrics[ n ][ "edgeRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp4 = ax12.boxplot( edge_ratio, label=lbl, patch_artist=True, showfliers=False )
+ 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)
- 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')
+ ax13 = fig.add_subplot( gs_main[ 1, 4 ] )
+ vol = [ self.validMetrics[ n ][ "volume" ] for n, _ in enumerate( self.meshes, 1 ) ]
+ bp5 = ax13.boxplot( vol, label=lbl, patch_artist=True, showfliers=False )
+ 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")
+ 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) ====================
-
- # Use existing idx1, idx2 from executive summary
-
# 14. Shape Quality vs Aspect Ratio (duplicate for detail)
- ax14 = fig.add_subplot(gs_main[2, 0])
+ 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] < ar_plot_limit
- 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, ar_plot_limit])
- ax14.set_ylim([0, 1.05])
- ax14.legend(loc='upper right', fontsize=7)
- ax14.grid(True, alpha=0.3)
+ idx = self.sample[ n ]
+ aspectRatio = self.validMetrics[ n ][ 'aspectRatio' ]
+ shapeQuality = self.validMetrics[ n ][ 'shapeQuality' ]
+ mask1 = aspectRatio[ idx ] < ar_plot_limit
+ 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, ar_plot_limit ) )
+ 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])
+ 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] < ar_plot_limit
- 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, ar_plot_limit])
- ax15.legend(loc='upper right', fontsize=7)
- ax15.grid(True, alpha=0.3)
+ idx = self.sample[ n ]
+ aspectRatio = self.validMetrics[ n ][ "aspectRatio" ]
+ flatnessRatio = self.validMetrics[ n ][ 'flatnessRatio' ]
+ mask1 = aspectRatio[ idx ] < ar_plot_limit
+ 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, ar_plot_limit ) )
+ 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])
+ 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] < ar_plot_limit
- 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, ar_plot_limit])
- ax16.legend(loc='upper right', fontsize=7)
- ax16.grid(True, alpha=0.3)
+ idx = self.sample[ n ]
+ aspectRatio = self.validMetrics[ n ][ "aspectRatio" ]
+ volume = self.validMetrics[ n ][ 'volume' ]
+ mask1 = aspectRatio[ idx ] < ar_plot_limit
+ 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, ar_plot_limit ) )
+ 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])
+ 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)
+ 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])
+ ax18 = fig.add_subplot( gs_main[ 2, 4 ] )
for n, _ in enumerate( self.meshes, 1 ):
- idx = self.sample[n]
- volume = self.validMetrics[n][ 'volume' ]
- edge_ratio = self.validMetrics[n][ 'edgeRatio' ]
- ax18.scatter(volume[idx], edge_ratio[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)
+ idx = self.sample[ n ]
+ volume = self.validMetrics[ n ][ 'volume' ]
+ edge_ratio = self.validMetrics[ n ][ 'edgeRatio' ]
+ ax18.scatter( volume[ idx ],
+ edge_ratio[ 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])
- edge_min = np.array( [ self.validMetrics[n]["minEdge"].min() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
- edge_max_min = np.array( [ self.validMetrics[n]["minEdge"].max() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
+ ax19 = fig.add_subplot( gs_main[ 3, 0 ] )
+ edge_min = np.array( [ self.validMetrics[ n ][ "minEdge" ].min()
+ for n, _ in enumerate( self.meshes, 1 ) ] ).min()
+ edge_max_min = np.array( [ self.validMetrics[ n ][ "minEdge" ].max()
+ for n, _ in enumerate( self.meshes, 1 ) ] ).min()
- bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
+ bins = np.logspace( np.log10( edge_min ), np.log10( edge_max_min ), 40 ).tolist()
for n, _ in enumerate( self.meshes, 1 ):
- # edge_min = min(minEdge.min(), min_edge2.min())
- # edge_max_min = max(minEdge.max(), min_edge2.max())
- # bins = np.logspace(np.log10(edge_min), np.log10(edge_max_min), 40)
- 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')
+ 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)
+ ax19.grid( True, alpha=0.3 )
# 20. Max Edge Length Histogram
- ax20 = fig.add_subplot(gs_main[3, 1])
- edge_max = np.array( [ self.validMetrics[n]["maxEdge"].max() for n,_ in enumerate( self.meshes, 1 ) ] ).max()
- edge_min_max = np.array( [ self.validMetrics[n]["maxEdge"].min() for n,_ in enumerate( self.meshes, 1 ) ] ).min()
+ ax20 = fig.add_subplot( gs_main[ 3, 1 ] )
+ edge_max = np.array( [ self.validMetrics[ n ][ "maxEdge" ].max()
+ for n, _ in enumerate( self.meshes, 1 ) ] ).max()
+ edge_min_max = np.array( [ self.validMetrics[ n ][ "maxEdge" ].min()
+ for n, _ in enumerate( self.meshes, 1 ) ] ).min()
- # edge_max = max(maxEdge.max(), max_edge2.max())
- # edge_min_max = min(maxEdge.min(), max_edge2.min())
- bins = np.logspace(np.log10(edge_min_max), np.log10(edge_max), 40)
+ bins = np.logspace( np.log10( edge_min_max ), np.log10( edge_max ), 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')
+ 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)
+ ax20.grid( True, alpha=0.3 )
# 21. Max Dihedral Histogram
- ax21 = fig.add_subplot(gs_main[3, 2])
- bins = np.linspace(90, 180, 40)
+ 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')
+ 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)
+ ax21.grid( True, alpha=0.3 )
# 22. Dihedral Range Box Plot
- ax22 = fig.add_subplot(gs_main[3, 3])
+ 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 )]
+ 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, 1 ) ] + [ n for n, _ in enumerate( self.meshes, ) ]
- bp_dih = ax22.boxplot(dih,
- positions=positions,
- labels=lbl_boxplot,
- 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')
+ bp_dih = ax22.boxplot( dih,
+ positions=positions,
+ label=lbl_boxplot,
+ 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])
+ 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)
+ 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
output_png = '/data/pau901/SIM_CS/04_WORKSPACE/USERS/PalomaMartinez/geosPythonPackages/TESST/mesh_comparison.png'
- print(f"\nSaving dashboard to: {output_png}")
- plt.savefig(output_png, dpi=300, bbox_inches='tight', facecolor='white')
- print(f"Dashboard saved successfully!")
+ print( f"\nSaving dashboard to: {output_png}" )
+ plt.savefig( output_png, dpi=300, bbox_inches='tight', facecolor='white' )
+ print( "Dashboard saved successfully!" )
+ def setDashboardFilename( self: Self, filename: str )
+ self.filename = filename
-
-
- def __loggerSection( self: Self, sectionName: str ):
- self.logger.info("="*80)
+ def __loggerSection( self: Self, sectionName: str ) -> None:
+ self.logger.info( "=" * 80 )
self.logger.info( sectionName )
- self.logger.info("="*80)
-
-
- def __orderMeshes( self: Self ):
- """Proposition of ordering as fonction of median quality score"""
- self.__loggerSection( "ORDERING MESHES (from median quality score)")
- median_score = { n : np.median( self.validMetrics[n]["qualityScore"] ) for n, _ in enumerate (self.meshes, 1) }
+ 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)" )
+ median_score = {
+ n: np.median( self.validMetrics[ n ][ "qualityScore" ] )
+ for n, _ in enumerate( self.meshes, 1 )
+ }
- sorted_meshes = sorted( median_score.items(), key=lambda x:x[1], reverse = True )
+ sorted_meshes = sorted( median_score.items(), key=lambda x: x[ 1 ], reverse=True )
self.sorted = sorted_meshes
- self.best = sorted_meshes[ 0 ][0]
- self.worst = sorted_meshes[ -1 ][0]
+ self.best = sorted_meshes[ 0 ][ 0 ]
+ self.worst = sorted_meshes[ -1 ][ 0 ]
- self.logger.info( f"Mesh order from median quality score:" )
+ self.logger.info( "Mesh order from median quality score:" )
top = [ f"Mesh {m[0]} ({m[1]:.2f})" for m in sorted_meshes ]
- toprint = (" > ").join( top )
+ toprint = ( " > " ).join( top )
self.logger.info( " [+] " + toprint + " [-]\n" )
-
- def compareIssuesFromBest( self: Self ):
- highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ]
- criticalMinDihedral = self.issues[ self.best ]["criticalMinDihedral"]
- lowShapeQuality = self.issues[ self.best ]["lowShapeQuality"]
- criticalCombo = self.issues[ self.best ]["criticalCombo"]
-
- def getPercentChangeFromBest( data, ref ):
- return (data - ref) / max( ref, 1)*100
-
- msg = f"Change from BEST [Mesh {self.best}]\n"
- msg += f" {f'Mesh':20}"+ ("").join([f"{f'Mesh {n}':>16}" for n, _ in enumerate( self.meshes, 1 )]) + "\n"
- highAspectRatio = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'highAspectRatio' ], highAspectRatio ):>+15,.1f}%" if n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ]
- lowShapeQuality = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'lowShapeQuality' ], lowShapeQuality ):>+15,.1f}%" if n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 ) ]
- criticalMinDihedral = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'criticalMinDihedral' ], criticalMinDihedral ):>+15,.1f}%" if criticalMinDihedral > 0 and n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 )]
- criticalCombo = [ f"{getPercentChangeFromBest( self.issues[ n ][ 'criticalCombo' ], criticalCombo ):>+15,.1f}%" if criticalCombo > 0 and n!= self.best else f"{'N/A':>16}" for n, _ in enumerate( self.meshes, 1 )]
+ 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"
@@ -1177,14 +1350,21 @@ def getPercentChangeFromBest( data, ref ):
self.logger.info( msg )
+ def setSampleForPlot( self: Self, data: npt.NDArray[ Any ], n: int ) -> None:
+ """Set sampling for a given metric of mesh n.
- def setSampleForPlot( self: Self, data, n ):
- n_sample = min(10000, len( data ))
- self.sample[n] = np.random.choice(len(data), n_sample, replace=False)
+ 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 )
# Combined quality score
-def computeQualityScore(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
+def computeQualityScore( aspectRatio: npt.NDArray[ np.float64 ], shapeQuality: npt.NDArray[ np.float64 ],
+ edgeRatio: npt.NDArray[ np.float64 ],
+ minDihedralAngle: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
"""Compute combined quality score (0-100).
Args:
@@ -1196,11 +1376,10 @@ def computeQualityScore(aspectRatio, shapeQuality, edgeRatio, minDihedralAngle):
Returns:
np.float64: Quality score
"""
- aspectRatioNorm = np.clip(1.0 / (aspectRatio / 1.73), 0, 1)
+ aspectRatioNorm = np.clip( 1.0 / ( aspectRatio / 1.73 ), 0, 1 )
shapeQualityNorm = shapeQuality
- edgeRatioNorm = np.clip(1.0 / edgeRatio, 0, 1)
- dihedralMinNorm = np.clip(minDihedralAngle / 60.0, 0, 1)
- score = (0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm) * 100
- return score
-
+ edgeRatioNorm = np.clip( 1.0 / edgeRatio, 0, 1 )
+ dihedralMinNorm = np.clip( minDihedralAngle / 60.0, 0, 1 )
+ score = ( 0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm ) * 100
+ return score
From a98cfe227fcd755a1e91c9a1754ec4c8b0c6d6eb Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Mon, 2 Feb 2026 11:19:20 +0100
Subject: [PATCH 05/11] Doc, typing and small fixes
---
.../pre_processing/TetQualityAnalysis.py | 298 ++++++++++--------
1 file changed, 171 insertions(+), 127 deletions(-)
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index 2a87a11a..058ac62b 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -14,7 +14,39 @@
from matplotlib.patches import Rectangle
__doc__ = """
-TetQualityAnalysis module is a filter that performs an an analysis of tetrahedras of one or several meshes and plot a summary.
+TetQualityAnalysis module is a filter that performs an analysis of tetrahedras quality of one or several meshes and generates a plot as summary.
+
+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 ( ValueError, IndexError, TypeError, AttributeError ) as e:
+ tetQualityAnalysisFilter.logger.error( f"The filter { tetQualityAnalysisFilter.logger.name } failed due to: { e }" )
+ except Exception as e:
+ mess: str = f"The filter { meshQualityEnhancedFilter.logger.name } failed due to: { e }"
+ tetQualityAnalysisFilter.logger.critical( mess, exc_info=True )
"""
loggerName: str = "Tetrahedra Quality Analysis"
@@ -72,7 +104,6 @@ def applyFilter( self: Self ) -> None:
self.__loggerSection( "MESH COMPARISON DASHBOARD" )
for n, ( nfilename, mesh ) in enumerate( self.meshes.items(), 1 ):
- # print( n, nfilename )
coords = self.getCoordinatesDoublePrecision( mesh )
tetrahedraIds, tetrahedraConnectivity = self.extractTetConnectivity( mesh )
ntets = len( tetrahedraIds )
@@ -132,7 +163,6 @@ def applyFilter( self: Self ) -> None:
# # ==================== Print Distribution Statistics ====================
# Problem element counts
-
highAspectRatio = np.sum( aspectRatio > 100 )
lowShapeQuality = np.sum( shapeQuality < 0.3 )
lowFlatness = np.sum( flatnessRatio < 0.01 )
@@ -154,7 +184,6 @@ def applyFilter( self: Self ) -> None:
}
# 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
@@ -187,7 +216,6 @@ def applyFilter( self: Self ) -> None:
self.__orderMeshes()
- # Percentile information
self.printPercentileAnalysis()
self.printQualityIssueSummary()
@@ -199,6 +227,8 @@ def applyFilter( self: Self ) -> None:
self.computeDeltasFromBest()
self.createComparisonDashboard()
+ self.logger.info( f"The filter { self.logger.name } succeeded." )
+
def getCoordinatesDoublePrecision( self, mesh: vtkDataSet ) -> npt.NDArray[ np.float64 ]:
"""Get coordinates with double precision.
@@ -238,8 +268,8 @@ def extractTetConnectivity( self: Self,
for cellID in range( ncells ):
if mesh.GetCellType( cellID ) == vtk.VTK_TETRA:
cell = mesh.GetCell( cellID )
- point_ids = cell.GetPointIds()
- conn = [ point_ids.GetId( i ) for i in range( 4 ) ]
+ pointIds = cell.GetPointIds()
+ conn = [ pointIds.GetId( i ) for i in range( 4 ) ]
tetrahedraIds.append( cellID )
tetrahedraConnectivity.append( conn )
@@ -325,11 +355,11 @@ def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
npt.NDArray[ np.float64 ]: Dihedral angle
"""
- n1_norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
- n2_norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
- cos_angle = np.sum( n1_norm * n2_norm, axis=1 )
- cos_angle = np.clip( cos_angle, -1.0, 1.0 )
- angle = np.arccos( cos_angle ) * 180.0 / np.pi
+ n1Norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
+ n2Norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
+ cosAngle = np.sum( n1Norm * n2Norm, axis=1 )
+ cosAngle = np.clip( cosAngle, -1.0, 1.0 )
+ angle = np.arccos( cosAngle ) * 180.0 / np.pi
return angle
# Face normals
@@ -547,61 +577,62 @@ def computeDeltasFromBest( self: Self ) -> None:
valueBest = self.medians[ self.best ][ metric ]
self.deltas[ n ][ metric ] = ( ( value - valueBest ) / valueBest * 100 ) if valueBest > 0 else 0
- dtets = [
+ deltaTets = [
f"{self.deltas[ n ][ 'tetrahedra' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- dar = [
+ deltaAspectRatio = [
f"{self.deltas[ n ][ 'aspectRatio' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- dsq = [
+ deltaShapeQuality = [
f"{self.deltas[ n ][ 'shapeQuality' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- dvol = [
+ deltaVolume = [
f"{self.deltas[ n ][ 'volume' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- d_min_edge = [
+ deltaMinEdge = [
f"{self.deltas[ n ][ 'minEdge' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- d_maxEdge = [
+ deltaMaxEdge = [
f"{self.deltas[ n ][ 'maxEdge' ]:>+12,.1f}%" if n != self.best else ""
for n, _ in enumerate( self.meshes, 1 )
]
- d_edge_ratio = [
+ 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(dtets)}\n" +
- f"{' Aspect Ratio:':<20}{('').join( dar)}\n" +
- f"{' Shape Quality:':<20}{('').join( dsq)}\n" + f"{' Volume:':<20}{('').join( dvol)}\n" +
- f"{' Min Edge Length:':<20}{('').join( d_min_edge)}\n" +
- f"{' Max Edge Length:':<20}{('').join( d_maxEdge)}\n" +
- f"{' Edge Length Ratio:':<20}{('').join( d_edge_ratio)}\n" )
+ 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" )
def createComparisonDashboard( self: Self ) -> None:
"""Create the comparison dashboard."""
lbl = [ f'Mesh {n}' for n, _ in enumerate( self.meshes, 1 ) ]
# Determine smart plot limits
- ar_99 = []
+ ar99 = []
for n, _ in enumerate( self.meshes, 1 ):
- ar_99.append( np.percentile( self.validMetrics[ n ][ "aspectRatio" ], 99 ) )
+ ar99.append( np.percentile( self.validMetrics[ n ][ "aspectRatio" ], 99 ) )
- ar_99_max = np.max( np.array( ar_99 ) )
+ ar99Max = np.max( np.array( ar99 ) )
- if ar_99_max < 10:
- ar_plot_limit = 100
- elif ar_99_max < 100:
- ar_plot_limit = 1000
+ if ar99Max < 10:
+ arPlotLimit = 100
+ elif ar99Max < 100:
+ arPlotLimit = 1000
else:
- ar_plot_limit = 10000
+ arPlotLimit = 10000
# Set style
plt.rcParams[ 'figure.facecolor' ] = 'white'
@@ -618,7 +649,7 @@ def createComparisonDashboard( self: Self ) -> None:
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.96, bottom=0.84, wspace=0.20 )
+ 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,
@@ -626,16 +657,16 @@ def createComparisonDashboard( self: Self ) -> None:
figure=fig,
left=0.05,
right=0.95,
- top=0.82,
+ top=0.80,
bottom=0.05,
hspace=0.35,
wspace=0.30 )
# Title
suptitle = 'Mesh Quality Comparison Dashboard (Progressive Detail Layout)\n'
- for n, _ in enumerate( self.meshes, 1 ):
- # suptitle += f'Mesh {n}: {ntets:,} tets\t'
- suptitle += f'Mesh {n}: {self.tets[n]:<15} tets'
+ suptitle += (' - ').join( [ f'Mesh {n}: {self.tets[n]} tets ' for n,_ in enumerate( self.meshes, 1)] )
+ # for n, _ in enumerate( self.meshes, 1 ):
+ # suptitle += f'Mesh {n}: {self.tets[n]:<15} tets '
fig.suptitle( suptitle, fontsize=16, fontweight='bold', y=0.99 )
# Color scheme
@@ -666,7 +697,7 @@ def createComparisonDashboard( self: Self ) -> None:
# Add summary text #### ONLY BEST AND WORST MESH?
ax1.text(
0.98,
- 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' +
@@ -684,7 +715,11 @@ def createComparisonDashboard( self: Self ) -> None:
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.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 )
@@ -705,10 +740,10 @@ def createComparisonDashboard( self: Self ) -> None:
idx = self.sample[ n ]
- mask1_plot = aspectRatio[ idx ] < ar_plot_limit
+ mask1Plot = aspectRatio[ idx ] < arPlotLimit
- ax2.scatter( aspectRatio[ idx ][ mask1_plot ],
- shapeQuality[ idx ][ mask1_plot ],
+ ax2.scatter( aspectRatio[ idx ][ mask1Plot ],
+ shapeQuality[ idx ][ mask1Plot ],
alpha=0.4,
s=5,
color=color[ n - 1 ],
@@ -721,18 +756,18 @@ def createComparisonDashboard( self: Self ) -> None:
ax2.axvline( x=100, color='orange', linestyle='--', linewidth=2, alpha=0.8, label='High AR (> 100)', zorder=5 )
# Highlight problem zone
- problem_zone = Rectangle( ( 100, 0 ),
- ar_plot_limit - 100,
- 0.3,
- alpha=0.2,
- facecolor='red',
- edgecolor='none',
- zorder=0 )
- ax2.add_patch( 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 > ar_plot_limit )
+ np.sum( aspectRatio > arPlotLimit )
# Problem annotation
annotateIssues = ( '\n' ).join(
@@ -762,7 +797,7 @@ def createComparisonDashboard( self: Self ) -> None:
fontweight='bold',
color='darkred',
pad=10 )
- ax2.set_xlim( ( 1, ar_plot_limit ) )
+ 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 )
@@ -772,60 +807,60 @@ def createComparisonDashboard( self: Self ) -> None:
ax3.axis( 'off' )
summaryStats = []
- summaryStats.append( [ 'CRITICAL ISSUE', 'WORST', 'BEST', 'Change' ] )
+ 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" ]
- critical_combo2 = self.issues[ self.worst ][ "criticalCombo" ]
+ criticalCombo2 = self.issues[ self.worst ][ "criticalCombo" ]
aspectRatio = self.validMetrics[ self.best ][ "aspectRatio" ]
- ar2 = self.validMetrics[ self.worst ][ "aspectRatio" ]
+ aspectRatio2 = self.validMetrics[ self.worst ][ "aspectRatio" ]
highAspectRatio = self.issues[ self.best ][ "highAspectRatio" ]
- high_ar2 = self.issues[ self.worst ][ "highAspectRatio" ]
+ highAspectRatio2 = self.issues[ self.worst ][ "highAspectRatio" ]
lowShapeQuality = self.issues[ self.best ][ "lowShapeQuality" ]
- low_sq2 = self.issues[ self.worst ][ "lowShapeQuality" ]
+ lowShapeQuality2 = self.issues[ self.worst ][ "lowShapeQuality" ]
criticalMinDihedral = self.issues[ self.best ][ "criticalMinDihedral" ]
- critical_dih2 = self.issues[ self.worst ][ "criticalMinDihedral" ]
+ criticalMinDihedral2 = self.issues[ self.worst ][ "criticalMinDihedral" ]
criticalMaxDihedral = self.issues[ self.best ][ "criticalMaxDihedral" ]
- critical_max_dih2 = self.issues[ self.worst ][ "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'{critical_combo2:,}',
- f'{((critical_combo2-criticalCombo)/max(criticalCombo,1)*100):+.1f}%' if criticalCombo > 0 else 'N/A'
+ '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'({critical_combo2/len(ar2)*100:.2f}%)',
- ''
+ '(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'{high_ar2:,}',
- f'{((high_ar2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%'
+ 'AR > 100', f'{highAspectRatio:,}', f'{highAspectRatio2:,}',
+ f'{((highAspectRatio2-highAspectRatio)/max(highAspectRatio,1)*100):+.1f}%'
] )
summaryStats.append( [
- 'Quality < 0.3', f'{lowShapeQuality:,}', f'{low_sq2:,}',
- f'{((low_sq2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%'
+ 'Quality < 0.3', f'{lowShapeQuality:,}', f'{lowShapeQuality2:,}',
+ f'{((lowShapeQuality2-lowShapeQuality)/max(lowShapeQuality,1)*100):+.1f}%'
] )
summaryStats.append( [
- 'MinDih < 5°', f'{criticalMinDihedral:,}', f'{critical_dih2:,}',
- f'{((critical_dih2-criticalMinDihedral)/max(criticalMinDihedral,1)*100):+.1f}%'
+ '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'{critical_max_dih2:,}',
- f'{((critical_max_dih2-criticalMaxDihedral)/max(criticalMaxDihedral,1)*100):+.1f}%'
+ 'MaxDih > 175°', f'{criticalMaxDihedral:,}', f'{criticalMaxDihedral2:,}',
+ f'{((criticalMaxDihedral2-criticalMaxDihedral)/max(criticalMaxDihedral,1)*100):+.1f}%'
if criticalMaxDihedral > 0 else 'N/A'
] )
@@ -859,7 +894,7 @@ def createComparisonDashboard( self: Self ) -> None:
# Style header
for i in range( 4 ):
table[ ( 0, i ) ].set_facecolor( '#34495e' )
- table[ ( 0, i ) ].set_text_props( weight='bold', color='white', fontsize=9 )
+ table[ ( 0, i ) ].set_text_props( weight='bold', color='black', fontsize=9 )
# Highlight CRITICAL row
for col in range( 4 ):
@@ -869,9 +904,9 @@ def createComparisonDashboard( self: Self ) -> None:
# Color code changes
for row in [ 2, 6, 7, 8, 9, 10, 13, 14, 15 ]:
if row < len( summaryStats ):
- change_text = summaryStats[ row ][ 3 ]
- if '%' in change_text and change_text != 'N/A':
- val = float( change_text.replace( '%', '' ).replace( '+', '' ) )
+ 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
@@ -883,7 +918,11 @@ def createComparisonDashboard( self: Self ) -> None:
elif val < -10:
table[ ( row, 3 ) ].set_facecolor( '#fadbd8' )
- ax3.set_title( 'CRITICAL ISSUES SUMMARY', fontsize=12, fontweight='bold', color='darkgreen', pad=10 )
+ ax3.set_title( 'CRITICAL ISSUES SUMMARY',
+ fontsize=12,
+ fontweight='bold',
+ color='darkgreen',
+ pad=10 )
# ==================== ROW 2: QUALITY DISTRIBUTIONS ====================
@@ -907,12 +946,12 @@ def createComparisonDashboard( self: Self ) -> None:
# 5. Aspect Ratio Histogram
ax5 = fig.add_subplot( gs_main[ 0, 1 ] )
- ar_max = np.array( [ self.validMetrics[ n ][ "aspectRatio" ].max() for n, _ in enumerate( self.meshes, 1 ) ] )
+ arMax = np.array( [ self.validMetrics[ n ][ "aspectRatio" ].max() for n, _ in enumerate( self.meshes, 1 ) ] )
- bins = np.logspace( 0, np.log10( min( ar_plot_limit, ar_max.max() ) ), 40 ).tolist()
+ 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 < ar_plot_limit ],
+ ax5.hist( aspectRatio[ aspectRatio < arPlotLimit ],
bins=bins,
alpha=0.6,
label=f'Mesh {n}',
@@ -967,10 +1006,10 @@ def createComparisonDashboard( self: Self ) -> None:
# 8. Volume Histogram
ax8 = fig.add_subplot( gs_main[ 0, 4 ] )
- vol_min = np.array( [ self.validMetrics[ n ][ "volume" ].min() for n, _ in enumerate( self.meshes, 1 ) ] ).min()
- vol_max = np.array( [ self.validMetrics[ n ][ "volume" ].max() for n, _ in enumerate( self.meshes, 1 ) ] ).max()
+ 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( vol_min ), np.log10( vol_max ), 40 ).tolist()
+ 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,
@@ -992,7 +1031,7 @@ def createComparisonDashboard( self: Self ) -> None:
# 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, label=lbl, patch_artist=True, showfliers=False )
+ 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' )
@@ -1000,7 +1039,7 @@ def createComparisonDashboard( self: Self ) -> None:
# 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, label=lbl, patch_artist=True, showfliers=False )
+ 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' )
@@ -1009,15 +1048,15 @@ def createComparisonDashboard( self: Self ) -> None:
# 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, label=lbl, patch_artist=True, showfliers=False )
+ 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 ] )
- edge_ratio = [ self.validMetrics[ n ][ "edgeRatio" ] for n, _ in enumerate( self.meshes, 1 ) ]
- bp4 = ax12.boxplot( edge_ratio, label=lbl, patch_artist=True, showfliers=False )
+ 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' )
@@ -1026,7 +1065,7 @@ def createComparisonDashboard( self: Self ) -> None:
# 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, label=lbl, patch_artist=True, showfliers=False )
+ 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' )
@@ -1051,7 +1090,7 @@ def createComparisonDashboard( self: Self ) -> None:
idx = self.sample[ n ]
aspectRatio = self.validMetrics[ n ][ 'aspectRatio' ]
shapeQuality = self.validMetrics[ n ][ 'shapeQuality' ]
- mask1 = aspectRatio[ idx ] < ar_plot_limit
+ mask1 = aspectRatio[ idx ] < arPlotLimit
ax14.scatter( aspectRatio[ idx ][ mask1 ],
shapeQuality[ idx ][ mask1 ],
alpha=0.4,
@@ -1063,7 +1102,7 @@ def createComparisonDashboard( self: Self ) -> None:
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, ar_plot_limit ) )
+ ax14.set_xlim( ( 1, arPlotLimit ) )
ax14.set_ylim( ( 0, 1.05 ) )
ax14.legend( loc='upper right', fontsize=7 )
ax14.grid( True, alpha=0.3 )
@@ -1074,7 +1113,7 @@ def createComparisonDashboard( self: Self ) -> None:
idx = self.sample[ n ]
aspectRatio = self.validMetrics[ n ][ "aspectRatio" ]
flatnessRatio = self.validMetrics[ n ][ 'flatnessRatio' ]
- mask1 = aspectRatio[ idx ] < ar_plot_limit
+ mask1 = aspectRatio[ idx ] < arPlotLimit
ax15.scatter( aspectRatio[ idx ][ mask1 ],
flatnessRatio[ idx ][ mask1 ],
alpha=0.4,
@@ -1087,7 +1126,7 @@ def createComparisonDashboard( self: Self ) -> None:
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, ar_plot_limit ) )
+ ax15.set_xlim( ( 1, arPlotLimit ) )
ax15.legend( loc='upper right', fontsize=7 )
ax15.grid( True, alpha=0.3 )
@@ -1097,7 +1136,7 @@ def createComparisonDashboard( self: Self ) -> None:
idx = self.sample[ n ]
aspectRatio = self.validMetrics[ n ][ "aspectRatio" ]
volume = self.validMetrics[ n ][ 'volume' ]
- mask1 = aspectRatio[ idx ] < ar_plot_limit
+ mask1 = aspectRatio[ idx ] < arPlotLimit
ax16.scatter( volume[ idx ][ mask1 ],
aspectRatio[ idx ][ mask1 ],
alpha=0.4,
@@ -1110,7 +1149,7 @@ def createComparisonDashboard( self: Self ) -> None:
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, ar_plot_limit ) )
+ ax16.set_ylim( ( 1, arPlotLimit ) )
ax16.legend( loc='upper right', fontsize=7 )
ax16.grid( True, alpha=0.3 )
@@ -1139,9 +1178,9 @@ def createComparisonDashboard( self: Self ) -> None:
for n, _ in enumerate( self.meshes, 1 ):
idx = self.sample[ n ]
volume = self.validMetrics[ n ][ 'volume' ]
- edge_ratio = self.validMetrics[ n ][ 'edgeRatio' ]
+ edgeRatio = self.validMetrics[ n ][ 'edgeRatio' ]
ax18.scatter( volume[ idx ],
- edge_ratio[ idx ],
+ edgeRatio[ idx ],
alpha=0.4,
s=5,
color=color[ n - 1 ],
@@ -1160,12 +1199,12 @@ def createComparisonDashboard( self: Self ) -> None:
# 19. Min Edge Length Histogram
ax19 = fig.add_subplot( gs_main[ 3, 0 ] )
- edge_min = np.array( [ self.validMetrics[ n ][ "minEdge" ].min()
- for n, _ in enumerate( self.meshes, 1 ) ] ).min()
- edge_max_min = np.array( [ self.validMetrics[ n ][ "minEdge" ].max()
- for n, _ in enumerate( self.meshes, 1 ) ] ).min()
+ 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( edge_min ), np.log10( edge_max_min ), 40 ).tolist()
+ bins = np.logspace( np.log10( edgeMinMin ), np.log10( edgeMaxMin ), 40 ).tolist()
for n, _ in enumerate( self.meshes, 1 ):
minEdge = self.validMetrics[ n ][ 'minEdge' ]
@@ -1187,12 +1226,12 @@ def createComparisonDashboard( self: Self ) -> None:
# 20. Max Edge Length Histogram
ax20 = fig.add_subplot( gs_main[ 3, 1 ] )
- edge_max = np.array( [ self.validMetrics[ n ][ "maxEdge" ].max()
- for n, _ in enumerate( self.meshes, 1 ) ] ).max()
- edge_min_max = np.array( [ self.validMetrics[ n ][ "maxEdge" ].min()
- for n, _ in enumerate( self.meshes, 1 ) ] ).min()
+ 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( edge_min_max ), np.log10( edge_max ), 40 ).tolist()
+ 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,
@@ -1237,13 +1276,14 @@ def createComparisonDashboard( self: Self ) -> None:
] + [ 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, 1 ) ] + [ n for n, _ in enumerate( self.meshes, ) ]
- bp_dih = ax22.boxplot( dih,
- positions=positions,
- label=lbl_boxplot,
- patch_artist=True,
- showfliers=False,
- widths=0.6 )
+ 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" )
@@ -1273,12 +1313,16 @@ def createComparisonDashboard( self: Self ) -> None:
ax23.grid( True, alpha=0.3 )
# Save figure
- output_png = '/data/pau901/SIM_CS/04_WORKSPACE/USERS/PalomaMartinez/geosPythonPackages/TESST/mesh_comparison.png'
- print( f"\nSaving dashboard to: {output_png}" )
- plt.savefig( output_png, dpi=300, bbox_inches='tight', facecolor='white' )
- print( "Dashboard saved successfully!" )
- def setDashboardFilename( self: Self, filename: str )
+ plt.savefig( self.filename, dpi=300, bbox_inches='tight', facecolor='white' )
+ self.logger.info( f"Dashboard saved successfully: {self.filename}" )
+
+ def setDashboardFilename( self: Self, filename: str ) -> None:
+ """Set comparison dashboard output filename.
+
+ Args:
+ filename (str): Output filename.
+ """
self.filename = filename
def __loggerSection( self: Self, sectionName: str ) -> None:
@@ -1289,19 +1333,19 @@ def __loggerSection( self: Self, sectionName: str ) -> None:
def __orderMeshes( self: Self ) -> None:
"""Proposition of ordering as fonction of median quality score."""
self.__loggerSection( "ORDERING MESHES (from median quality score)" )
- median_score = {
+ medianScore = {
n: np.median( self.validMetrics[ n ][ "qualityScore" ] )
for n, _ in enumerate( self.meshes, 1 )
}
- sorted_meshes = sorted( median_score.items(), key=lambda x: x[ 1 ], reverse=True )
- self.sorted = sorted_meshes
- self.best = sorted_meshes[ 0 ][ 0 ]
- self.worst = sorted_meshes[ -1 ][ 0 ]
+ 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 sorted_meshes ]
- toprint = ( " > " ).join( top )
+ top = [ f"Mesh {m[0]} ({m[1]:.2f})" for m in sortedMeshes ]
+ toprint: str = ( " > " ).join( top )
self.logger.info( " [+] " + toprint + " [-]\n" )
def compareIssuesFromBest( self: Self ) -> None:
From 07af2676970afddea3fb2fb6f9eda779844db4ab Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Mon, 2 Feb 2026 11:47:38 +0100
Subject: [PATCH 06/11] Add corresponding plugin
---
.../pv/plugins/qc/PVTetQualityAnalysis.py | 114 ++++++++++++++++++
1 file changed, 114 insertions(+)
create mode 100644 geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
diff --git a/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
new file mode 100644
index 00000000..c3ea5660
--- /dev/null
+++ b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
@@ -0,0 +1,114 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
+# SPDX-FileContributor: Paloma Martinez
+# ruff: noqa: E402 # disable Module level import not at top of file
+import sys
+from pathlib import Path
+from typing_extensions import Self, Optional
+
+from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector
+from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid
+
+# update sys.path to load all GEOS Python Package dependencies
+geos_pv_path: Path = Path( __file__ ).parent.parent.parent.parent.parent.parent
+sys.path.insert( 0, str( geos_pv_path / "src" ) )
+from geos.pv.utils.config import update_paths
+
+update_paths()
+
+from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found]
+ VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy )
+# source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py
+from paraview.detail.loghandler import VTKHandler # type: ignore[import-not-found]
+# source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py
+
+from geos.processing.pre_processing.TetQualityAnalysis import TetQualityAnalysis
+from geos.pv.utils.details import FilterCategory
+
+__doc__ = f"""
+Tetrahedra QC is a ParaView plugin filter that analyze and compare the tetrahedras of two given vtkUnstructured grid datasets.
+
+A figure with relevant quality metrics is saved at the end of the process for a visual comparison.
+
+To use it:
+
+* Load the plugin in ParaView: Tools > Manage Plugins ... > Load New ... > .../geosPythonPackages/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis
+* Select the first dataset
+* Select the filter: Filters > { FilterCategory.QC.value } > Tetrahedras QC
+* In the dialog box, select the `mesh 1` and the `mesh 2`
+* Change the output filename if needed
+* Apply
+"""
+
+
+@smproxy.filter( name="PVTetQualityAnalysis", label="Tetrahedra QC" )
+@smhint.xml( f'' )
+@smproperty.input( name="inputMesh", port_index=0, label="mesh 1" )
+@smdomain.datatype(
+ dataTypes=[ "vtkUnstructuredGrid" ],
+ composite_data_supported=True,
+)
+@smproperty.input( name="inputMesh2", port_index=1, label="mesh 2" )
+@smdomain.datatype(
+ dataTypes=[ "vtkUnstructuredGrid" ],
+ composite_data_supported=True,
+)
+class PVTetQualityAnalysis( VTKPythonAlgorithmBase ):
+
+ def __init__( self: Self ) -> None:
+ """QC analysis of the tetrahedras from 2 meshes."""
+ super().__init__( nInputPorts=2, inputType="vtkObject" )
+
+ self._filename: Optional[ str ] = None
+ self._meshes: dict[ str, vtkUnstructuredGrid ] = {}
+ self._outputFilename: bool = True
+
+ @smproperty.stringvector( name="FilePath", label="File Path" )
+ @smdomain.xml( """
+
+ Output file path.
+
+
+
+
+ """ )
+ def SetFileName( self: Self, fname: str ) -> None:
+ """Specify filename for the output figure.
+
+ Args:
+ fname (str): File path
+ """
+ if self._filename != fname:
+ self._filename = fname
+ self.Modified()
+
+ def RequestData(
+ self: Self,
+ request: vtkInformation, #noqa: F841
+ inInfoVec: list[ vtkInformationVector ],
+ outInfoVec: vtkInformationVector,
+ ) -> int:
+ """Inherited from VTKPythonAlgorithmBase::RequestData.
+
+ Args:
+ request (vtkInformation): Request.
+ inInfoVec (list[vtkInformationVector]): Input objects.
+ outInfoVec (vtkInformationVector): Output objects.
+
+ Returns:
+ int: 1 if calculation successfully ended, 0 otherwise.
+ """
+ for n in range( 2 ):
+ self._meshes[ str( n ) ] = self.GetInputData( inInfoVec, n, 0 )
+
+ tetQualityAnalysisFilter: TetQualityAnalysis = TetQualityAnalysis( self._meshes, True )
+
+ if len( tetQualityAnalysisFilter.logger.handlers ) == 0:
+ tetQualityAnalysisFilter.setLoggerHandler( VTKHandler() )
+
+ try:
+ tetQualityAnalysisFilter.applyFilter()
+ except Exception as e:
+ tetQualityAnalysisFilter.logger.error( f' FATAL ERROR due to:\n{e}' )
+
+ return 1
From 37b4611b6318f869912140bc5a018716a5d55de6 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Mon, 2 Feb 2026 14:08:45 +0100
Subject: [PATCH 07/11] Adding a test
---
.../mesh/stats/tetrahedraAnalysisHelpers.py | 203 +
.../pre_processing/TetQualityAnalysis.py | 216 +-
geos-processing/tests/conftest.py | 4 +
geos-processing/tests/data/mesh1.vtu | 3534 +++++++++++++++++
geos-processing/tests/data/mesh1b.vtu | 2876 ++++++++++++++
.../tests/test_TetQualityAnalysis.py | 17 +
6 files changed, 6644 insertions(+), 206 deletions(-)
create mode 100644 geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
create mode 100755 geos-processing/tests/data/mesh1.vtu
create mode 100755 geos-processing/tests/data/mesh1b.vtu
create mode 100644 geos-processing/tests/test_TetQualityAnalysis.py
diff --git a/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py b/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
new file mode 100644
index 00000000..d040e6ad
--- /dev/null
+++ b/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
@@ -0,0 +1,203 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
+# SPDX-FileContributor: Bertrand Denel, Paloma Martinez
+import numpy as np
+import numpy.typing as npt
+from typing_extensions import Any
+
+from vtkmodules.vtkCommonDataModel import vtkDataSet, VTK_TETRA
+
+
+def getCoordinatesDoublePrecision( mesh: vtkDataSet ) -> npt.NDArray[ np.float64 ]:
+ """Get coordinates with double precision.
+
+ Args:
+ mesh (vtkDataSet): Input mesh.
+
+ Returns:
+ npt.NDArray[np.float64]: Coordinates
+
+ """
+ points = mesh.GetPoints()
+ npoints = points.GetNumberOfPoints()
+
+ coords = np.zeros( ( npoints, 3 ), dtype=np.float64 )
+ for i in range( npoints ):
+ point = points.GetPoint( i )
+ coords[ i ] = [ point[ 0 ], point[ 1 ], point[ 2 ] ]
+
+ return coords
+
+
+def extractTetConnectivity( mesh: vtkDataSet ) -> tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ] ]:
+ """Extract connectivity for all tetrahedra.
+
+ Args:
+ mesh (vtkDataSet): Mesh to analyze.
+
+ Returns:
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
+ Cell IDS corresponding to tetrahedra,
+ Connectivity of these cells
+ """
+ ncells = mesh.GetNumberOfCells()
+ tetrahedraIds = []
+ tetrahedraConnectivity = []
+
+ for cellID in range( ncells ):
+ if mesh.GetCellType( cellID ) == VTK_TETRA:
+ cell = mesh.GetCell( cellID )
+ pointIds = cell.GetPointIds()
+ conn = [ pointIds.GetId( i ) for i in range( 4 ) ]
+ tetrahedraIds.append( cellID )
+ tetrahedraConnectivity.append( conn )
+
+ return np.array( tetrahedraIds ), np.array( tetrahedraConnectivity )
+
+
+def analyzeAllTets( n: int, coords: npt.NDArray[ np.float64 ],
+ connectivity: npt.NDArray[ np.float64 ] ) -> dict[ str, dict[ str, Any ] ]:
+ """Vectorized analysis of all tetrahedra.
+
+ Args:
+ n (int): Mesh id.
+ coords (npt.NDArray[np.float64]): Tetrahedra coordinates.
+ connectivity (npt.NDArray[np.float64]): Connectivity.
+
+ Returns:
+ dict[str, dict[str, Any]]: Dictionary with keys 'volumes', 'aspectRatio', 'radiusRatio', 'flatnessRatio', 'shapeQuality', 'minEdge', 'maxEdge', 'minDihedral', 'maxDihedral', 'dihedralRange'
+ """
+ # Get coordinates for all vertices
+ v0 = coords[ connectivity[ :, 0 ] ]
+ v1 = coords[ connectivity[ :, 1 ] ]
+ v2 = coords[ connectivity[ :, 2 ] ]
+ v3 = coords[ connectivity[ :, 3 ] ]
+
+ # Compute edges
+ e01 = v1 - v0
+ e02 = v2 - v0
+ e03 = v3 - v0
+ e12 = v2 - v1
+ e13 = v3 - v1
+ e23 = v3 - v2
+
+ # Edge lengths
+ l01 = np.linalg.norm( e01, axis=1 )
+ l02 = np.linalg.norm( e02, axis=1 )
+ l03 = np.linalg.norm( e03, axis=1 )
+ l12 = np.linalg.norm( e12, axis=1 )
+ l13 = np.linalg.norm( e13, axis=1 )
+ l23 = np.linalg.norm( e23, axis=1 )
+
+ allEdges = np.stack( [ l01, l02, l03, l12, l13, l23 ], axis=1 )
+ minEdge = np.min( allEdges, axis=1 )
+ maxEdge = np.max( allEdges, axis=1 )
+
+ # Volumes
+ cross_product = np.cross( e01, e02 )
+ volumes = np.abs( np.sum( cross_product * e03, axis=1 ) / 6.0 )
+
+ # Face areas
+ face012 = 0.5 * np.linalg.norm( np.cross( e01, e02 ), axis=1 )
+ face013 = 0.5 * np.linalg.norm( np.cross( e01, e03 ), axis=1 )
+ face023 = 0.5 * np.linalg.norm( np.cross( e02, e03 ), axis=1 )
+ face123 = 0.5 * np.linalg.norm( np.cross( e12, e13 ), axis=1 )
+
+ allFaces = np.stack( [ face012, face013, face023, face123 ], axis=1 )
+ maxFaceArea = np.max( allFaces, axis=1 )
+ totalSurfaceArea = np.sum( allFaces, axis=1 )
+
+ # Aspect ratio
+ minAltitude = 3.0 * volumes / np.maximum( maxFaceArea, 1e-15 )
+ aspectRatio = maxEdge / np.maximum( minAltitude, 1e-15 )
+ aspectRatio[ volumes < 1e-15 ] = np.inf
+
+ # Radius ratio
+ inradius = 3.0 * volumes / np.maximum( totalSurfaceArea, 1e-15 )
+ circumradius = ( maxEdge**3 ) / np.maximum( 24.0 * volumes, 1e-15 )
+ radiusRatio = circumradius / np.maximum( inradius, 1e-15 )
+
+ # Flatness ratio
+ flatnessRatio = volumes / np.maximum( maxFaceArea * minEdge, 1e-15 )
+
+ # Shape quality
+ sumEdgeShapeQuality = np.sum( allEdges**2, axis=1 )
+ shapeQuality = 12.0 * ( 3.0 * volumes )**( 2.0 / 3.0 ) / np.maximum( sumEdgeShapeQuality, 1e-15 )
+ shapeQuality[ volumes < 1e-15 ] = 0.0
+
+ # Dihedral angles
+ def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
+ normal2: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
+ """Compute dihedral angle between two face normals.
+
+ Args:
+ normal1 (npt.NDArray[np.float64]): Normal vector 1
+ normal2 (npt.NDArray[np.float64]): Normal vector 2
+
+ Returns:
+ npt.NDArray[ np.float64 ]: Dihedral angle
+
+ """
+ n1Norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
+ n2Norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
+ cosAngle = np.sum( n1Norm * n2Norm, axis=1 )
+ cosAngle = np.clip( cosAngle, -1.0, 1.0 )
+ angle = np.arccos( cosAngle ) * 180.0 / np.pi
+ return angle
+
+ # Face normals
+ normal012: npt.NDArray[ np.float64 ] = np.cross( e01, e02 )
+ normal013: npt.NDArray[ np.float64 ] = np.cross( e01, e03 )
+ normal023: npt.NDArray[ np.float64 ] = np.cross( e02, e03 )
+ normal123: npt.NDArray[ np.float64 ] = np.cross( e12, e13 )
+
+ # Dihedral angles for each edge
+ dihedral01 = computeDihedralAngle( normal012, normal013 )
+ dihedral02 = computeDihedralAngle( normal012, normal023 )
+ dihedral03 = computeDihedralAngle( normal013, normal023 )
+ dihedral12 = computeDihedralAngle( normal012, normal123 )
+ dihedral13 = computeDihedralAngle( normal013, normal123 )
+ dihedral23 = computeDihedralAngle( normal023, normal123 )
+
+ allDihedrals = np.stack( [ dihedral01, dihedral02, dihedral03, dihedral12, dihedral13, dihedral23 ], axis=1 )
+
+ minDihedral = np.min( allDihedrals, axis=1 )
+ maxDihedral = np.max( allDihedrals, axis=1 )
+ dihedralRange = maxDihedral - minDihedral
+
+ return {
+ 'volumes': volumes,
+ 'aspectRatio': aspectRatio,
+ 'radiusRatio': radiusRatio,
+ 'flatnessRatio': flatnessRatio,
+ 'shapeQuality': shapeQuality,
+ 'minEdge': minEdge,
+ 'maxEdge': maxEdge,
+ 'minDihedral': minDihedral,
+ 'maxDihedral': maxDihedral,
+ 'dihedralRange': dihedralRange
+ }
+
+
+# Combined quality score
+def computeQualityScore( aspectRatio: npt.NDArray[ np.float64 ], shapeQuality: npt.NDArray[ np.float64 ],
+ edgeRatio: npt.NDArray[ np.float64 ],
+ minDihedralAngle: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
+ """Compute combined quality score (0-100).
+
+ Args:
+ aspectRatio(npt.NDArray[np.float64]): Aspect ratio
+ shapeQuality(npt.NDArray[np.float64]): Shape quality
+ edgeRatio(npt.NDArray[np.float64]): Edge ratio
+ minDihedralAngle(npt.NDArray[np.float64]): Minimal edge ratio
+
+ Returns:
+ np.float64: Quality score
+ """
+ aspectRatioNorm = np.clip( 1.0 / ( aspectRatio / 1.73 ), 0, 1 )
+ shapeQualityNorm = shapeQuality
+ edgeRatioNorm = np.clip( 1.0 / edgeRatio, 0, 1 )
+ dihedralMinNorm = np.clip( minDihedralAngle / 60.0, 0, 1 )
+ score = ( 0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm ) * 100
+
+ return score
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index 058ac62b..7203ff2c 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -4,15 +4,17 @@
import logging
import numpy as np
import numpy.typing as npt
-from typing_extensions import Self, Any #, Union
+from typing_extensions import Self, Any
from geos.utils.Logger import ( Logger, getLogger )
-import vtk
from vtkmodules.vtkCommonDataModel import vtkDataSet
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Rectangle
+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.
@@ -104,8 +106,8 @@ def applyFilter( self: Self ) -> None:
self.__loggerSection( "MESH COMPARISON DASHBOARD" )
for n, ( nfilename, mesh ) in enumerate( self.meshes.items(), 1 ):
- coords = self.getCoordinatesDoublePrecision( mesh )
- tetrahedraIds, tetrahedraConnectivity = self.extractTetConnectivity( mesh )
+ coords = getCoordinatesDoublePrecision( mesh )
+ tetrahedraIds, tetrahedraConnectivity = extractTetConnectivity( mesh )
ntets = len( tetrahedraIds )
self.logger.info( f" Mesh {n} info: \n" + f" Name: {nfilename}\n" +
@@ -113,7 +115,7 @@ def applyFilter( self: Self ) -> None:
f" Points: {mesh.GetNumberOfPoints()}" + "\n" + "-" * 80 + "\n" )
# Analyze both meshes
- self.analyzeAllTets( n, coords, tetrahedraConnectivity )
+ self.analyzedMesh[ n ] = analyzeAllTets( n, coords, tetrahedraConnectivity )
metrics = self.analyzedMesh[ n ]
self.tets[ n ] = ntets
@@ -229,172 +231,6 @@ def applyFilter( self: Self ) -> None:
self.logger.info( f"The filter { self.logger.name } succeeded." )
- def getCoordinatesDoublePrecision( self, mesh: vtkDataSet ) -> npt.NDArray[ np.float64 ]:
- """Get coordinates with double precision.
-
- Args:
- mesh (vtkDataSet): Input mesh.
-
- Returns:
- npt.NDArray[np.float64]: Coordinates
-
- """
- points = mesh.GetPoints()
- npoints = points.GetNumberOfPoints()
-
- coords = np.zeros( ( npoints, 3 ), dtype=np.float64 )
- for i in range( npoints ):
- point = points.GetPoint( i )
- coords[ i ] = [ point[ 0 ], point[ 1 ], point[ 2 ] ]
-
- return coords
-
- def extractTetConnectivity( self: Self,
- mesh: vtkDataSet ) -> tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ] ]:
- """Extract connectivity for all tetrahedra.
-
- Args:
- mesh (vtkDataSet): Mesh to analyze.
-
- Returns:
- tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
- Cell IDS corresponding to tetrahedra,
- Connectivity of these cells
- """
- ncells = mesh.GetNumberOfCells()
- tetrahedraIds = []
- tetrahedraConnectivity = []
-
- for cellID in range( ncells ):
- if mesh.GetCellType( cellID ) == vtk.VTK_TETRA:
- cell = mesh.GetCell( cellID )
- pointIds = cell.GetPointIds()
- conn = [ pointIds.GetId( i ) for i in range( 4 ) ]
- tetrahedraIds.append( cellID )
- tetrahedraConnectivity.append( conn )
-
- return np.array( tetrahedraIds ), np.array( tetrahedraConnectivity )
-
- def analyzeAllTets( self, n: int, coords: npt.NDArray[ np.float64 ],
- connectivity: npt.NDArray[ np.float64 ] ) -> None:
- """Vectorized analysis of all tetrahedra.
-
- Args:
- n (int): Mesh id.
- coords (npt.NDArray[np.float64]): Tetrahedra coordinates.
- connectivity (npt.NDArray[np.float64]): Connectivity.
- """
- # Get coordinates for all vertices
- v0 = coords[ connectivity[ :, 0 ] ]
- v1 = coords[ connectivity[ :, 1 ] ]
- v2 = coords[ connectivity[ :, 2 ] ]
- v3 = coords[ connectivity[ :, 3 ] ]
-
- # Compute edges
- e01 = v1 - v0
- e02 = v2 - v0
- e03 = v3 - v0
- e12 = v2 - v1
- e13 = v3 - v1
- e23 = v3 - v2
-
- # Edge lengths
- l01 = np.linalg.norm( e01, axis=1 )
- l02 = np.linalg.norm( e02, axis=1 )
- l03 = np.linalg.norm( e03, axis=1 )
- l12 = np.linalg.norm( e12, axis=1 )
- l13 = np.linalg.norm( e13, axis=1 )
- l23 = np.linalg.norm( e23, axis=1 )
-
- allEdges = np.stack( [ l01, l02, l03, l12, l13, l23 ], axis=1 )
- minEdge = np.min( allEdges, axis=1 )
- maxEdge = np.max( allEdges, axis=1 )
-
- # Volumes
- cross_product = np.cross( e01, e02 )
- volumes = np.abs( np.sum( cross_product * e03, axis=1 ) / 6.0 )
-
- # Face areas
- face012 = 0.5 * np.linalg.norm( np.cross( e01, e02 ), axis=1 )
- face013 = 0.5 * np.linalg.norm( np.cross( e01, e03 ), axis=1 )
- face023 = 0.5 * np.linalg.norm( np.cross( e02, e03 ), axis=1 )
- face123 = 0.5 * np.linalg.norm( np.cross( e12, e13 ), axis=1 )
-
- allFaces = np.stack( [ face012, face013, face023, face123 ], axis=1 )
- maxFaceArea = np.max( allFaces, axis=1 )
- totalSurfaceArea = np.sum( allFaces, axis=1 )
-
- # Aspect ratio
- minAltitude = 3.0 * volumes / np.maximum( maxFaceArea, 1e-15 )
- aspectRatio = maxEdge / np.maximum( minAltitude, 1e-15 )
- aspectRatio[ volumes < 1e-15 ] = np.inf
-
- # Radius ratio
- inradius = 3.0 * volumes / np.maximum( totalSurfaceArea, 1e-15 )
- circumradius = ( maxEdge**3 ) / np.maximum( 24.0 * volumes, 1e-15 )
- radiusRatio = circumradius / np.maximum( inradius, 1e-15 )
-
- # Flatness ratio
- flatnessRatio = volumes / np.maximum( maxFaceArea * minEdge, 1e-15 )
-
- # Shape quality
- sumEdgeShapeQuality = np.sum( allEdges**2, axis=1 )
- shapeQuality = 12.0 * ( 3.0 * volumes )**( 2.0 / 3.0 ) / np.maximum( sumEdgeShapeQuality, 1e-15 )
- shapeQuality[ volumes < 1e-15 ] = 0.0
-
- # Dihedral angles
- def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
- normal2: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
- """Compute dihedral angle between two face normals.
-
- Args:
- normal1 (npt.NDArray[np.float64]): Normal vector 1
- normal2 (npt.NDArray[np.float64]): Normal vector 2
-
- Returns:
- npt.NDArray[ np.float64 ]: Dihedral angle
-
- """
- n1Norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
- n2Norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
- cosAngle = np.sum( n1Norm * n2Norm, axis=1 )
- cosAngle = np.clip( cosAngle, -1.0, 1.0 )
- angle = np.arccos( cosAngle ) * 180.0 / np.pi
- return angle
-
- # Face normals
- normal012: npt.NDArray[ np.float64 ] = np.cross( e01, e02 )
- normal013: npt.NDArray[ np.float64 ] = np.cross( e01, e03 )
- normal023: npt.NDArray[ np.float64 ] = np.cross( e02, e03 )
- normal123: npt.NDArray[ np.float64 ] = np.cross( e12, e13 )
-
- # Dihedral angles for each edge
- dihedral01 = computeDihedralAngle( normal012, normal013 )
- dihedral02 = computeDihedralAngle( normal012, normal023 )
- dihedral03 = computeDihedralAngle( normal013, normal023 )
- dihedral12 = computeDihedralAngle( normal012, normal123 )
- dihedral13 = computeDihedralAngle( normal013, normal123 )
- dihedral23 = computeDihedralAngle( normal023, normal123 )
-
- allDihedrals = np.stack( [ dihedral01, dihedral02, dihedral03, dihedral12, dihedral13, dihedral23 ], axis=1 )
-
- minDihedral = np.min( allDihedrals, axis=1 )
- maxDihedral = np.max( allDihedrals, axis=1 )
- dihedralRange = maxDihedral - minDihedral
-
- self.analyzedMesh[ n ] = {
- 'volumes': volumes,
- 'aspectRatio': aspectRatio,
- 'radiusRatio': radiusRatio,
- 'flatnessRatio': flatnessRatio,
- 'shapeQuality': shapeQuality,
- 'minEdge': minEdge,
- 'maxEdge': maxEdge,
- 'minDihedral': minDihedral,
- 'maxDihedral': maxDihedral,
- 'dihedralRange': dihedralRange
- }
-
def printDistributionStatistics( self: Self ) -> None:
"""Print the distribution statistics for various metrics."""
self.__loggerSection( "DISTRIBUTION STATISTICS (MIN / MEDIAN / MAX)" )
@@ -664,7 +500,7 @@ def createComparisonDashboard( self: Self ) -> None:
# 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)] )
+ suptitle += ( ' - ' ).join( [ f'Mesh {n}: {self.tets[n]} tets ' for n, _ in enumerate( self.meshes, 1 ) ] )
# for n, _ in enumerate( self.meshes, 1 ):
# suptitle += f'Mesh {n}: {self.tets[n]:<15} tets '
fig.suptitle( suptitle, fontsize=16, fontweight='bold', y=0.99 )
@@ -715,11 +551,7 @@ def createComparisonDashboard( self: Self ) -> None:
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.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 )
@@ -918,11 +750,7 @@ def createComparisonDashboard( self: Self ) -> None:
elif val < -10:
table[ ( row, 3 ) ].set_facecolor( '#fadbd8' )
- ax3.set_title( 'CRITICAL ISSUES SUMMARY',
- fontsize=12,
- fontweight='bold',
- color='darkgreen',
- pad=10 )
+ ax3.set_title( 'CRITICAL ISSUES SUMMARY', fontsize=12, fontweight='bold', color='darkgreen', pad=10 )
# ==================== ROW 2: QUALITY DISTRIBUTIONS ====================
@@ -1403,27 +1231,3 @@ def setSampleForPlot( self: Self, data: npt.NDArray[ Any ], n: int ) -> None:
"""
sampleSize = min( 10000, len( data ) )
self.sample[ n ] = np.random.choice( len( data ), sampleSize, replace=False )
-
-
-# Combined quality score
-def computeQualityScore( aspectRatio: npt.NDArray[ np.float64 ], shapeQuality: npt.NDArray[ np.float64 ],
- edgeRatio: npt.NDArray[ np.float64 ],
- minDihedralAngle: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
- """Compute combined quality score (0-100).
-
- Args:
- aspectRatio(npt.NDArray[np.float64]): Aspect ratio
- shapeQuality(npt.NDArray[np.float64]): Shape quality
- edgeRatio(npt.NDArray[np.float64]): Edge ratio
- minDihedralAngle(npt.NDArray[np.float64]): Minimal edge ratio
-
- Returns:
- np.float64: Quality score
- """
- aspectRatioNorm = np.clip( 1.0 / ( aspectRatio / 1.73 ), 0, 1 )
- shapeQualityNorm = shapeQuality
- edgeRatioNorm = np.clip( 1.0 / edgeRatio, 0, 1 )
- dihedralMinNorm = np.clip( minDihedralAngle / 60.0, 0, 1 )
- score = ( 0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm ) * 100
-
- return score
diff --git a/geos-processing/tests/conftest.py b/geos-processing/tests/conftest.py
index 8a1767df..5da07469 100644
--- a/geos-processing/tests/conftest.py
+++ b/geos-processing/tests/conftest.py
@@ -46,6 +46,10 @@ def _get_dataset( datasetType: str ) -> Union[ vtkMultiBlockDataSet, vtkPolyData
vtkFilename = "data/domain_res5_id_empty.vtu"
elif datasetType == "meshGeosExtractBlockTmp":
vtkFilename = "data/meshGeosExtractBlockTmp.vtm"
+ elif datasetType == "meshtet1":
+ vtkFilename = "data/mesh1.vtu"
+ elif datasetType == "meshtet1b":
+ vtkFilename = "data/mesh1b.vtu"
datapath: str = os.path.join( os.path.dirname( os.path.realpath( __file__ ) ), vtkFilename )
reader.SetFileName( datapath )
diff --git a/geos-processing/tests/data/mesh1.vtu b/geos-processing/tests/data/mesh1.vtu
new file mode 100755
index 00000000..275fd492
--- /dev/null
+++ b/geos-processing/tests/data/mesh1.vtu
@@ -0,0 +1,3534 @@
+
+
+
+
+
+ 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0
+
+
+ 1
+2 1 3 1 4 1 1 2
+5 5 4 1 1 6 1 2
+5 1 2 2 5 3 2 2
+5 5 1 3 5 5 2 5
+5 7 2 3 4 5 8 8
+2 8 5 5 8 2 5 7
+7 5 7 5 5 7 8 2
+5 2 2 8 8 8 8 2
+5 8 1 5 5 5 2 3
+2 7 2 2 2 1 5 7
+8 5 8 3 2 1 1 2
+3 2 3 6 6 7 6 2
+6 2 2 2 2 2 1 3
+4 8 4 5 7 7 4 4
+3 1 1 2 5 3 3 5
+1 1 5 5 1 1 4 1
+1 4 1 4 1 4 1 8
+4 1 2 1 5 2 1 2
+1 5 2 1 2 5 1 2
+5 2 1 1 2 5 1 2
+5 1 2 1 1 2 1 2
+1 1 2 1 1 1 2 3
+1 2 1 5 1 2 1 2
+1 2 8 2 8 2 1 2
+2 1 6 3 1 1 6 1
+1 2 1 2 2 1 2 1
+1 1 1 1 8 8 1 6
+1 6 1 2 3 5 1 2
+1 1 2 5 5 1 5 1
+2 2 5 5 2 1 5 1
+2 1 1 2 1 1 2 6
+6 1 2 5 2 2 5 6
+5 4 2 2 5 5 5 2
+6 2 2 5 2 5 3 5
+5 5 7 7 6 3 2 3
+2 5 3 3 7 5 2 2
+3 5 3 3 5 2 2 7
+5 5 2 7 5 5 2 2
+3 2 5 5 3 2 3 2
+3 5 2 5 3 5 5 3
+2 3 3 3 2 2 5 3
+5 2 3 5 2 5 3 3
+2 3 5 3 2 3 2 2
+5 3 3 2 3 5 3 5
+3 2 2 3 2 3 3 2
+5 3 2 5 2 5 4 5
+7 2 7 2 5 5 7 2
+7 5 5 7 7 2 2 5
+7 2 5 7 2 5 7 2
+7 2 2 7 5 7 2 5
+7 5 7 5 7 2 2 7
+5 5 7 5 7 5 2 2
+5 2 5 7 6 5 5 2
+2 2 2 6 2 5 4 2
+5 2 5 5 2 8 8 5
+2 8 2 2 5 5 5 2
+2 8 2 2 2 8 5 2
+6 2 2 5 5 5 8 2
+2 8 5 5 2 8 5 8
+5 8 8 2 8 2 5 8
+2 5 2 8 2 2 8 2
+8 5 2 7 5 7 2 2
+7 8 8 2 8 5 2 5
+2 8 8 5 8 2 8 2
+8 5 8 5 8 2 8 5
+2 2 2 5 8 8 8 2
+2 8 2 5 5 7 2 8
+7 5 2 5 2 5 5 2
+2 7 5 7 2 7 5 2
+7 2 7 2 2 5 2 7
+2 2 7 5 5 5 7 2
+7 2 7 5 7 2 7 7
+2 5 7 5 2 7 5 2
+2 5 5 2 2 8 5 8
+5 8 2 8 2 8 5 8
+2 8 1 2 8 8 2 2
+2 5 2 7 8 2 2 2
+8 8 8 2 8 2 2 8
+2 2 2 8 6 2 7 8
+2 8 2 2 2 8 6 8
+2 2 8 2 8 2 2 2
+7 2 7 2 7 8 2 8
+8 2 8 2 7 8 7 8
+7 2 2 7 8 8 6 5
+5 5 5 1 5 1 8 8
+5 5 8 1 8 8 1 1
+4 8 4 8 8 5 5 5
+5 5 5 8 5 5 7 8
+8 5 3 2 3 3 5 7
+5 5 5 5 5 5 5 3
+3 7 2 5 5 3 5 3
+4 5 5 5 3 5 5 3
+2 5 5 1 4 3 3 3
+4 3 2 3 3 3 5 3
+2 2 7 7 2 2 3 6
+3 2 2 3 2 3 5 2
+5 2 2 3 2 2 3 3
+2 2 6 6 2 2 3 3
+3 3 3 3 1 2 3 3
+2 3 6 1 7 4 7 3
+7 5 7 5 5 7 5 7
+8 7 7 5 5 7 4 7
+5 7 5 7 5 7 5 5
+5 7 5 8 8 5 2 2
+7 7 6 3 3 7 7 2
+6 2 2 7 2 2 2 2
+7 7 7 7 7 8 8 6
+6 6 1 6 2 2 6 2
+2 2 2 3 6 2 6 2
+6 6 1 2 3 3 3 6
+3 3 3 6 2 7 7 2
+2 2 6 2 6 2 2 2
+2 2 6 7 6 6 6 6
+6 6 2 7 6 7 3 6
+2 6 6 8 6 8 2 6
+2 2 2 6 8 8 8 6
+6 2 6 6 6 8 6 2
+6 1 8 2 1 1 8 7
+6 3 4 3 4 3 1 1
+4 5 1 4 3 5 5 5
+5 1 1 5 4 4 1 4
+5 5 5 4 8 8 8 4
+8 4 4 4 8 8 8 4
+4 5 8 8 8 7 4 4
+5 4 4 5 5 7 7 7
+4 7 4 4 4 4 3 7
+4 4 7 7 4 7 7 4
+7 8 8 4 1 4 1 1
+1 1 1 3 5 4 5 4
+1 4 8 1 1 5 5 1
+8 1 5 1 1 2 1 1
+3 2 1 3 3 1 3 2
+2 1 2 1 1 1 1 1
+1 1 6 8 6 1 2 3
+3 2 5 1 2 2 1 2
+2 3 5 5 2 5 1 2
+5 2 5 5 1 5 2 6
+2 3 2 5 5 2 4 6
+3 5 2 2 5 2 5 5
+5 5 5 3 5 3 3 2
+3 2 3 3 5 3 5 5
+2 3 5 3 2 3 2 5
+3 3 3 5 3 2 5 3
+3 2 3 2 5 5 3 5
+2 3 2 5 2 2 1 2
+2 2 2 5 5 6 2 5
+3 2 3 2 3 3 3 5
+2 3 3 5 5 5 2 5
+7 7 5 2 7 7 5 5
+5 4 7 6 5 2 6 5
+8 2 8 2 2 2 8 5
+2 5 8 2 5 8 2 2
+2 8 5 2 2 5 5 5
+5 2 7 2 2 7 7 2
+5 5 5 7 7 2 7 2
+7 6 5 5 2 5 2 5
+2 5 5 2 8 2 2 5
+1 1 6 5 8 6 2 5
+5 4 5 2 1 8 6 1
+6 8 6 8 8 5 2 8
+5 8 5 8 4 8 4 4
+4 4 8 8 4 8 4 7
+8 4 8 8 8 3 3 3
+3 3 3 3 3 3 3 5
+3 3 3 3 3 4 3 5
+3 3 4 3 2 3 2 2
+6 3 6 2 3 3 3 6
+3 6 3 3 6 1 3 6
+3 3 3 4 4 5 7 5
+3 5 7 7 4 4 7 7
+7 5 5 5 5 7 8 7
+4 7 5 5 7 5 5 5
+7 7 3 7 2 2 7 7
+2 7 2 2 7 6 7 2
+6 7 2 7 7 7 7 6
+7 7 7 6 6 7 6 7
+6 6 6 6 6 3 6 6
+2 2 6 3 6 3 6 6
+6 6 2 6 6 6 6 6
+8 6 2 6 6 6 6 7
+7 7 7 4 1 4 3 4
+4 4 4 4 4 1 1 1
+1 4 5 8 8 8 8 4
+4 4 4 4 4 5 4 5
+4 4 4 4 3 7 7 4
+4 4 8 4 7 5 7 5
+3 7 1 3 1 1 2 5
+8 8 8 5 8 7 8 2
+5 5 5 5 5 5 5 5
+5 1 8 5 5 5 5 5
+5 5 5 5 1 1 3 1
+5 1 1 5 1 5 1 1
+1 5 1 1 5 1 5 1
+1 1 5 5 1 1 1 1
+5 1 1 1 5 1 1 2
+1 2 2 5 1 7 7 2
+7 5 7 5 2 1 5 5
+1 1 5 5 1 2 1 2
+2 1 2 1 1 2 5 1
+2 5 1 2 2 2 5 5
+1 5 5 1 2 2 5 2
+2 7 5 7 5 7 7 8
+7 2 2 5 5 5 5 8
+8 5 5 1 8 8 1 1
+8 8 8 8 7 8 8 7
+5 5 5 5 5 8 8 5
+5 5 5 3 1 1 1 3
+4 3 3 3 4 4 3 4
+4 7 7 5 5 7 5 8
+8 7 6 7 1 6 6 6
+6 2 2 6 6 2 4 7
+7 4 4 4 4 4 3 4
+3 3 4 4 3 4 4 3
+8 3 8 1 2 2 1 8
+8 8 4 5 5 2 2 6
+5 4 5 7 2 2 2 3
+6 7 3 7 5 5 1 5
+5 4 5 4 4 8 8 1
+6 1 2 2 2 2 6 6
+5 6 6 2 2 2 2 3
+2 2 7 7 5 5 4 5
+6 6 5 5 5 2 2 6
+2 8 2 2 6 2 2 5
+8 8 7 2 2 7 2 2
+1 5 5 4 4 4 4 4
+7 7 1 4 5 2 5 5
+5 2 5 1 2 2 5 5
+7 2 2 2 2 6 5 5
+3 4 4 4 8 7 4 1
+5 5 3 3 1 5 5 8
+8 8 8 5 5 5 5 8
+5 5 5 5 5 1 1 1
+5 3 5 3 3 5 5 7
+7 7 2 7 2 2 7 3
+3 8 8 8 5 8 1 1
+1 1 5 5 5 5 5 1
+5 3 5 5 5 8 8 8
+8 8 2 8 8 5 3 7
+3 7 7 7 2 2 2 2
+7 7 7 3 7 7 7 8
+8 7 7 2 5 5 1 5
+1 5 2 5 7 5 5 8
+1 5 5 7 5 8 2 7
+2 8 1 5 5 8 2 5
+5 1 5 7 2 2 3 8
+5 2 5 2
+1 1 2 2 2 1 2 1
+1 1 1 2 2 1 2 2
+1 2 2 1 2 1 2 1
+1 1 2 1 1 2 1 1
+2 2 1 1 2 1 1 2
+1 1 2 1 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 1 1 2 2 1
+1 2 2 1 2 1 2 2
+1 2 1 1 2 1 2 2
+2 2 1 1 2 2 1 1
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 2
+2 1 1 2 1 2 1 1
+2 2 1 2 2 1 2 1
+2 1 1 1 2 1 1 2
+1 2 1 2 2 1 1 1
+2 2 2 1 2 1 2 1
+2 1 2 1 2 1 1 2
+2 1 1 1 2 2 2 2
+1 2 2 1 1 1 1 1
+2 1 2 2 2 2 2 1
+1 1 1 2 2 1 2 1
+1 2 2 1 1 1 2 1
+2 1 2 2 2 2 2 2
+2 1 1 2 2 1 2 2
+1 1 2 1 1 2 2 2
+2 1 2 2 1 1 2 1
+2 1 2 2 1 2 2 2
+1 2 1 1 2 2 2 2
+1 2 1 2 2 1 2 1
+1 2 1 1 2 1 1 2
+2 2 1 1 2 1 1 2
+1 2 1 2 2 1 2 1
+2 2 2 2 1 1 2 2
+1 1 1 2 2 2 1 1
+1 2 1 2 2 1 1 2
+1 2 1 1 2 1 2 2
+2 2 2 2 1 1 2 1
+2 2 1 1 2 1 1 1
+2 2 2 2 2 1 1 1
+1 2 2 1 2 1 2 2
+2 1 2 2 2 2 1 1
+1 2 1 2 2 2 1 1
+1 2 1 1 2 2 1 2
+1 1 2 2 2 1 2 1
+1 2 1 1 2 2 2 1
+1 2 2 1 2 1 2 1
+1 2 2 1 1 2 1 2
+2 2 2 1 1 2 1 2
+2 2 1 1 2 2 1 1
+2 1 1 2 1 1 2 2
+2 1 2 1 2 2 2 2
+2 1 2 2 2 1 1 1
+2 2 1 2 2 2 1 1
+2 1 2 2 1 1 1 2
+2 1 1 2 2 2 1 1
+2 2 2 2 2 2 2 2
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 1 1 1 1 2
+1 1 1 1 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 1 2 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 2 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 2 1 2 2 2 2
+1 2 2 2 1 1 2 1
+2 2 1 2 1 2 1 2
+1 1 1 2 1 2 2 2
+1 2 1 1 2 2 1 2
+1 2 1 2 1 2 2 1
+1 2 1 1 1 2 1 1
+1 2 2 1 1 1 1 2
+1 2 2 1 2 2 2 1
+2 1 2 2 1 1 1 2
+2 1 2 2 1 1 1 2
+2 1 2 2 2 1 1 2
+2 1 2 2 1 1 2 1
+1 1 1 1 2 1 1 1
+2 1 1 2 2 1 1 1
+1 1 2 1 2 2 1 2
+2 2 2 2 2 2 2 1
+2 1 2 1 2 1 2 2
+2 1 2 2 1 1 1 1
+2 1 2 2 1 2 1 1
+1 1 2 2 1 1 1 2
+1 2 1 1 2 1 2 1
+2 1 2 1 1 2 2 2
+2 2 2 2 2 2 2 1
+1 2 2 1 2 2 2 2
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 2
+2 2 2 2 2 2 2 2
+1 2 1 1 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 1 2 2 2
+2 2 1 1 2 2 2 2
+2 1 1 1 2 2 2 1
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 1 2 1 2 1 1
+2 2 2 2 1 1 1 1
+1 2 2 2 1 1 2 1
+2 2 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 2 1 1 1 1 1 2
+2 2 2 1 2 2 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 1 2
+2 2 1 1 1 2 2 1
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 2
+1 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 1 1 1 2 1 1 1
+1 1 1 1 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 1 1 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 1
+1 2 1 1 2 2 1 1
+1 1 1 2 2 1 1 2
+2 2 1 1 1 1 2
+ 2
+2 1 1 1 1 2 2 2
+1 1 1 1 2 2 2 2
+1 2 2 2 1 1 2 2
+1 1 2 1 1 1 2 1
+1 1 2 2 1 1 2 2
+2 1 1 1 2 2 1 1
+1 1 2 1 1 1 1 2
+1 2 2 2 2 2 2 2
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 1 2 2 2
+1 1 2 2 2 1 2 2
+1 2 2 2 2 1 2 2
+1 2 2 1 2 2 2 2
+2 2 2 1 1 2 2 2
+2 2 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1 1 1 2
+1 2 2 1 1 1 1 2
+2 2 1 1 2 1 1 2
+2 1 1 2 2 1 2 2
+2 1 2 1 2 2 1 2
+1 1 2 2 1 1 1 2
+2 2 2 1 2 1 1 1
+1 1 2 2 2 2 2 2
+2 1 1 1 1 1 2 2
+2 1 2 1 1 2 2 1
+1 1 2 2 1 1 2 2
+2 2 1 1 1 2 2 2
+2 1 2 1 1 1 1 2
+2 2 2 1 2 2 1 1
+1 2 2 1 2 1 1 2
+2 2 1 1 2 2 2 2
+1 1 2 2 1 1 1 1
+2 2 2 2 2 2 2 2
+1 1 2 1 2 1 1 1
+1 2 2 2 1 1 1 2
+2 1 1 1 2 2 2 1
+1 2 1 2 2 1 1 2
+2 2 2 1 1 1 2 1
+1 1 1 1 2 2 2 1
+1 1 1 1 1 1 2 2
+1 2 1 2 2 1 1 2
+2 2 2 2 2 1 1 2
+1 2 1 1 2 1 1 1
+2 2 2 2 1 1 1 2
+2 1 2 2 2 2 1 2
+2 2 2 1 1 1 2 2
+2 2 1 1 2 2 1 1
+1 1 2 2 2 2 1 1
+2 1 2 2 2 2 2 2
+1 1 2 2 1 1 2 2
+1 1 2 2 1 1 2 1
+2 2 1 1 1 2 2 2
+2 1 1 1 1 2 2 1
+2 2 2 1 1 1 2 2
+2 2 2 1 1 2 2 2
+1 1 2 1 2 1 1 2
+2 2 1 1 2 2 1 2
+2 2 2 2 2 1 2 2
+2 2 2 1 1 1 1 2
+2 2 2 1 1 2 2 2
+2 1 1 1 2 2 1 2
+2 1 1 2 2 2 1 1
+1 1 2 2 2 2 1 1
+2 2 2 2 2 2 2 2
+2 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 1 1 1 1 1 1 1
+1 1 2 2 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 2
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 1
+2 2 1 2 2 2 2 2
+2 1 1 1 2 1 1 2
+1 2 1 1 1 1 2 2
+2 1 2 1 1 2 1 2
+1 1 2 2 1 2 1 1
+1 1 1 1 1 1 2 2
+2 2 1 1 1 1 1 1
+2 2 1 2 2 2 2 1
+1 2 2 1 2 2 1 1
+2 2 2 2 1 1 1 1
+2 2 2 1 2 2 1 2
+2 2 2 1 1 2 2 1
+1 2 2 2 2 1 1 1
+2 2 1 1 1 1 2 1
+1 1 1 2 1 1 1 1
+1 1 1 2 1 2 2 1
+2 2 2 2 2 2 2 1
+2 1 2 2 1 1 2 2
+2 2 1 2 2 1 1 1
+1 2 1 2 2 1 2 2
+1 1 1 1 2 2 1 2
+1 2 1 1 2 1 2 1
+2 1 1 2 2 2 2 1
+1 2 2 1 2 2 2 1
+1 1 1 2 2 2 2 2
+2 2 2 2 1 1 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 1
+1 2 2 2 2 2 2 1
+1 2 1 1 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 1 1 2 2 2
+1 1 1 1 2 2 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 1 2
+2 1 1 2 2 2 1 1
+1 1 1 2 2 2 1 2
+2 1 1 1 1 1 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 1
+2 2 1 1 1 2 2 2
+1 1 1 2 2 2 2 2
+2 2 2 2 1 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+1 2 2 2 2 2 2 2
+2 2 2 2 1 1 1 1
+2 2 1 1 1 2 2 2
+2 2 2 2 2 2 2 1
+1 1 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 2 1 1
+1 2 1 1 2 2 1 1
+1 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 1 1 1
+1 1 1 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 2 2 2 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 1 1 2 2 1
+2 2 1 1 1 1 2 1
+1 1 1 1 2 2 2 1
+1 2 1 2
+1 1 2 2 2 1 2 1
+1 1 1 2 2 1 2 2
+1 2 2 1 2 1 2 1
+1 1 2 1 1 2 1 1
+2 2 1 1 2 1 1 2
+1 1 2 1 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 1 1 2 2 1
+1 2 2 1 2 1 2 2
+1 2 1 1 2 1 2 2
+2 2 1 1 2 2 1 1
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 2
+2 1 1 2 1 2 1 1
+2 2 1 2 2 1 2 1
+2 1 1 1 2 1 1 2
+1 2 1 2 2 1 1 1
+2 2 2 1 2 1 2 1
+2 1 2 1 2 1 1 2
+2 1 1 1 2 2 2 2
+1 2 2 1 1 1 1 1
+2 1 2 2 2 2 2 1
+1 1 1 2 2 1 2 1
+1 2 2 1 1 1 2 1
+2 1 2 2 2 2 2 2
+2 1 1 2 2 1 2 2
+1 1 2 1 1 2 2 2
+2 1 2 2 1 1 2 1
+2 1 2 2 1 2 2 2
+1 2 1 1 2 2 2 2
+1 2 1 2 2 1 2 1
+1 2 1 1 2 1 1 2
+2 2 1 1 2 1 1 2
+1 2 1 2 2 1 2 1
+2 2 2 2 1 1 2 2
+1 1 1 2 2 2 1 1
+1 2 1 2 2 1 1 2
+1 2 1 1 2 1 2 2
+2 2 2 2 1 1 2 1
+2 2 1 1 2 1 1 1
+2 2 2 2 2 1 1 1
+1 2 2 1 2 1 2 2
+2 1 2 2 2 2 1 1
+1 2 1 2 2 2 1 1
+1 2 1 1 2 2 1 2
+1 1 2 2 2 1 2 1
+1 2 1 1 2 2 2 1
+1 2 2 1 2 1 2 1
+1 2 2 1 1 2 1 2
+2 2 2 1 1 2 1 2
+2 2 1 1 2 2 1 1
+2 1 1 2 1 1 2 2
+2 1 2 1 2 2 2 2
+2 1 2 2 2 1 1 1
+2 2 1 2 2 2 1 1
+2 1 2 2 1 1 1 2
+2 1 1 2 2 2 1 1
+2 2 2 2 2 2 2 2
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 1 1 1 1 2
+1 1 1 1 1 1 1 1
+1 1 2 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 1 2 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 2 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 2 1 2 2 2 2
+1 2 2 2 1 1 2 1
+2 2 1 2 1 2 1 2
+1 1 1 2 1 2 2 2
+1 2 1 1 2 2 1 2
+1 2 1 2 1 2 2 1
+1 2 1 1 1 2 1 1
+1 2 2 1 1 1 1 2
+1 2 2 1 2 2 2 1
+2 1 2 2 1 1 1 2
+2 1 2 2 1 1 1 2
+2 1 2 2 2 1 1 2
+2 1 2 2 1 1 2 1
+1 1 1 1 2 1 1 1
+2 1 1 2 2 1 1 1
+1 1 2 1 2 2 1 2
+2 2 2 2 2 2 2 1
+2 1 2 1 2 1 2 2
+2 1 2 2 1 1 1 1
+2 1 2 2 1 2 1 1
+1 1 2 2 1 1 1 2
+1 2 1 1 2 1 2 1
+2 1 2 1 1 2 2 2
+2 2 2 2 2 2 2 1
+1 2 2 1 2 2 2 2
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 2
+2 2 2 2 2 2 2 2
+1 2 1 1 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 1 2 2 2
+2 2 1 1 2 2 2 2
+2 1 1 1 2 2 2 1
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 1 2 1 2 1 1
+2 2 2 2 1 1 1 1
+1 2 2 2 1 1 2 1
+2 2 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 2 1 1 1 1 1 2
+2 2 2 1 2 2 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 1 2
+2 2 1 1 1 2 2 1
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 2
+1 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 1 1 1 2 1 1 1
+1 1 1 1 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 1 1 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 1
+1 2 1 1 2 2 1 1
+1 1 1 2 2 1 1 2
+2 2 1 1 1 1 2
+
+
+ 9 1.125 0 9 3.676895141601562 3.502532958984375
+9 2.25 0 9 1.125 3.375
+9 4.5 3.375 9 6.75 3.502532958984375
+3.331053018569946 0 3.502532958984375 3.435313701629639 0 3.502532958984375
+3.435313701629639 0.08947739005088806 3.502532958984375 3.375 0 3.502532958984375
+5.839126586914062 0 3.502532958984375 5.752532958984375 0 3.502532958984375
+6.779411315917969 0.08947733044624329 3.502532958984375 6.779411315917969 0 3.502532958984375
+6.877532958984375 0 3.502532958984375 7.615435600280762 0 3.502532958984375
+0 1.252532958984375 3.502532958984375 1.387778780781446e-17 1.446161985397339 3.502532958984375
+5.752532958984375 9 3.502532958984375 3.435313701629639 9 3.502532958984375
+3.502532958984375 9 3.502532958984375 2.377532958984375 9 3.502532958984375
+2.25 9 3.502532958984375 2.113669395446777 9 3.502532958984375
+0 6.877532958984375 3.502532958984375 0 4.5 3.502532958984375
+1.387778780781446e-17 4.394378662109375 3.502532958984375 6.779411315917969 9 3.502532958984375
+7.129815578460693 9 3.502532958984375 7.615435600280762 9 3.502532958984375
+6.75 9 3.375 6.75 9 3.502532958984375
+6.779411315917969 8.69927978515625 3.502532958984375 0 9 3.502532958984375
+1.387778780781446e-17 7.903513431549072 3.502532958984375 0.09121626615524292 7.981796264648438 3.502532958984375
+2.25 9 3.375 0 7.875 3.502532958984375
+1.387778780781446e-17 7.18602991104126 3.502532958984375 0 7.875 0
+0 9 0 1.125 9 1.125
+9 9 9 2.599289417266846 8.69927978515625 3.502532958984375
+0.9272406101226807 0 3.502532958984375 2.599289417266846 0.8069609999656677 3.502532958984375
+2.599289417266846 0 3.502532958984375 3.375 0 3.375
+3.502532958984375 0 3.502532958984375 5.943387031555176 0.08947736024856567 3.502532958984375
+5.943387031555176 0 3.502532958984375 4.271337985992432 0 3.502532958984375
+7.511175155639648 0 3.502532958984375 7.875 0 3.375
+6.75 0 3.375 0 6.75 3.502532958984375
+2.599289417266846 2.95941162109375 3.502532958984375 0 9 1.125
+0.09121626615524292 7.264312744140625 3.502532958984375 1.387778780781446e-17 7.264312744140625 3.502532958984375
+1.125 9 0 3.375 0 0
+4.5 0 1.125 3.375 1.125 0
+3.375 0 1.125 0 1.125 0
+0 0 0 1.125 2.25 0
+2.25 9 2.25 5.625 9 0
+5.107362747192383 6.546829223632812 3.502532958984375 5.943387031555176 5.111862182617188 3.502532958984375
+9 7.875 6.75 7.875 9 6.75
+7.875 5.625 9 9 9 7.875
+7.875 9 7.875 3.435313701629639 4.394378662109375 3.502532958984375
+0 1.125 9 0 5.625 9
+3.375 9 9 4.5 9 9
+3.375 9 7.875 9 1.125 9
+9 0 7.875 9 0 0
+9 0 1.125 7.875 0 0
+9 6.300107955932617 3.502532958984375 9 5.625 3.375
+9 5.625 1.125 7.875 9 0
+9 6.75 0 8.451459884643555 0.08947727084159851 3.502532958984375
+9 0.08947724848985672 3.502532958984375 9 2.241928100585938 3.502532958984375
+9 2.25 3.375 9 6.75 5.625
+3.375 0 2.25 5.943387031555176 1.524444460868835 3.502532958984375
+2.25 0 3.502532958984375 2.599289417266846 0.08947741985321045 3.502532958984375
+2.495028734207153 0 3.502532958984375 2.25 0 3.375
+2.377532958984375 0 3.502532958984375 4.167077541351318 0 3.502532958984375
+4.5 0 3.502532958984375 4.627532958984375 0 3.502532958984375
+4.5 0 2.25 4.271337985992432 0.08947736024856567 3.502532958984375
+5.625 0 3.375 5.107362747192383 0.08947736024856567 3.502532958984375
+5.625 0 3.502532958984375 4.5 0 3.375
+5.00310230255127 0 3.502532958984375 6.675150871276855 0 3.502532958984375
+6.75 0 3.502532958984375 9 3.375 6.75
+1.763265013694763 0 3.502532958984375 1.763265013694763 0.08947744965553284 3.502532958984375
+1.659004330635071 0 3.502532958984375 1.252532958984375 0 3.502532958984375
+1.125 0 3.375 1.125 0 3.502532958984375
+1.387778780781446e-17 0.8069610595703125 3.502532958984375 5.943387031555176 8.69927978515625 3.502532958984375
+5.943387031555176 7.981796264648438 3.502532958984375 1.763265013694763 8.69927978515625 3.502532958984375
+1.387778780781446e-17 3.598612546920776 3.502532958984375 0 3.502532958984375 3.502532958984375
+0 3.375 3.502532958984375 0.09121626615524292 3.676895141601562 3.502532958984375
+0 3.375 4.5 0 5.625 5.625
+2.25 4.5 0 3.435313701629639 5.829345703125 3.502532958984375
+4.271337985992432 7.264312744140625 3.502532958984375 1.763265013694763 6.546829223632812 3.502532958984375
+7.875 9 3.375 7.875 9 2.25
+6.75 5.625 9 9 7.875 0
+9 9 2.25 3.375 9 0
+3.375 7.875 0 3.375 9 3.502532958984375
+9 9 6.75 7.875 9 9
+6.75 9 4.5 6.877532958984375 9 3.502532958984375
+2.25 0 5.625 3.375 0 4.5
+6.75 0 7.875 5.107362747192383 1.524444460868835 3.502532958984375
+7.875 0 7.875 7.875 0 6.75
+7.875 0 9 9 0 9
+0 0 9 2.599289417266846 1.524444580078125 3.502532958984375
+0 0 2.25 0 1.125 1.125
+1.125 1.125 0 4.5 0 0
+7.875 1.125 0 9 0 2.25
+0 2.25 9 0.09121626615524292 1.524444580078125 3.502532958984375
+0 6.75 9 0 4.627532958984375 3.502532958984375
+0 9 9 0.09121626615524292 2.241928100585938 3.502532958984375
+0 1.125 4.5 1.387778780781446e-17 1.524444580078125 3.502532958984375
+0 1.125 3.502532958984375 0 0 1.125
+0 1.125 3.375 1.763265013694763 1.524444580078125 3.502532958984375
+0.09121626615524292 2.95941162109375 3.502532958984375 1.125 6.75 0
+0 7.875 3.375 0 2.25 0
+0 3.375 0 7.875 3.375 0
+9 3.375 1.125 9 3.375 0
+7.875 2.25 0 5.625 0 0
+6.75 0 0 6.75 0 1.125
+4.5 3.375 0 5.625 3.375 0
+2.25 0 0 1.125 0 1.125
+1.125 0 0 5.625 7.875 0
+6.75 7.875 0 6.75 9 0
+6.75 9 1.125 9 7.875 1.125
+9 9 0 9 9 1.125
+2.25 9 1.125 4.5 9 0
+2.25 9 0 0 6.75 0
+0 6.75 1.125 0 7.875 1.125
+2.25 7.875 0 0 5.625 0
+0 4.5 0 0 4.5 1.125
+9 2.25 9 6.75 0 6.75
+9 3.375 9 9 3.375 7.875
+7.875 3.375 9 9 4.5 9
+7.875 4.5 9 9 5.625 9
+5.625 4.5 9 9 6.75 9
+9 6.75 6.75 9 5.625 7.875
+9 7.875 9 7.875 7.875 9
+5.625 7.875 9 5.625 9 9
+6.75 9 9 4.5 9 6.75
+3.375 6.75 9 2.25 9 7.875
+2.25 7.875 9 3.375 4.5 9
+3.375 3.375 9 0 4.5 9
+0 9 7.875 2.25 9 9
+5.107362747192383 0 3.502532958984375 8.451459884643555 0 3.502532958984375
+9 0 3.375 2.949693918228149 9 3.502532958984375
+2.599289417266846 9 3.502532958984375 0 4.5 3.375
+9 9 3.502532958984375 9 0.8069608211517334 3.502532958984375
+8.347199440002441 0 3.502532958984375 9 3.502532958984375 3.502532958984375
+9 4.394378662109375 3.502532958984375 8.451459884643555 3.676895141601562 3.502532958984375
+9 3.375 3.502532958984375 9 5.582624435424805 3.502532958984375
+9 4.865140914916992 3.502532958984375 9 4.5 3.502532958984375
+9 5.111862182617188 3.502532958984375 8.451459884643555 4.394378662109375 3.502532958984375
+9 4.627532958984375 3.502532958984375 9 5.829345703125 3.502532958984375
+9 5.752532958984375 3.502532958984375 8.445431663518496 6.16289745090147 3.870839563648538
+9 5.625 3.502532958984375 8.771621932123496 1.16493775030033 3.583975486802759
+9 0.127532958984375 3.502532958984375 9 6.546829223632812 3.502532958984375
+0 6.75 3.375 1.387778780781446e-17 6.546829223632812 3.502532958984375
+6.779411315917969 2.95941162109375 3.502532958984375 8.519183657554317 3.658390299087706 3.785528965553484
+9 3.375 2.25 9 3.375 3.375
+9 3.430173635482788 3.502532958984375 9 4.14765739440918 3.502532958984375
+9 6.877532958984375 3.502532958984375 9 6.75 4.5
+8.533309684555668 7.11039571689918 3.803314178665166 7.615435600280762 6.546829223632812 3.502532958984375
+9 7.735074996948242 3.502532958984375 9 2.25 1.125
+8.451459884643555 2.95941162109375 3.502532958984375 7.615435600280762 2.241928100585938 3.502532958984375
+8.261755381287962 2.271645776458715 2.840930698522785 1.387778780781446e-17 6.468546390533447 3.502532958984375
+1.387778780781446e-17 5.829345703125 3.502532958984375 0.09121626615524292 6.546829223632812 3.502532958984375
+0 5.752532958984375 3.502532958984375 0 5.625 3.502532958984375
+0.4416207075119019 9 3.502532958984375 1.125 9 2.25
+5.625 9 3.502532958984375 6.2937912940979 9 3.502532958984375
+9 7.875 5.625 9 9 5.625
+7.875 9 5.625 6.75 9 5.625
+0.127532958984375 9 3.502532958984375 2.599289417266846 5.829345703125 3.502532958984375
+9 1.125 6.75 9 0 6.75
+6.75 3.375 9 5.625 0 5.625
+4.5 0 4.5 5.625 0 4.5
+5.943387031555176 0.8069609403610229 3.502532958984375 7.875 0 4.5
+1.125 0 9 2.25 0 7.875
+1.125 1.125 9 1.125 2.25 9
+0 2.25 6.75 9 0 4.5
+0 5.625 4.5 0 7.875 5.625
+0 4.5 4.5 1.125 3.375 0
+7.875 5.625 0 6.75 5.625 0
+7.875 6.75 0 9 4.5 0
+6.779411315917969 3.676895141601562 3.502532958984375 0 6.75 7.875
+1.125 7.875 9 0 7.875 9
+2.25 4.5 9 3.375 0 9
+2.25 0 9 4.5 0 7.875
+5.625 0 7.875 6.75 0 9
+6.75 2.25 9 6.535866364768601 1.031474222162056 8.278649107442352
+5.625 0 9 5.625 1.125 9
+7.875 0 5.625 9 0 5.625
+6.75 1.125 9 9 6.75 3.375
+3.375 0 7.875 4.5 1.125 9
+9 6.75 1.125 9 5.625 0
+9 7.264312744140625 3.502532958984375 8.582412628123429 7.094355755594355 3.190321599853458
+9 7.01759147644043 3.502532958984375 8.451459884643555 6.546829223632812 3.502532958984375
+4.5 9 5.625 0 9 2.25
+1.125 9 3.375 1.125 3.375 9
+0 3.375 9 1.125 4.5 9
+9 8.69927978515625 3.502532958984375 8.451459884643555 7.981796264648438 3.502532958984375
+1.763265013694763 0.8069610595703125 3.502532958984375 3.435313701629639 0.8069609403610229 3.502532958984375
+1.763265013694763 2.241928100585938 3.502532958984375 0 4.5 2.25
+0 2.25 2.25 1.125 0 2.25
+4.5 2.25 0 0 1.125 2.25
+9 4.5 7.875 4.5 2.25 9
+5.625 9 1.125 7.875 7.875 0
+7.875 9 1.125 6.75 1.125 0
+4.5 1.125 0 7.875 0 1.125
+7.875 0 2.25 9 1.125 2.25
+9 1.125 1.125 9 2.25 2.25
+6.779411315917969 6.546829223632812 3.502532958984375 9 5.625 2.25
+8.451459884643555 5.111862182617188 3.502532958984375 9 4.5 1.125
+4.5 5.625 0 5.625 4.5 0
+6.75 3.375 0 5.625 9 5.625
+5.943387031555176 2.95941162109375 3.502532958984375 4.271337985992432 2.95941162109375 3.502532958984375
+2.25 2.25 0 0.9272406101226807 2.241928100585938 3.502532958984375
+0 2.25 4.5 2.25 0 4.5
+1.125 0 4.5 2.25 3.375 0
+0 2.25 1.125 1.763265013694763 4.394378662109375 3.502532958984375
+1.234891575158559 4.270146731458665 2.019959804183972 0.09121626615524292 4.394378662109375 3.502532958984375
+0.9272406101226807 3.676895141601562 3.502532958984375 0 3.375 2.25
+1.387778780781446e-17 3.676895141601562 3.502532958984375 1.387778780781446e-17 4.31609582901001 3.502532958984375
+0 3.375 3.375 1.763265013694763 3.676895141601562 3.502532958984375
+2.599289417266846 3.676895141601562 3.502532958984375 0 3.375 5.625
+3.375 5.625 9 4.271337985992432 5.829345703125 3.502532958984375
+4.271337985992432 5.111862182617188 3.502532958984375 5.570656843395302 4.690803812530331 1.956367174397994
+3.375 5.625 0 5.943387031555176 6.546829223632812 3.502532958984375
+6.75 9 6.75 7.615435600280762 7.981796264648438 3.502532958984375
+6.779411315917969 7.264312744140625 3.502532958984375 6.75 9 2.25
+6.779411315917969 7.981796264648438 3.502532958984375 1.125 7.875 0
+3.375 9 2.25 3.375 9 1.125
+0 7.875 2.25 2.25 5.625 0
+2.599289417266846 6.546829223632812 3.502532958984375 0 6.75 4.5
+0 6.75 5.625 0.6341115187101333 0.4081168195189446 3.008036268305967
+0.09121626615524292 0.8069610595703125 3.502532958984375 3.375 2.25 9
+0 2.25 5.625 5.652656033953676 6.072963813757366 2.300632961303739
+5.625 6.75 0 5.625 5.625 0
+4.5 4.5 0 4.5 7.875 0
+4.5 9 1.125 2.599289417266846 4.394378662109375 3.502532958984375
+6.75 4.5 0 7.875 4.5 0
+7.24138845956492 7.439566501025292 1.859793888676632 6.75 6.75 0
+9 6.75 2.25 7.615435600280762 5.829345703125 3.502532958984375
+9 4.5 2.25 7.540124246545455 5.833683665487454 1.694155427117124
+7.312412606938891 4.791222850138663 2.487254913677085 6.779411315917969 5.829345703125 3.502532958984375
+5.107362747192383 3.676895141601562 3.502532958984375 5.625 2.25 9
+4.5 4.5 9 4.5 0 6.75
+5.107362747192383 5.111862182617188 3.502532958984375 4.271337985992432 3.676895141601562 3.502532958984375
+5.107362747192383 4.394378662109375 3.502532958984375 2.145949359780621 4.640737129168067 4.563727106778785
+3.435313701629639 3.676895141601562 3.502532958984375 6.75 4.5 9
+5.625 0 6.75 3.716024459182148 4.364852473586002 4.620851120490612
+6.75 9 7.875 5.625 9 7.875
+5.625 5.625 9 5.625 6.75 9
+6.75 7.875 9 4.5 5.625 9
+4.5 3.375 9 0 4.5 6.75
+0 5.625 7.875 7.615435600280762 1.524444460868835 3.502532958984375
+7.875 1.125 9 9 2.25 6.75
+9 1.125 5.625 9 2.25 7.875
+5.625 3.375 9 9 1.125 7.875
+7.875 2.25 9 6.469315394224849 3.256591989182156 2.159056020176944
+6.75 2.25 0 6.75 0 2.25
+7.615435600280762 7.264312744140625 3.502532958984375 8.505535885853932 5.392135845875329 3.768442318649113
+9 5.625 4.5 7.615435600280762 5.111862182617188 3.502532958984375
+7.78380200757167 7.698391316925724 5.297583608402414 5.943387031555176 2.241928100585938 3.502532958984375
+7.548208697340079 1.627598208484745 2.144860817038661 4.832298333528852 3.103551019751242 2.067974083506454
+5.943387031555176 4.394378662109375 3.502532958984375 7.749441149564372 3.388593793343309 1.614448776063265
+7.615435600280762 2.95941162109375 3.502532958984375 6.779411315917969 2.241928100585938 3.502532958984375
+6.234117985484739 1.280826335617317 2.300632858321647 5.107362747192383 2.95941162109375 3.502532958984375
+8.015458357290976 2.901690231036569 4.444367511077555 9 2.25 5.625
+6.779411315917969 4.394378662109375 3.502532958984375 5.625 0 2.25
+4.271337985992432 2.241928100585938 3.502532958984375 3.435313701629639 1.524444580078125 3.502532958984375
+5.625 0 1.125 5.625 1.125 0
+5.625 2.25 0 3.375 2.25 0
+4.562069121455412 1.280826346234248 2.300633251629598 4.271337985992432 1.524444460868835 3.502532958984375
+4.271337985992432 0.8069609403610229 3.502532958984375 3.435313701629639 2.241928100585938 3.502532958984375
+3.09786030852054 1.262175402157083 4.494759286428109 0 4.5 5.625
+0 5.625 6.75 1.472837606920996 5.95254532036558 5.010406437526791
+0 4.5 7.875 0 2.25 7.875
+1.125 4.5 0 0 5.625 1.125
+3.375 3.375 0 3.375 6.75 0
+2.25 6.75 0 1.763265013694763 5.829345703125 3.502532958984375
+3.435313701629639 7.264312744140625 3.502532958984375 5.409500735588122 7.771348996305957 2.206179291281297
+4.487983553849537 8.887802347509504 3.202845724663746 2.599289417266846 7.264312744140625 3.502532958984375
+4.5 6.75 0 3.435313701629639 2.95941162109375 3.502532958984375
+4.111321221595106 6.013678778404893 1.555986976041781 3.435313701629639 5.111862182617188 3.502532958984375
+2.563049794432179 5.161253219873943 1.521905225450433 3.726044699231141 4.150760551477011 2.300632961303742
+3.375 4.5 0 4.271337985992432 4.394378662109375 3.502532958984375
+2.599289417266846 7.981796264648438 3.502532958984375 3.435313701629639 6.546829223632812 3.502532958984375
+3.375 0 5.625 4.5 0 5.625
+5.05478067801972 2.506758493261287 6.989306652459141 2.610042479961499 4.978286781297337 7.278883662556543
+5.522438851330582 3.979699693014349 4.847427077260235 5.107362747192383 0.8069609403610229 3.502532958984375
+7.136783789876119 1.731430218232272 6.363216210123881 5.943387031555176 3.676895141601562 3.502532958984375
+4.585961962835035 1.99339818105492 4.495363411725839 0.3311731019124124 0.8190698474982007 3.033489568834056
+0.9272406101226807 1.524444580078125 3.502532958984375 3.192215233364269 1.842564406232126 2.176648318446237
+2.599289417266846 2.241928100585938 3.502532958984375 2.25 0 2.25
+2.25 0 1.125 1.763265013694763 2.95941162109375 3.502532958984375
+0.9272406101226807 2.95941162109375 3.502532958984375 1.38894815027959 1.66324515253998 2.156772122970167
+2.25 1.125 0 2.308558408540556 3.203029744097147 2.300632884139985
+1.256314575717081 2.966460140175106 1.119492520611655 0 3.375 1.125
+2.694808836036466 6.241946655394024 2.575108844635929 1.851736152990676 7.595133254446319 1.805800375880526
+1.763265013694763 7.264312744140625 3.502532958984375 2.599289417266846 5.111862182617188 3.502532958984375
+1.763265013694763 5.111862182617188 3.502532958984375 9 5.625 5.625
+6.75 6.75 9 7.875 6.75 9
+9 6.75 7.875 7.315446459078533 4.917459855116865 5.339675597850087
+7.274804005161163 6.363062629448739 7.13693737055126 6.980598168991299 6.11552589269467 4.442614263256035
+9 7.875 7.875 2.25 6.75 9
+3.375 7.875 9 3.375 9 6.75
+5.314790745455006 7.15165552374292 7.370258274397591 2.375795880709986 7.233954914050975 7.358464790762127
+0 7.875 7.875 0 7.875 6.75
+0 6.75 6.75 1.125 6.75 9
+1.125 5.625 9 4.5 7.875 9
+4.5 6.75 9 1.125 9 7.875
+4.113289006797724 1.734449198438731 6.011710993202275 2.107019961617235 2.933641275326702 4.61554225628983
+3.980607083587111 3.203029769641998 4.704432701776389 5.275791311326738 4.73830991268275 7.175875927778296
+0 3.375 7.875 9 3.375 5.625
+6.779411315917969 5.111862182617188 3.502532958984375 9 4.5 6.75
+7.312217130128784 3.870124452530884 7.50507506331641 9 5.625 6.75
+7.615435600280762 3.676895141601562 3.502532958984375 7.615435600280762 4.394378662109375 3.502532958984375
+6.65745099357092 3.261729417021645 4.675278155804871 9 4.5 4.5
+5.693949807974339 6.206261506936472 4.906742171244713 5.625 9 6.75
+3.375 9 5.625 4.5 9 7.875
+6.779411315917969 1.524444460868835 3.502532958984375 6.008214242936078 1.503181611579868 5.226524556208743
+6.779411315917969 0.8069609403610229 3.502532958984375 6.75 0 4.5
+9 2.25 3.502532958984375 9 2.377532958984375 3.502532958984375
+9 4.5 5.625 1.125 9 9
+5.107362747192383 2.241928100585938 3.502532958984375 3.375 0 6.75
+4.5 0 9 0 6.75 2.25
+1.260904734186886 6.134081558659342 2.18616894181551 0.9272406101226807 6.546829223632812 3.502532958984375
+3.375 1.125 9 2.25 2.25 9
+1.125 5.625 0 9 7.875 3.375
+9 7.875 2.25 8.462937302221281 8.557621953065443 3.167286470670322
+9 9 3.375 5.943387031555176 7.264312744140625 3.502532958984375
+6.433553026697342 7.575023136319463 4.744387111067645 5.943387031555176 9 3.502532958984375
+5.107362747192383 7.264312744140625 3.502532958984375 4.271337985992432 6.546829223632812 3.502532958984375
+3.937565055049622 5.904087670145881 4.765581356328397 5.457767009735107 9 3.502532958984375
+4.689730494585711 7.817902636744186 5.166346028825599 3.375 9 4.5
+2.25 9 4.5 1.109502727159427 8.373965148437145 4.113808435684165
+1.763265013694763 7.981796264648438 3.502532958984375 9 2.25 4.5
+9 1.277722835540771 3.502532958984375 9 1.252532958984375 3.502532958984375
+9 1.524444341659546 3.502532958984375 9 1.125 4.5
+9 0 3.502532958984375 7.875 0 3.502532958984375
+8.002532958984375 0 3.502532958984375 0.03072597177569709 2.250724164575014 3.469908609627758
+1.387778780781446e-17 2.241928100585938 3.502532958984375 1.387778780781446e-17 2.163645505905151 3.502532958984375
+0 2.25 3.502532958984375 0.03439940795526743 0.02997382427664288 3.474233252044904
+1.863216210123879 1.731430218232269 6.01178378987612 0.995617281480054 4.055202619780672 8.245401716974937
+0 3.375 6.75 1.385131889807029 1.985749811036993 7.739994758686955
+2.25 0 6.75 2.988396128990416 2.636603871009582 7.261033368166233
+2.56651708797859 0.9355909882200921 8.151614700913811 1.125 0 6.75
+1.452478361892485 3.581769269491715 6.441386636033688 2.25 3.375 9
+2.25 1.125 9 8.002532958984375 9 3.502532958984375
+9 9 4.5 7.965839862823486 9 3.502532958984375
+8.801864624023438 9 3.502532958984375 9 8.002532958984375 3.502532958984375
+9 7.981796264648438 3.502532958984375 9 8.452558517456055 3.502532958984375
+9 7.875 3.502532958984375 0.9272406101226807 0.8069610595703125 3.502532958984375
+0.09121626615524292 0.08947750926017761 3.502532958984375 0.9272406101226807 0.08947747945785522 3.502532958984375
+7.615435600280762 0.8069608807563782 3.502532958984375 8.451459884643555 0.8069608211517334 3.502532958984375
+3.547844060432746 7.534800294998534 4.480512350882099 4.627532958984375 9 3.502532958984375
+4.584130752766058 8.948389833069077 3.533276607765899 4.621742725372314 9 3.502532958984375
+4.5 9 3.502532958984375 5.625 9 4.5
+4.296065256666489 8.441996671123004 4.024779935732092 4.271337985992432 9 3.502532958984375
+4.5 9 4.5 3.435313701629639 7.981796264648438 3.502532958984375
+3.435313701629639 8.69927978515625 3.502532958984375 3.375 9 3.375
+3.785718202590942 9 3.502532958984375 4.613004691374806 8.813180763514939 3.718947597288265
+4.271337985992432 7.981796264648438 3.502532958984375 5.107362747192383 7.981796264648438 3.502532958984375
+5.356343154558674 8.173222732405279 4.080434143257682 8.451459884643555 5.829345703125 3.502532958984375
+2.25 5.625 9 0.127532958984375 0 3.502532958984375
+0 0 3.502532958984375 0.09121626615524292 0 3.502532958984375
+0 0 4.5 1.387778780781446e-17 0.01119490247219801 3.502532958984375
+0 9 3.375 0.3086500621928674 8.38996734917518 3.240383120107055
+0.9204625400924104 8.463648303043309 2.888070878058432 0.254614219069482 2.885955810546874 3.438766479492187
+0 2.25 3.375 1.387778780781446e-17 2.95941162109375 3.502532958984375
+0 2.377532958984375 3.502532958984375 1.387778780781446e-17 2.881129026412964 3.502532958984375
+0 0 5.625 0 1.125 6.75
+0.5625000000000007 0.843750000000001 6.468749999999999 0 1.125 5.625
+1.125 0 5.625 1.31595357436064 1.304732392090907 4.637354894735387
+0 1.125 7.875 1.125 0 7.875
+0 0 6.75 0 0 7.875
+0 9 6.75 0 9 4.5
+0 9 5.625 1.125 9 6.75
+1.125 9 5.625 2.25 9 6.75
+2.25 9 5.625 1.387778780781446e-17 5.033579349517822 3.502532958984375
+1.387778780781446e-17 5.111862182617188 3.502532958984375 0.4143398542404136 4.01004000895747 3.097874028629212
+5.943387031555176 5.829345703125 3.502532958984375 5.107362747192383 5.829345703125 3.502532958984375
+6.159829514405706 4.981507835244013 4.157082457936593 0.396505242021744 6.011797294217433 3.083050123384267
+0.09686421602964419 0.02796172164380645 3.564874649047851 1.387778780781446e-17 0.08947750926017761 3.502532958984375
+0.1636355900417819 0.1358352580968225 3.615262784876803 8.635027843850425 1.47525722106522 3.828303070257911
+9 1.995206475257874 3.502532958984375 0.9045640952757059 7.966293326802795 5.710505735655847
+1.789226390679782 7.723482906847655 4.726330114944425 9 3.375 4.5
+9 2.712690114974976 3.502532958984375 9 2.95941162109375 3.502532958984375
+0.09121626615524292 8.69927978515625 3.502532958984375 4.271337985992432 8.69927978515625 3.502532958984375
+1.387778780781446e-17 7.981796264648438 3.502532958984375 0 8.002532958984375 3.502532958984375
+1.387778780781446e-17 8.69927978515625 3.502532958984375 0.09121626615524292 9 3.502532958984375
+9 1.125 3.502532958984375 0 0.127532958984375 3.502532958984375
+1.387778780781446e-17 0.7286784648895264 3.502532958984375 0.8229798674583435 0 3.502532958984375
+0.4664618779822396 0.3364242922231262 3.89111057249061 0.9272406101226807 7.264312744140625 3.502532958984375
+0 7.875 4.5 4.5 9 3.375
+5.625 9 3.375 4.5 9 2.25
+5.625 9 2.25 7.33887533910787 1.497988513796702 4.561518098262509
+8.156340718371691 0.9281638603494392 5.186215878017435 6.75 0 5.625
+8.451459884643555 9 3.502532958984375 1.229167281133554 8.836556730076344 3.586204456517275
+1.277645111083984 9 3.502532958984375 1.252532958984375 9 3.502532958984375
+1.763265013694763 9 3.502532958984375 9 0.560239315032959 3.502532958984375
+0.3931061394642433 8.105533767759253 3.719445271130248 0.4137253092132407 8.52498439764287 3.877221334451831
+8.451459884643555 7.264312744140625 3.502532958984375 0.9272406101226807 4.394378662109375 3.502532958984375
+0 5.625 2.25 0.9272406101226807 5.111862182617188 3.502532958984375
+0.254614219069482 5.087181091308593 3.438766479492187 0 5.625 3.375
+0.9272406101226807 5.829345703125 3.502532958984375 0.09121626615524292 5.829345703125 3.502532958984375
+8.550440100987007 8.308243833892794 3.861853648239981 1.125 9 4.5
+0.9272406101226807 7.981796264648438 3.502532958984375 0.9272406101226807 8.69927978515625 3.502532958984375
+1.013957517375565 8.562228274659388 3.75017356556579 7.875 9 3.502532958984375
+7.615435600280762 8.69927978515625 3.502532958984375 7.875 9 4.5
+8.009491852386146 8.784504889248915 3.603957929018302 7.615435600280762 0.0894773006439209 3.502532958984375
+8.347048808540125 0.1706542014512756 3.773513684276248 1.387778780781446e-17 8.620997428894043 3.502532958984375
+9 7.875 4.5 0.09121626615524292 5.111862182617188 3.502532958984375
+5.107362747192383 8.69927978515625 3.502532958984375 1.125 9 3.502532958984375
+0.9272406101226807 9 3.502532958984375 1.365195245262221 8.768577669088369 3.684282050294184
+8.696654072273855 2.281879823016757 3.852415077321564 8.560115968154676 0.6864217843879264 3.981789622887954
+3.986303794100596 7.55236472062994 2.305148219768089 8.228423440586161 8.653925268042904 3.756444164345976
+8.451459884643555 8.69927978515625 3.502532958984375 0 0 3.375
+8.361111387088744 1.922168594725715 4.210292958285929 8.451459884643555 1.524444341659546 3.502532958984375
+8.451459884643555 2.241928100585938 3.502532958984375 5.107362747192383 9 3.502532958984375
+
+
+
+ 276 656 609 276 756 656 84 157 83 157 84 154 214 465 212 468 214 212 96 727 269 95 596 96
+596 95 790 249 278 247 257 88 679 216 328 298 271 587 248 317 90 339 339 431 317 4 271 248
+249 248 271 263 679 88 4 248 253 255 253 248 119 45 101 46 6 101 6 46 151 101 8 6
+101 353 8 353 45 101 773 93 661 436 184 268 11 595 10 10 49 11 592 99 302 159 491 45
+159 45 491 167 17 16 384 16 17 737 659 44 122 737 44 226 228 456 715 402 444 30 742 744
+673 674 20 672 20 674 20 674 670 674 20 672 624 43 127 779 346 751 767 779 751 58 38 24
+740 24 38 693 132 692 692 178 693 693 692 690 294 728 286 733 33 732 705 732 33 33 733 705
+655 351 350 448 56 517 715 444 402 401 402 519 68 673 36 412 741 673 743 741 412 750 751 346
+751 750 767 512 676 671 622 751 750 94 630 309 627 626 661 628 661 626 483 477 99 46 101 6
+47 46 6 167 173 635 635 173 690 381 517 445 448 56 398 627 661 626 3 627 626 49 302 111
+640 308 422 231 709 82 541 56 398 573 704 707 122 118 103 628 626 661 626 628 3 369 3 96
+541 398 56 492 189 187 483 279 592 163 61 62 163 366 61 426 522 508 210 209 507 209 210 608
+512 671 676 376 404 522 408 372 405 714 405 372 523 446 445 439 580 71 263 88 679 88 263 335
+336 642 305 555 74 140 573 237 599 320 599 237 714 372 405 567 568 418 79 235 504 235 79 570
+468 212 462 155 84 297 467 297 84 597 726 96 726 597 790 12 14 773 297 332 155 303 52 14
+773 14 52 464 309 333 487 463 117 380 477 266 93 753 661 581 117 360 215 463 465 218 216 298
+217 360 215 217 215 214 216 217 214 219 360 217 218 219 217 221 223 219 74 221 219 553 598 583
+147 76 42 42 225 147 42 75 560 257 679 88 89 257 88 260 374 251 258 374 679 89 260 258
+374 258 260 273 260 474 258 257 679 257 258 679 258 257 89 343 263 679 343 679 263 335 5 272
+343 272 5 374 260 258 254 252 255 252 254 4 4 253 256 255 256 253 271 249 1 271 269 1
+249 271 1 789 722 95 596 597 96 597 596 790 625 725 597 369 165 3 245 661 734 3 245 734
+627 734 661 734 627 3 722 96 628 789 722 628 95 625 596 753 629 245 240 630 94 239 94 630
+630 240 239 240 94 262 93 262 94 342 272 776 472 351 407 609 656 654 351 654 656 609 654 653
+351 653 654 612 350 244 652 244 350 652 612 244 769 770 29 138 769 29 753 661 245 3 753 245
+753 93 661 789 628 661 3 628 96 87 0 164 368 240 165 277 0 370 240 3 165 164 0 2
+2 0 277 269 727 250 278 250 727 269 247 1 249 1 247 374 254 255 260 89 251 251 374 260
+335 373 89 2 277 184 89 436 4 609 434 335 338 199 92 409 198 139 139 142 612 200 141 201
+141 200 363 2 184 185 186 2 185 240 262 753 93 753 262 246 309 239 773 632 93 625 95 722
+592 302 99 725 598 587 4 256 252 255 252 256 790 727 278 482 278 249 579 487 117 255 584 249
+89 254 251 374 251 254 248 249 255 475 374 679 255 374 585 475 374 585 5 263 273 263 5 343
+335 263 5 343 5 263 97 290 273 272 273 776 407 351 472 609 610 434 222 553 583 435 475 580
+315 430 314 315 425 377 581 583 598 714 439 71 488 475 580 488 580 475 480 580 488 533 488 480
+378 494 470 380 485 600 466 449 298 440 480 533 446 71 480 446 480 71 440 480 446 446 523 445
+445 446 440 426 190 377 600 380 477 523 77 402 381 440 445 381 440 485 381 485 440 299 527 450
+538 517 56 499 490 381 538 499 517 690 173 176 181 388 65 65 67 181 108 62 64 9 6 8
+6 9 47 159 538 499 7 8 9 300 7 9 526 150 601 697 422 308 159 491 499 159 499 491
+123 44 659 123 122 44 354 538 56 540 98 64 645 694 702 352 159 177 657 177 352 352 177 159
+172 422 697 658 735 719 536 657 177 174 420 124 176 174 124 659 352 657 119 45 352 100 119 101
+121 659 119 103 118 100 100 118 119 353 491 45 9 8 6 151 9 6 9 8 7 47 9 7
+48 7 8 7 48 47 353 8 498 109 8 498 48 8 7 300 48 7 109 8 48 531 498 109
+47 105 113 48 8 109 113 98 47 113 108 98 300 9 151 107 109 111 109 531 111 302 49 594
+50 10 595 50 49 10 50 10 49 10 50 110 592 594 461 660 12 594 106 109 51 51 300 106
+110 108 113 11 49 10 110 11 10 49 112 11 11 112 595 11 112 49 112 11 110 115 49 12
+49 594 12 49 50 115 115 50 595 54 116 13 12 13 116 13 116 595 116 13 12 13 595 14
+12 13 14 54 13 14 12 14 13 15 52 773 52 15 53 631 15 773 15 631 53 632 53 631
+631 773 632 631 773 15 303 631 15 773 661 93 773 631 632 592 594 302 483 266 477 441 298 328
+318 488 533 308 695 697 719 735 658 735 719 787 659 681 658 683 681 658 167 16 174 384 174 16
+16 17 167 17 16 176 658 685 682 174 124 420 124 174 172 736 176 124 124 420 736 167 536 420
+636 634 171 634 636 384 174 167 420 536 167 420 174 176 16 16 167 174 635 173 167 173 635 384
+173 167 17 176 173 17 167 171 383 167 635 171 132 384 692 634 635 171 635 634 384 636 171 634
+636 171 692 384 636 692 129 131 128 128 312 129 542 171 383 396 394 128 131 128 394 171 542 383
+132 693 691 691 693 178 131 394 128 312 128 394 395 394 391 394 395 312 692 171 178 541 383 542
+542 171 178 541 542 383 397 542 392 178 542 392 210 211 182 182 506 210 397 541 398 354 177 538
+354 538 177 757 389 392 394 391 131 77 398 429 134 522 404 429 551 519 551 389 429 400 322 680
+519 402 77 519 429 77 135 519 551 552 551 295 401 519 402 401 519 135 563 590 229 344 229 590
+617 135 401 714 715 405 743 412 413 714 71 444 715 402 401 715 401 402 70 401 715 677 676 136
+677 136 676 136 616 617 589 229 344 126 613 616 770 407 410 613 70 616 744 428 362 126 410 32
+410 407 408 433 196 424 410 613 126 408 405 613 410 126 613 408 613 405 613 408 410 409 138 30
+32 410 126 31 125 289 289 30 31 28 32 770 778 125 619 29 770 28 28 30 29 27 31 32
+31 27 148 125 289 615 148 615 289 742 615 18 125 18 615 742 18 288 125 288 18 125 18 288
+148 288 18 677 126 616 729 669 674 791 619 778 619 791 742 729 778 676 729 791 778 673 19 145
+672 145 19 621 241 145 43 145 241 24 38 58 38 24 264 515 671 524 24 740 417 23 43 127
+671 524 672 705 775 732 621 590 622 673 20 19 672 19 20 19 20 670 20 19 672 524 43 672
+23 127 43 21 242 43 242 21 36 21 621 622 22 43 21 622 22 21 22 21 43 21 22 36
+23 22 43 22 23 36 23 43 22 622 23 22 752 127 23 622 752 23 752 23 127 23 752 36
+739 624 766 295 511 416 550 515 137 58 766 739 752 767 127 622 750 752 622 779 751 780 346 779
+767 780 779 767 728 286 286 346 780 767 286 780 767 35 766 767 286 728 414 345 686 733 294 728
+294 733 686 34 705 37 775 35 728 775 728 35 37 35 34 58 35 766 766 35 58 283 58 24
+283 739 605 55 24 283 24 55 264 55 283 24 417 55 24 265 55 283 55 265 264 763 283 282
+281 265 283 265 281 264 285 284 763 284 285 761 283 24 58 605 763 283 283 605 739 284 282 763
+282 284 761 169 711 777 712 285 777 285 712 761 757 759 391 131 392 757 25 391 169 312 25 169
+25 169 391 169 25 243 26 25 391 25 26 243 395 391 26 312 395 26 395 26 391 26 395 243
+393 355 243 178 131 392 178 392 131 132 691 130 130 691 178 130 178 691 691 396 130 525 617 136
+295 135 551 512 416 515 137 511 416 550 137 515 733 728 732 149 32 27 30 149 27 149 27 32
+27 149 148 28 149 32 149 28 771 28 32 149 30 28 149 649 770 651 138 649 651 29 28 770
+28 29 771 651 770 769 138 651 769 612 655 350 770 649 748 138 748 649 652 748 612 652 786 748
+652 350 786 685 719 658 685 658 719 682 658 683 787 685 682 364 91 201 786 407 770 770 410 32
+27 32 31 30 27 31 138 612 748 138 409 139 351 407 786 138 29 30 472 408 407 778 125 677
+742 288 619 125 619 288 125 31 32 770 32 28 143 203 428 203 143 144 733 732 33 686 33 732
+686 733 33 294 286 728 286 294 686 550 739 624 739 766 624 672 43 145 68 412 673 208 143 204
+413 204 143 43 524 624 550 524 515 36 241 242 242 241 43 202 412 68 137 515 416 58 37 35
+730 731 35 731 730 180 37 34 35 34 37 180 34 730 35 730 34 180 36 346 287 624 127 43
+36 287 68 346 36 752 766 127 624 41 202 68 59 37 58 37 59 180 40 41 57 39 40 57
+345 57 287 57 345 207 550 605 739 59 58 38 264 59 38 57 41 287 40 60 41 60 40 39
+616 136 617 616 617 70 126 616 613 405 613 70 146 290 72 677 616 126 146 72 75 75 73 146
+379 670 667 408 405 372 410 408 407 406 379 293 75 72 560 589 344 379 292 73 293 72 222 560
+42 560 224 225 42 224 76 73 75 147 456 228 228 452 147 82 591 81 624 524 43 550 515 524
+137 416 515 767 780 286 145 43 672 672 524 671 590 710 622 58 739 766 59 58 37 740 59 37
+765 779 622 710 590 563 737 44 659 44 737 386 659 658 657 304 701 703 324 701 304 420 167 174
+172 697 684 659 119 352 121 119 659 119 101 45 352 45 159 657 352 177 177 159 538 123 659 44
+386 123 44 684 694 698 694 684 697 536 657 420 159 499 538 491 490 499 601 150 642 499 381 517
+336 601 642 606 361 337 99 531 302 600 490 497 600 485 490 151 46 102 101 102 46 105 51 109
+105 300 51 49 111 302 497 490 491 153 531 99 463 296 465 156 327 462 156 152 327 326 325 330
+154 84 155 156 462 83 152 156 154 101 46 102 102 46 103 386 698 150 526 385 150 681 659 658
+737 787 681 659 657 352 536 177 657 356 690 359 161 66 65 177 538 159 491 499 490 540 192 194
+544 194 192 47 539 103 353 498 8 105 48 109 105 47 48 531 109 498 301 114 107 111 107 114
+111 49 112 111 302 531 594 49 302 115 50 49 115 54 50 99 302 531 105 109 51 51 113 105
+489 110 54 153 99 531 302 111 531 470 494 493 365 188 164 660 773 12 592 461 594 54 110 50
+53 54 52 489 54 471 53 471 54 492 489 471 368 189 471 240 753 3 660 594 461 53 368 471
+239 53 246 246 93 239 240 368 53 87 85 0 370 0 85 158 703 78 78 306 158 348 578 235
+150 698 642 578 308 640 397 429 389 384 132 399 762 763 777 712 777 285 310 712 285 777 763 285
+459 504 640 170 236 599 236 170 566 460 79 504 762 137 605 265 283 55 417 265 55 569 79 168
+460 168 79 712 711 777 711 712 310 757 392 389 133 502 501 26 391 25 312 26 25 391 759 777
+552 389 551 58 283 739 568 502 133 131 391 394 231 707 709 573 707 231 58 35 37 395 391 394
+178 392 542 176 359 690 160 176 787 162 66 194 193 194 66 357 175 160 359 160 175 356 161 388
+354 56 538 387 508 522 397 398 541 712 777 711 355 758 761 282 283 763 397 392 389 391 757 131
+739 283 58 207 39 57 59 264 180 264 281 761 686 775 180 686 180 414 60 202 41 60 204 202
+204 60 411 60 39 411 317 183 185 185 375 317 164 2 186 533 480 488 318 533 488 186 183 470
+482 266 584 592 279 461 483 482 279 483 477 266 188 187 189 187 188 365 483 99 477 378 470 183
+318 266 380 431 339 314 430 378 431 185 184 375 488 475 585 489 62 108 61 64 62 192 64 61
+63 192 61 62 492 163 492 62 489 108 64 98 61 366 63 538 517 499 388 161 65 181 547 388
+497 491 490 499 517 381 499 381 490 544 63 495 440 446 480 430 191 378 480 71 580 446 444 71
+523 402 444 448 445 517 448 398 77 382 495 508 211 547 182 426 377 425 426 508 190 192 63 544
+162 382 67 387 67 382 67 313 181 397 429 398 359 175 161 162 67 65 161 175 66 193 66 175
+182 313 506 429 389 551 134 313 387 313 134 506 203 69 362 69 203 427 203 362 428 137 416 511
+144 510 509 401 617 70 616 70 617 525 617 135 415 404 509 41 68 287 69 198 362 69 197 198
+197 69 195 376 425 424 70 715 401 195 427 516 69 427 195 196 91 197 364 197 91 315 433 425
+70 405 715 195 424 196 142 201 199 201 142 364 472 275 372 338 90 434 373 434 90 413 143 428
+207 205 39 411 39 205 330 325 602 331 330 602 152 213 450 334 462 327 326 152 450 214 215 465
+215 117 463 441 328 331 485 533 440 360 117 215 337 331 602 443 326 450 381 445 517 440 533 480
+446 71 444 523 444 402 581 360 223 219 223 360 523 402 77 454 140 220 218 449 74 556 560 222
+458 442 220 448 517 445 519 77 402 401 135 519 454 220 442 452 589 406 76 452 406 81 591 227
+571 81 227 76 406 73 555 225 224 221 74 555 226 571 227 76 75 42 147 452 76 80 231 82
+226 455 572 457 572 455 70 617 401 453 589 452 563 229 591 563 591 82 321 168 319 168 321 320
+233 322 400 135 551 519 561 230 680 400 230 572 523 77 448 77 429 398 505 166 700 504 235 578
+504 578 640 233 647 322 166 78 700 78 166 307 607 647 234 421 234 458 648 421 606 336 305 323
+700 78 703 307 306 78 348 347 166 166 505 348 505 308 578 349 235 570 319 502 568 566 567 236
+569 570 79 460 502 319 563 82 709 80 82 81 562 80 81 467 84 83 212 83 462 83 212 467
+467 465 296 467 296 297 464 629 309 464 487 629 155 747 213 318 266 584 74 449 140 480 580 71
+555 140 554 556 221 224 555 224 221 73 406 293 97 72 290 97 553 222 439 275 372 374 254 251
+146 291 290 291 146 292 597 596 625 461 790 279 340 609 335 340 335 342 340 756 276 276 609 340
+370 86 369 367 368 165 461 279 790 371 277 370 367 189 368 86 85 87 85 86 370 367 86 87
+789 461 790 727 726 790 726 727 96 374 475 679 436 375 184 434 373 335 335 89 88 436 89 373
+254 89 4 90 375 373 317 375 90 363 91 196 364 142 139 92 141 316 92 199 141 94 239 93
+632 309 246 53 632 246 93 246 632 93 239 94 96 371 369 660 461 789 789 790 95 95 96 722
+722 789 95 661 773 660 595 303 14 773 660 12 660 594 12 632 773 93 255 252 254 474 254 252
+585 255 584 255 254 374 435 343 679 435 679 343 435 275 439 474 97 273 435 439 580 435 580 439
+358 494 191 380 485 533 299 213 747 380 266 477 318 533 380 448 398 56 448 517 56 233 442 234
+358 366 494 300 151 527 153 99 477 153 477 99 354 383 541 354 56 541 354 541 56 8 353 101
+6 8 101 353 497 491 100 101 119 100 119 118 118 385 100 101 100 104 104 100 385 103 100 104
+104 100 101 103 104 102 102 104 101 151 104 385 104 151 102 102 101 104 300 105 48 109 48 105
+109 498 8 106 51 109 106 113 51 111 109 107 114 113 107 107 111 114 113 106 107 107 106 109
+109 106 107 107 106 300 110 489 108 113 238 110 109 111 531 110 238 112 111 112 238 111 238 112
+301 112 238 238 113 114 114 111 238 115 116 54 116 115 12 12 49 115 49 12 594 115 595 116
+12 115 116 14 773 52 52 54 14 773 14 12 483 99 592 483 592 99 318 380 533 581 579 117
+600 490 485 600 497 490 153 498 497 153 497 600 531 153 498 153 477 600 366 493 494 153 600 477
+120 118 119 120 385 118 120 119 118 118 122 120 119 120 121 121 120 122 385 123 386 385 120 121
+121 120 119 659 121 123 123 121 122 385 121 123 123 121 659 657 536 420 536 354 383 167 383 536
+736 420 124 172 736 124 459 640 399 397 398 429 397 389 429 399 132 501 312 501 132 397 542 541
+397 389 392 295 416 511 400 680 230 418 568 133 415 134 404 295 551 135 525 136 617 427 509 516
+512 515 416 742 30 289 125 778 619 744 409 30 126 125 32 677 616 136 677 125 126 743 428 744
+677 136 616 665 663 729 666 669 729 666 669 741 669 666 729 145 672 19 19 670 145 729 672 674
+767 127 766 767 750 752 346 752 750 767 728 35 127 767 752 767 35 728 759 511 762 552 757 389
+762 605 763 762 511 137 552 389 757 312 169 310 777 762 759 777 759 391 312 133 501 129 396 128
+128 131 129 131 757 391 178 131 130 131 129 130 312 130 129 396 129 130 130 129 131 130 312 132
+552 757 759 310 133 312 415 506 134 525 135 617 525 295 135 525 295 416 512 525 136 512 676 136
+512 136 676 512 136 525 512 416 525 550 605 137 550 137 605 612 138 139 656 756 351 472 372 408
+472 408 372 273 290 776 610 338 434 363 196 433 472 407 408 364 139 198 433 424 425 714 372 439
+714 439 372 714 71 439 454 554 140 523 446 444 523 444 446 199 201 141 364 198 197 363 316 141
+142 199 610 144 143 208 413 202 204 144 509 427 743 413 428 670 621 145 671 515 512 43 241 145
+673 145 241 672 43 524 292 146 73 769 29 770 29 769 771 147 225 456 453 228 227 226 227 228
+226 572 571 562 571 230 230 571 572 708 707 706 80 237 231 80 232 237 573 231 237 561 569 232
+236 567 704 573 236 704 168 320 569 292 293 148 149 771 148 148 293 667 386 737 681 386 150 385
+526 151 385 526 527 151 443 526 601 324 323 305 326 327 152 443 527 526 325 326 443 595 112 301
+112 49 111 299 450 213 747 301 299 300 301 107 332 747 155 154 155 213 156 83 157 154 156 157
+15 773 52 303 15 52 158 304 703 158 306 304 160 787 122 119 352 45 122 103 357 659 352 119
+352 159 45 544 382 162 357 160 122 162 65 66 353 101 45 46 47 103 47 98 539 539 357 103
+544 162 194 540 193 539 163 187 493 163 492 187 492 471 189 163 493 366 86 165 369 165 86 367
+188 87 164 87 188 367 645 698 694 698 645 642 347 647 307 347 307 166 536 383 167 384 422 172
+171 635 167 386 684 698 383 171 167 399 640 422 348 505 578 561 680 570 349 570 680 460 319 168
+459 399 501 311 567 418 552 511 759 777 169 391 759 762 777 169 777 711 711 310 169 704 706 707
+417 311 418 321 170 599 170 321 566 779 780 767 542 178 171 384 399 422 354 541 383 536 177 354
+167 173 17 17 173 384 174 384 172 193 175 357 359 176 160 536 383 354 536 354 177 396 356 393
+393 356 388 392 131 757 547 211 355 552 759 511 759 762 511 243 396 393 179 205 209 206 209 205
+762 605 137 608 510 179 243 355 761 762 137 511 211 507 758 603 264 758 414 180 603 758 507 603
+608 179 209 414 603 206 603 180 264 207 414 206 208 179 510 411 205 179 345 414 207 608 210 506
+181 182 547 181 313 182 365 164 186 185 183 186 365 186 470 365 493 187 367 188 189 191 377 190
+191 494 378 485 440 533 490 381 485 358 191 190 540 64 192 544 495 382 540 194 193 387 313 67
+387 522 134 387 382 508 415 509 510 295 551 552 376 522 426 376 426 425 195 516 424 195 196 197
+363 433 316 91 200 201 200 91 363 585 584 318 430 315 377 144 208 510 144 427 203 411 208 204
+207 206 205 206 507 209 603 507 206 210 507 211 334 331 328 467 212 465 468 328 216 154 213 152
+468 216 214 217 216 218 218 298 449 218 74 219 466 220 449 466 458 220 556 223 221 556 222 583
+556 583 223 556 224 560 555 554 225 714 444 71 453 227 591 453 452 228 453 591 229 453 229 589
+232 80 562 81 571 562 561 562 230 233 457 442 457 455 454 233 400 457 519 551 429 233 234 647
+523 448 445 421 361 606 331 337 441 607 421 648 607 307 647 645 701 305 702 695 700 505 700 695
+607 234 421 349 322 347 320 232 569 236 573 599 320 237 232 690 356 396 59 38 58 59 740 38
+111 114 238 238 114 301 690 691 693 269 268 371 239 240 53 268 269 4 610 612 142 672 729 671
+242 21 43 242 621 21 791 741 742 621 242 241 43 241 242 673 241 36 711 243 761 742 289 615
+615 289 125 756 656 351 653 655 351 786 652 748 770 748 649 651 769 770 649 651 770 351 654 653
+351 656 654 652 350 244 650 244 350 650 652 244 753 245 661 93 262 753 262 93 94 627 661 734
+93 632 246 628 789 661 660 661 789 789 461 660 596 790 95 629 625 734 722 628 625 595 747 332
+587 247 270 249 584 482 587 250 725 1 247 587 255 249 248 271 1 587 249 271 248 587 270 250
+474 251 254 587 598 553 255 248 253 587 253 248 587 553 474 255 256 252 474 252 256 255 253 256
+587 256 253 260 251 474 256 587 474 258 679 374 585 374 475 273 257 258 257 273 88 263 88 273
+260 273 258 584 255 585 309 629 262 753 262 629 262 94 309 93 246 239 97 474 553 340 342 776
+655 653 351 653 655 609 350 351 786 770 748 786 770 786 748 655 650 350 758 264 761 761 281 282
+762 763 605 417 282 281 283 281 282 417 281 265 283 265 281 483 266 482 371 268 277 279 278 482
+483 279 482 625 487 579 625 579 725 483 482 266 726 727 790 269 250 270 278 270 250 269 371 96
+278 727 790 482 584 249 247 269 270 270 278 247 269 271 4 268 4 436 278 249 247 272 342 343
+342 335 272 272 343 342 343 5 272 273 272 5 343 275 756 472 756 275 351 756 472 343 756 275
+472 275 756 776 276 340 756 340 276 340 343 342 268 184 277 278 790 279 482 278 279 278 279 790
+777 763 762 281 283 282 417 284 282 763 282 284 417 285 284 763 284 285 310 285 417 775 731 180
+286 686 346 686 732 775 728 775 732 767 766 35 677 126 125 677 125 778 125 32 31 125 288 619
+667 619 288 289 31 148 31 289 125 472 372 275 97 222 72 406 589 379 293 379 667 776 290 291
+291 650 776 291 771 650 771 291 292 288 148 667 286 765 294 740 706 311 294 765 705 294 705 733
+705 730 731 733 732 728 410 407 770 770 32 410 525 416 295 778 729 676 379 344 670 663 670 665
+671 676 729 592 302 594 592 461 279 483 592 279 595 332 303 333 296 464 333 297 296 464 463 487
+299 300 527 441 466 298 595 301 747 301 300 299 309 303 333 306 648 304 324 304 648 324 305 701
+607 648 306 306 307 607 505 695 308 418 310 417 550 739 605 605 283 763 740 311 417 739 550 624
+765 708 705 708 765 710 133 310 418 608 506 415 758 355 211 92 339 338 339 92 314 316 314 92
+315 314 316 315 316 433 439 275 435 275 439 372 431 378 183 431 183 317 585 318 488 320 321 599
+566 319 568 566 321 319 349 348 235 680 322 349 347 322 647 324 606 323 324 648 606 601 336 325
+337 323 606 325 443 601 326 330 327 334 328 468 334 330 331 332 333 303 327 330 334 337 602 323
+336 323 602 771 292 148 332 297 333 705 708 706 90 338 339 340 343 756 342 343 340 612 609 655
+670 344 621 287 346 345 346 686 345 347 348 349 786 351 350 756 343 340 482 249 278 584 255 249
+430 431 314 353 498 497 421 458 361 397 392 542 393 547 355 523 448 77 523 445 448 519 77 429
+539 193 357 358 190 495 393 388 547 540 539 98 356 359 161 318 380 266 318 585 488 581 223 583
+480 488 580 318 584 585 466 441 361 71 580 439 466 361 458 457 454 442 409 362 198 409 744 362
+199 338 610 470 493 365 371 370 369 600 485 380 585 374 255 714 444 715 381 445 440 376 424 516
+191 430 377 448 77 398 358 495 63 358 63 366 353 45 491 353 497 498 517 538 56 659 737 681
+122 787 737 657 658 420 541 542 397 243 395 396 396 395 394 511 552 295 552 759 757 572 457 400
+714 405 715 376 516 404 516 509 404 405 70 715 613 616 70 125 126 32 70 613 405 408 613 410
+411 179 208 550 624 524 202 413 412 515 524 671 512 515 671 510 608 415 512 525 416 525 135 295
+550 524 624 765 622 710 563 709 710 736 735 787 735 736 420 735 420 658 787 176 736 475 435 679
+275 343 435 436 373 375 445 440 446 153 600 497 458 234 442 337 361 441 449 220 140 226 456 455
+455 456 554 459 502 460 459 460 504 464 296 463 334 468 462 600 477 380 660 789 661 475 679 435
+488 585 475 435 580 475 584 266 318 584 266 482 579 598 725 153 531 498 491 497 353 153 497 498
+381 490 485 502 459 501 551 389 552 552 511 295 401 135 617 671 729 676 508 495 190 594 660 461
+533 485 380 443 450 527 455 554 454 225 554 456 561 232 562 621 344 590 566 568 567 561 570 569
+598 579 581 596 597 790 435 343 275 702 701 645 602 325 336 612 610 609 729 791 663 125 615 18
+148 18 615 667 663 791 670 663 667 619 667 791 791 778 619 729 672 671 663 665 729 665 666 729
+665 670 666 672 729 674 767 750 751 767 751 779 767 752 750 766 624 127 625 628 626 625 626 627
+625 627 734 789 628 722 239 309 630 239 630 94 309 631 303 309 632 631 682 683 658 658 659 657
+172 684 735 636 634 690 634 635 690 634 171 635 636 690 692 636 692 171 658 682 685 787 719 685
+682 683 787 305 642 645 650 649 748 650 651 649 650 769 651 786 350 652 771 769 650 650 653 654
+650 654 656 650 655 653 650 656 776 655 350 351 95 790 789 278 250 270 270 247 278 278 727 250
+725 250 727 1 249 247 681 684 386 420 658 657 681 737 659 734 661 245 670 669 666 670 674 669
+741 791 663 741 663 665 665 666 741 666 665 729 742 741 743 741 674 673 778 676 677 684 681 683
+684 719 735 684 683 682 684 682 685 684 685 719 396 691 690 178 171 692 178 693 691 693 178 692
+178 130 131 695 694 697 695 702 694 700 703 702 701 702 703 311 706 704 311 704 567 705 706 740
+708 709 707 710 709 708 761 712 711 243 711 169 391 169 777 715 444 714 735 658 420 597 725 726
+597 726 790 725 727 726 775 728 732 35 731 775 35 731 730 730 705 34 294 733 728 775 731 35
+765 286 780 245 629 734 461 789 790 735 420 736 736 172 735 34 35 730 674 741 669 669 729 674
+37 705 740 791 729 663 743 744 742 625 629 487 767 766 127 472 756 351 650 748 652 777 285 763
+661 660 773 407 351 786 656 276 776 276 656 756 775 705 731 778 677 676 658 681 683 787 683 681
+765 780 779 786 770 407 759 757 391 791 729 778 276 756 656 609 154 84 83 157 468 214 212 465 278 96 269 727
+790 95 96 596 96 280 278 371 267 249 247 278 280 789 96 790 259 257 679 88 582 216 298 328 764 776 650 771 267 271 248 587
+317 431 90 339 249 4 248 271 259 263 88 679 255 4 253 248 119 103 45 101 151 46 101 6 101 47 8 6 101 47 353 8
+151 353 101 45 45 353 47 98 774 773 661 93 481 436 268 184 11 49 595 10 745 595 660 303 593 592 302 99 537 159 45 491
+500 159 491 45 384 167 16 17 122 737 44 659 226 452 228 456 618 715 444 402 125 30 744 742 672 673 20 674 672 20 670 674
+622 624 127 43 767 779 751 346 740 58 24 38 693 178 132 692 689 693 690 692 755 294 286 728 705 733 732 33 611 655 350 351
+575 448 517 56 423 715 402 444 68 127 43 36 518 401 519 402 36 673 43 68 514 412 673 741 514 743 412 741 767 750 346 751
+451 77 398 447 662 512 671 676 781 622 750 751 774 94 309 630 745 486 279 483 261 627 661 626 261 628 626 661 593 483 99 477
+721 746 461 660 47 46 6 101 635 173 690 167 521 381 445 517 545 448 398 56 3 627 626 661 301 49 111 302 646 640 422 308
+638 699 500 150 565 231 82 709 575 541 398 56 781 623 766 127 573 704 707 567 537 539 98 45 122 118 103 119 3 628 661 626
+789 369 96 3 518 403 426 376 545 541 56 398 365 492 187 189 478 483 592 279 163 366 61 62 521 426 508 522 608 210 507 209
+784 512 676 671 518 376 522 404 423 408 405 372 469 479 380 533 423 714 372 405 479 523 445 446 438 439 71 580 335 263 679 88
+644 336 305 642 558 555 140 74 320 573 599 237 559 714 405 372 723 567 418 568 570 79 504 235 468 467 212 462 467 155 297 84
+790 597 96 726 12 303 14 773 746 297 155 332 773 303 14 52 746 464 333 309 532 487 117 463 586 380 266 477 774 93 661 753
+582 581 360 117 582 215 465 463 558 582 74 140 582 218 298 216 218 217 215 360 216 217 214 215 218 219 217 360 74 221 219 223
+557 553 583 598 564 226 591 571 678 126 125 148 147 225 76 42 225 42 560 75 476 776 274 472 274 476 275 273 89 257 88 679
+259 473 273 474 473 260 251 374 258 89 374 679 374 89 258 260 273 473 260 474 259 258 679 257 89 257 679 258 343 335 263 679
+259 343 263 679 343 335 272 5 473 374 258 260 4 254 255 252 255 4 256 253 267 271 1 249 249 271 1 269 278 725 250 267
+782 789 95 722 790 596 96 597 782 625 597 725 369 661 165 3 3 245 734 661 3 627 661 734 789 722 628 96 782 95 596 625
+783 753 245 629 239 240 94 630 93 240 262 94 774 309 262 783 274 342 776 272 764 472 407 351 351 609 654 656 351 609 653 654
+652 612 244 350 138 769 29 770 3 753 245 661 661 93 240 753 789 3 628 661 789 3 96 628 370 87 164 0 165 240 661 368
+370 0 164 277 661 240 165 3 2 0 277 164 278 269 250 727 249 269 1 247 255 254 4 374 260 374 89 251 335 373 89 679
+183 184 375 481 186 2 184 277 374 89 4 436 437 373 434 679 343 335 679 434 341 609 335 434 432 275 341 472 92 199 316 338
+432 409 139 198 432 139 407 610 341 432 610 434 611 139 612 142 611 612 748 786 363 200 201 141 186 2 185 184 93 240 753 262
+774 246 239 309 303 660 12 595 774 773 93 632 782 625 722 95 789 369 3 661 484 592 99 302 745 788 461 279 486 725 587 598
+255 4 252 256 782 790 278 727 249 278 268 269 482 267 278 249 532 579 117 487 255 436 4 249 438 373 375 437 267 255 249 584
+374 89 251 254 373 374 89 679 267 255 587 248 255 249 4 248 473 475 679 374 255 436 374 4 473 255 585 374 259 559 553 97
+255 474 585 587 438 475 585 374 343 5 273 263 474 473 435 259 343 335 5 263 476 97 273 290 341 275 432 434 274 272 776 273
+472 351 610 407 341 609 434 610 274 343 273 275 435 474 553 475 558 222 583 553 438 435 580 475 437 314 90 431 437 315 314 430
+559 435 553 475 403 315 377 425 557 581 598 583 423 714 71 439 557 488 580 475 438 488 475 580 582 220 140 577 438 480 488 580
+586 533 480 488 469 378 470 494 600 485 586 380 582 466 298 449 403 440 533 480 403 446 480 71 716 446 71 480 530 440 446 480
+451 446 445 523 479 445 440 446 479 426 377 190 479 600 477 380 521 523 402 77 576 381 445 440 353 47 98 498 479 381 485 440
+576 381 440 485 593 299 450 527 575 538 56 517 576 499 381 490 576 538 517 499 176 173 167 690 575 384 132 542 181 67 388 65
+537 497 353 496 496 108 64 62 47 9 8 6 537 159 499 538 300 7 9 8 638 526 601 150 638 697 308 422 500 159 499 491
+537 159 491 499 123 122 44 659 575 354 56 538 537 540 64 98 696 645 702 694 500 352 177 159 543 657 352 177 543 352 159 177
+699 172 697 422 720 658 719 735 699 536 177 657 176 174 124 420 386 659 657 352 357 122 352 419 385 119 352 45 103 352 45 539
+419 787 420 535 122 103 352 119 100 119 101 103 121 122 659 119 100 118 119 103 537 353 45 98 500 353 45 491 151 9 6 8
+47 9 7 8 47 48 8 7 353 151 8 498 496 108 531 498 47 109 498 8 300 48 7 8 48 8 300 109 108 113 531 498
+300 531 109 498 498 531 534 300 109 47 113 105 47 48 109 8 498 113 47 98 498 113 98 108 151 9 8 300 111 109 113 107
+113 109 111 531 594 49 595 302 50 49 10 595 110 50 49 10 745 592 461 594 660 54 12 594 106 300 109 51 531 110 113 108
+110 11 10 49 11 112 595 49 110 11 49 112 12 49 54 115 54 49 12 594 115 50 595 49 12 54 13 116 12 13 595 116
+12 13 14 595 12 54 14 13 53 15 773 52 53 631 773 15 632 773 53 631 303 631 15 773 53 773 93 661 774 773 632 631
+484 592 302 594 478 186 277 164 586 483 477 266 469 484 494 470 582 441 328 298 586 486 587 598 438 584 469 481 586 318 533 488
+587 725 486 267 535 419 787 160 696 308 697 695 720 738 684 718 419 681 658 787 787 719 658 735 658 681 419 659 718 683 658 681
+384 167 174 16 176 16 167 17 718 658 682 685 172 174 420 124 736 420 176 124 420 172 735 720 420 536 176 167 384 636 171 634
+633 171 690 692 420 167 176 174 172 536 420 167 174 167 176 16 384 635 167 173 176 173 17 167 383 171 690 167 171 635 690 167
+689 171 692 690 171 132 692 384 384 634 171 635 633 171 635 690 633 636 634 171 384 636 692 171 129 312 131 128 384 542 383 171
+131 396 128 394 171 690 542 383 542 690 171 689 691 693 178 132 312 131 128 394 312 395 391 394 713 243 395 391 692 132 171 178
+543 541 542 383 689 542 178 171 575 541 383 542 575 392 542 132 390 397 392 542 392 542 132 178 210 506 211 182 575 397 398 541
+538 177 699 354 543 354 177 538 447 312 392 132 390 757 392 389 713 394 131 391 521 77 429 398 520 134 404 522 604 548 390 520
+520 429 519 551 447 551 429 389 529 400 680 322 521 519 77 402 447 519 77 429 784 524 671 412 548 135 551 519 447 552 295 551
+618 401 402 519 518 401 135 519 620 563 229 590 620 344 590 229 618 617 401 135 423 714 405 715 784 743 413 412 423 714 444 71
+423 715 401 402 784 70 518 423 618 715 402 401 716 714 588 559 618 70 715 401 662 677 136 676 784 677 676 136 784 136 617 616
+620 589 344 229 513 126 616 613 70 614 588 405 409 770 410 407 513 613 616 70 513 744 362 428 148 126 32 410 432 410 408 407
+432 433 424 196 513 410 126 613 423 408 613 405 476 275 97 559 614 410 613 126 614 408 405 613 32 126 744 409 614 613 410 408
+409 770 138 30 409 32 126 410 31 30 125 289 771 28 770 32 778 667 125 619 148 771 614 410 29 30 770 28 148 27 32 31
+148 125 615 289 125 742 18 615 125 742 288 18 148 125 288 18 513 677 616 126 125 148 667 678 668 729 674 669 742 791 778 619
+514 729 676 778 514 729 778 791 668 672 670 674 672 673 145 19 620 590 621 662 43 621 145 241 264 24 58 38 662 515 524 671
+24 58 740 417 549 412 524 784 36 23 127 43 672 524 412 671 755 705 732 775 662 621 622 590 673 68 672 43 672 673 19 20
+672 19 670 20 68 524 672 43 622 23 43 127 36 21 43 242 43 21 622 621 622 22 21 43 36 22 43 21 36 23 43 22
+622 23 22 43 622 752 23 127 36 752 127 23 724 739 766 624 688 287 346 127 548 295 416 511 548 550 137 515 180 58 739 766
+749 752 127 767 781 622 752 750 767 687 346 688 781 622 751 779 767 780 779 346 687 767 286 728 767 286 780 346 754 767 766 35
+755 767 728 286 688 414 686 345 686 733 728 294 754 34 37 705 754 775 728 35 687 775 35 728 754 37 34 35 754 37 58 740
+180 58 766 35 754 766 58 35 283 417 58 24 605 603 264 739 283 264 739 605 264 55 283 24 417 55 24 283 264 265 283 55
+763 283 282 417 264 281 283 265 761 285 763 284 264 283 58 24 717 605 283 763 411 207 41 39 417 283 739 605 761 284 763 282
+760 169 777 711 761 712 777 285 391 759 312 757 131 312 392 757 312 25 169 391 243 25 391 169 243 26 391 25 312 395 26 391
+243 395 391 26 713 393 243 355 396 178 392 131 131 392 132 178 130 691 178 132 130 396 178 691 662 525 136 617 548 295 551 135
+548 512 515 416 724 417 739 605 503 137 416 511 724 550 515 137 686 733 732 728 30 149 27 32 148 149 32 27 771 28 32 149
+32 30 770 409 30 28 149 32 138 649 651 770 771 29 770 28 785 764 650 771 138 651 769 770 611 407 610 351 350 655 611 612
+748 611 770 138 138 770 748 649 652 786 748 612 786 350 612 652 637 685 658 719 718 685 719 658 637 682 683 658 637 787 682 685
+432 199 338 316 363 364 201 91 611 786 770 407 409 770 32 410 30 27 31 32 748 612 611 138 138 770 409 139 611 139 407 770
+611 351 786 407 138 29 30 770 432 613 410 513 432 472 407 408 409 139 770 407 432 364 139 610 677 125 742 778 125 742 619 288
+125 30 31 32 770 30 32 28 513 198 195 362 30 744 32 125 742 744 126 513 144 143 428 203 686 733 33 732 686 294 728 286
+549 550 624 739 549 739 624 766 287 68 127 624 145 43 673 672 672 68 673 412 413 208 204 143 68 43 624 524 549 550 515 524
+687 180 766 35 242 241 43 36 549 202 68 412 548 137 416 515 549 414 603 739 180 58 35 37 180 730 35 731 180 37 35 34
+180 34 35 730 287 346 127 36 43 127 68 624 36 127 287 68 346 36 752 127 688 766 624 127 549 41 68 202 549 287 624 68
+180 59 58 37 39 40 57 41 207 345 287 57 604 550 739 605 264 59 38 58 207 57 287 41 39 40 41 60 662 616 617 136
+620 616 70 617 678 126 613 616 70 613 614 405 292 146 72 290 558 476 72 222 678 677 126 616 125 667 778 678 146 73 72 75
+620 379 667 670 559 408 372 405 588 70 715 618 614 410 407 408 614 406 293 379 76 75 560 72 620 589 379 344 476 292 293 73
+558 72 560 222 558 564 456 452 716 444 530 588 225 42 224 560 558 225 452 456 558 476 406 73 76 73 75 72 147 452 456 228
+562 82 81 591 622 624 43 524 724 550 524 515 724 137 515 416 755 767 286 780 145 621 43 672 621 672 671 524 724 590 622 710
+754 58 766 739 740 59 37 58 781 765 622 779 723 704 567 707 563 590 724 710 723 567 565 707 386 737 659 44 419 659 657 658
+306 304 703 701 644 324 304 701 420 172 167 174 172 697 684 698 385 659 352 119 121 385 119 659 385 119 45 101 500 352 159 45
+699 657 177 352 500 177 538 159 386 123 44 659 697 684 698 694 535 536 420 657 699 500 352 177 500 159 538 499 534 491 499 490
+638 601 642 150 576 499 517 381 644 336 642 601 337 336 361 325 606 336 361 337 576 490 485 534 593 99 302 531 531 302 110 489
+534 600 497 490 534 600 490 485 500 353 498 151 385 151 101 45 500 385 45 151 300 498 8 151 101 151 102 46 105 300 51 109
+49 110 111 302 534 497 491 490 593 153 99 531 746 532 155 296 532 463 465 296 156 152 327 462 326 331 325 330 154 467 84 155
+532 154 462 152 154 156 83 462 154 156 462 152 102 46 103 101 699 386 150 698 500 526 150 385 738 420 172 657 720 681 658 659
+419 737 681 787 787 736 420 535 419 659 352 657 543 536 657 177 359 690 536 356 193 161 65 66 543 177 159 538 537 491 490 499
+544 540 194 192 103 47 45 101 45 47 103 539 353 47 498 8 105 47 48 109 113 531 498 109 111 301 107 114 111 110 49 112
+111 301 302 531 54 594 302 49 302 110 54 49 115 54 50 49 484 99 531 302 47 113 498 109 105 113 109 51 54 110 302 489
+484 153 531 99 110 302 531 111 484 470 493 494 365 367 188 164 12 773 54 660 478 592 594 461 478 370 164 277 484 471 302 489
+54 49 110 50 53 773 54 52 489 302 54 471 54 471 302 594 660 53 54 471 484 492 471 489 478 368 471 189 478 365 164 367
+53 240 661 93 661 240 3 753 478 660 461 594 660 53 471 368 239 93 53 246 661 240 53 368 660 53 368 661 54 53 660 773
+370 87 0 85 158 306 703 78 639 348 235 578 641 700 695 696 638 150 642 698 641 505 700 307 641 578 640 308 447 397 389 429
+575 384 399 132 759 310 391 312 310 762 777 763 310 712 285 777 310 777 285 763 696 641 638 308 646 459 640 504 566 170 599 236
+460 570 79 504 503 762 605 137 417 265 55 283 460 569 168 79 565 529 460 502 310 712 777 711 447 757 389 392 503 133 501 502
+312 26 25 391 391 310 759 777 447 552 551 389 58 417 283 739 503 568 133 502 131 312 391 394 565 231 709 707 565 573 231 707
+754 58 37 35 713 395 394 391 396 178 542 392 176 536 359 690 536 690 176 167 176 536 420 535 359 536 176 535 535 160 787 176
+193 162 194 66 359 357 160 175 396 392 713 131 543 356 388 161 545 354 538 56 546 313 388 547 520 387 522 508 545 397 541 398
+760 391 169 243 760 712 711 777 760 355 761 758 717 282 763 283 390 397 389 392 548 551 390 520 713 391 131 757 58 283 264 739
+207 39 57 41 180 264 58 59 717 264 761 281 687 686 180 775 687 686 414 180 411 60 41 202 411 60 202 204 411 39 41 60
+317 375 183 185 186 2 277 164 469 533 488 480 469 318 488 533 481 186 470 183 469 482 584 266 478 592 461 279 478 483 279 482
+469 483 266 477 365 188 189 187 484 483 477 99 481 378 183 470 469 318 380 266 481 184 186 183 431 90 339 314 481 430 431 378
+375 184 183 185 438 488 585 475 496 489 108 62 484 365 189 492 63 61 62 64 63 192 61 64 163 492 366 62 496 492 489 62
+496 108 98 64 63 366 62 61 543 162 161 388 537 538 499 517 162 388 65 161 181 313 547 388 537 497 490 491 537 499 381 517
+537 499 490 381 496 63 366 62 537 544 495 63 496 63 62 64 403 440 480 446 469 430 378 191 438 480 580 71 403 446 71 444
+403 523 444 402 521 495 190 479 521 448 517 445 521 448 77 398 545 382 508 495 506 211 182 547 403 426 425 377 521 426 190 508
+544 63 64 192 537 544 63 64 543 162 67 382 543 546 67 388 382 67 546 387 181 313 388 67 546 313 67 388 545 397 398 429
+359 193 175 161 162 67 65 388 193 161 66 175 543 193 161 162 543 162 388 67 506 313 547 182 390 429 551 389 390 134 387 313
+390 520 387 134 390 313 506 134 427 203 362 69 427 203 428 362 548 137 511 416 549 144 509 510 518 401 70 617 784 616 617 70
+518 135 617 548 548 525 135 617 520 415 509 404 549 41 287 68 195 69 362 198 195 69 198 197 518 376 424 425 423 70 401 715
+513 195 516 427 195 427 362 69 364 196 197 91 423 315 425 433 513 427 362 195 423 70 715 405 432 195 196 424 364 142 199 201
+432 472 372 275 437 338 434 90 437 432 275 434 437 373 90 434 144 413 428 143 784 549 144 509 411 207 39 205 549 411 41 202
+331 330 602 325 532 152 450 213 327 462 329 334 329 462 327 152 329 326 450 152 468 214 465 215 532 467 155 296 582 215 463 117
+576 440 446 530 450 528 574 443 329 441 331 328 530 485 440 533 450 152 532 329 582 360 215 117 532 582 328 468 337 331 602 325
+443 326 450 441 576 381 517 445 574 601 361 643 530 440 480 533 716 446 444 71 451 523 402 444 582 581 223 360 582 218 216 215
+218 219 360 223 451 523 77 402 577 454 220 140 582 218 74 449 558 556 222 560 577 458 220 442 576 448 445 517 451 519 402 77
+618 401 519 135 577 454 442 220 564 452 406 589 558 76 406 452 571 81 227 591 558 76 73 406 558 555 74 223 558 76 560 72
+558 76 72 73 555 225 224 560 555 74 223 221 226 571 227 591 558 564 452 406 225 76 75 560 225 76 42 75 147 225 452 76
+562 80 82 231 564 226 572 455 564 457 455 572 618 70 401 617 564 226 456 452 564 453 452 589 564 563 591 229 565 563 82 591
+320 321 319 168 529 233 400 322 447 135 519 551 565 561 680 230 565 400 572 230 451 523 448 77 447 77 398 429 505 307 166 700
+639 504 578 235 646 504 640 578 529 233 322 647 307 166 700 78 643 607 234 647 644 361 601 643 643 421 458 234 644 648 606 421
+644 336 323 305 306 700 703 78 306 700 701 703 307 306 78 700 641 306 700 701 348 505 347 166 641 505 578 308 529 647 639 646
+570 235 639 349 565 319 568 502 566 567 236 573 460 569 79 570 565 460 319 502 565 563 709 82 562 80 81 82 532 154 155 467
+154 467 83 84 467 212 462 83 532 467 296 465 467 155 296 297 746 464 309 629 746 464 629 487 532 155 213 747 586 318 584 266
+558 582 583 223 582 74 140 449 716 480 71 580 558 555 554 140 555 556 224 221 476 73 293 406 476 97 290 72 558 97 222 553
+559 439 372 275 473 374 251 254 559 275 259 435 292 146 290 291 782 597 625 596 788 629 487 746 788 461 279 790 341 340 335 609
+341 340 342 335 340 609 756 276 340 776 756 274 367 370 369 86 165 661 369 368 367 369 368 165 280 461 790 279 367 370 87 164
+478 371 370 277 478 367 368 189 370 86 87 85 367 370 86 87 280 789 790 461 96 727 790 726 438 374 679 475 481 436 184 375
+249 584 436 481 335 373 679 434 88 89 679 335 679 374 438 373 436 374 89 373 4 89 374 254 437 679 438 373 437 90 373 375
+317 431 375 90 363 364 91 196 432 364 196 198 364 142 139 610 92 199 141 316 93 239 240 94 774 632 246 309 93 53 246 632
+774 93 94 239 280 96 369 371 478 660 789 461 96 789 95 790 95 789 96 722 789 788 461 721 478 369 280 789 661 53 773 660
+478 661 368 369 14 303 12 595 12 660 303 773 478 789 661 369 595 660 12 594 93 773 53 632 660 54 594 471 474 255 254 252
+436 585 584 255 473 255 374 254 434 435 679 343 259 435 343 679 341 343 275 434 559 435 439 275 259 275 97 273 259 474 273 97
+435 275 434 437 559 435 580 439 438 435 439 580 479 358 191 494 479 380 533 485 593 299 747 213 469 380 477 266 586 318 380 533
+575 448 56 398 521 520 522 508 545 448 56 517 529 233 234 442 496 108 498 98 500 300 151 498 496 358 494 366 500 300 527 151
+496 489 531 108 593 153 477 99 484 153 99 477 543 354 541 383 575 354 541 56 699 177 538 500 545 354 56 541 101 353 151 8
+101 8 151 6 500 353 491 497 496 353 98 498 100 385 101 119 100 385 119 118 104 100 385 101 104 100 101 103 102 104 101 103
+151 101 104 385 104 101 151 102 109 300 48 105 8 498 300 109 106 113 51 109 111 300 109 107 114 111 113 107 107 106 109 113
+107 106 300 109 531 110 108 489 111 113 110 238 113 110 531 111 109 300 111 531 111 110 112 238 301 111 112 238 238 111 113 114
+12 115 54 116 12 595 49 115 49 595 12 594 12 115 116 595 14 54 773 52 773 54 14 12 593 483 592 99 484 483 99 592
+469 318 533 380 582 581 117 579 586 532 579 582 557 581 586 598 479 600 485 490 496 600 490 497 593 534 300 527 496 153 497 498
+496 153 600 497 496 492 62 366 534 531 498 153 593 153 600 477 496 366 494 493 496 153 477 600 120 385 118 119 120 122 119 118
+121 120 122 119 386 123 659 385 121 120 119 385 123 121 122 659 123 121 659 385 420 536 172 657 699 172 698 697 699 172 536 657
+699 536 383 354 536 383 690 167 172 736 124 420 646 459 399 640 447 397 429 398 545 397 429 389 447 399 501 132 447 312 132 501
+390 547 211 506 545 542 397 390 575 397 541 542 447 397 392 389 503 295 511 416 565 400 230 680 503 418 133 568 520 415 404 134
+447 295 135 551 784 525 617 136 784 427 516 509 662 512 416 515 742 30 289 125 125 742 778 619 32 744 30 409 744 126 32 125
+513 677 126 742 784 677 136 616 677 125 126 742 513 743 744 428 662 677 616 136 514 665 729 663 675 666 729 669 729 666 741 669
+145 670 672 19 729 673 672 674 621 622 43 524 672 524 68 412 688 767 766 127 346 767 752 750 754 767 35 728 752 767 346 127
+687 767 728 35 503 759 762 511 447 552 389 757 762 310 605 763 503 762 137 511 390 552 757 389 310 169 391 312 761 777 759 762
+760 777 391 759 503 312 501 133 129 131 396 128 131 312 132 392 131 312 757 391 130 131 132 178 312 131 130 129 542 178 396 689
+130 129 131 396 130 131 312 132 447 759 757 312 503 552 295 447 390 552 759 757 503 310 312 133 520 415 134 506 618 525 617 135
+618 525 135 295 548 525 416 295 662 512 136 525 784 512 136 676 662 512 676 136 784 512 525 136 662 512 525 416 549 68 624 524
+724 622 524 624 724 550 137 605 549 207 287 41 549 411 179 206 604 550 605 137 610 407 611 139 612 611 138 139 351 756 609 656
+341 609 351 756 432 409 407 139 432 472 408 372 472 351 341 610 559 472 372 408 476 273 776 290 432 610 434 338 432 363 433 196
+614 472 408 407 432 363 199 316 432 364 198 139 423 433 425 424 423 714 439 372 559 714 372 439 588 444 714 716 716 714 439 71
+558 454 140 554 451 523 444 446 403 523 446 444 363 199 141 201 364 196 198 197 363 199 316 141 364 142 610 199 144 413 143 208
+413 208 202 204 549 144 208 413 784 144 427 509 784 144 428 427 784 743 428 413 514 513 742 778 672 670 145 621 662 671 512 515
+673 43 145 241 621 672 524 43 476 292 73 72 292 146 73 72 764 476 472 776 771 769 770 29 147 225 456 452 226 453 227 228
+564 226 571 572 565 562 230 571 565 230 572 571 724 740 739 417 723 708 706 707 80 232 237 231 573 232 231 237 565 561 232 569
+573 236 704 567 569 320 319 168 771 410 148 32 292 293 148 476 148 771 32 149 614 148 667 293 696 697 698 694 738 386 172 684
+386 385 659 352 738 386 681 737 738 657 386 659 699 386 385 150 500 526 385 151 500 526 151 527 574 443 601 526 644 324 305 323
+329 326 331 441 329 326 152 327 326 441 325 331 574 450 527 593 574 443 526 527 325 441 326 443 586 534 530 485 593 534 600 153
+301 112 49 595 111 49 301 112 534 593 531 153 593 299 213 450 593 747 299 301 301 111 300 531 107 301 111 300 532 332 155 747
+532 154 467 462 532 154 213 155 154 467 462 83 154 156 157 83 303 15 52 773 746 660 783 303 158 306 304 703 352 122 659 419
+419 160 122 787 419 359 357 160 45 352 103 119 352 122 357 103 659 122 352 119 543 352 45 159 543 544 162 382 419 357 122 160
+543 359 161 193 193 162 66 65 193 162 65 161 543 539 352 357 47 353 45 101 543 544 193 162 103 47 101 46 45 47 539 98
+103 357 352 539 544 193 162 194 543 540 539 193 163 492 187 493 484 492 189 471 163 492 493 366 478 367 164 370 478 367 370 369
+367 86 369 165 367 188 164 87 701 644 645 641 696 641 701 645 696 645 694 698 641 695 308 696 698 638 645 642 641 347 307 647
+505 347 166 307 384 536 167 383 699 384 172 422 384 171 167 635 172 386 698 684 699 172 386 698 384 383 167 171 646 399 422 640
+348 505 578 347 565 561 570 680 680 570 639 349 460 569 319 168 529 460 504 570 646 459 501 399 322 639 680 529 565 529 502 503
+565 529 680 570 723 311 418 567 503 552 759 511 447 503 552 759 391 169 310 777 310 759 777 762 169 310 777 711 723 704 707 706
+723 565 503 724 723 417 418 311 566 321 599 170 565 320 232 573 740 417 58 739 749 779 767 780 132 542 171 178 171 542 132 384
+575 384 422 399 575 354 383 541 699 536 354 177 699 384 536 172 384 172 167 536 17 173 384 167 174 167 384 172 535 536 543 359
+359 193 357 175 535 419 657 420 535 359 160 176 543 536 354 383 543 536 177 354 690 383 536 356 543 356 359 536 689 396 691 178
+396 542 356 393 543 393 388 356 713 392 757 131 390 547 355 211 760 391 243 713 604 552 511 759 760 355 713 243 604 759 511 762
+713 243 393 396 206 179 209 205 717 264 283 605 604 762 137 605 604 608 179 510 760 243 761 355 604 762 511 137 390 758 355 759
+390 211 758 507 717 603 758 264 549 411 207 41 603 180 739 414 739 180 264 58 414 180 739 766 604 758 603 507 608 206 179 209
+549 414 206 603 604 608 206 179 264 180 739 603 688 414 345 287 549 207 206 414 549 688 624 287 549 208 510 179 411 206 205 179
+549 207 414 287 207 414 287 345 608 210 506 211 181 313 182 547 478 365 186 164 186 183 184 185 481 277 186 184 478 365 470 186
+365 492 493 187 484 365 492 493 365 367 189 188 478 365 367 189 479 191 190 377 469 191 378 494 479 485 533 440 496 479 494 358
+479 490 485 381 479 358 190 191 544 540 192 64 537 544 382 495 537 544 64 540 544 540 193 194 543 544 540 193 546 387 67 313
+520 387 134 522 545 387 508 382 520 415 510 509 552 551 548 295 390 608 506 211 518 376 426 522 403 376 425 426 513 195 424 516
+513 432 424 195 432 195 198 196 195 196 197 198 432 364 610 199 432 363 316 433 363 91 201 200 363 364 199 201 437 432 434 338
+438 585 318 584 403 430 377 315 784 144 413 428 549 144 510 208 549 202 413 208 144 427 203 428 411 208 204 202 411 207 205 206
+549 411 206 207 549 411 202 208 608 206 209 507 604 603 206 507 608 210 211 507 329 532 462 152 329 334 328 331 329 328 468 532
+468 467 465 212 582 468 216 328 532 154 152 213 468 216 214 215 582 468 215 216 218 216 215 217 582 218 223 74 582 218 449 298
+218 74 219 223 582 466 449 220 577 466 220 458 555 556 221 223 558 556 583 222 558 556 223 583 558 225 560 76 555 556 560 224
+558 555 225 554 558 225 76 452 564 554 454 558 716 714 71 444 226 453 591 227 226 453 228 452 564 453 229 591 564 453 589 229
+565 562 571 591 562 80 231 232 562 571 591 81 565 561 230 562 565 562 231 232 529 233 442 457 564 457 454 455 529 233 457 400
+447 519 429 551 529 233 647 234 451 523 445 448 644 421 606 361 441 337 325 331 644 607 648 421 641 607 647 307 645 644 701 305
+696 702 700 695 695 700 641 505 643 607 421 234 639 349 347 322 639 235 570 504 639 504 570 529 565 460 570 569 565 573 232 231
+565 503 502 568 320 319 565 569 565 460 569 319 662 618 503 416 565 562 82 231 567 573 565 707 565 320 569 232 566 236 599 573
+320 573 237 232 689 690 396 356 59 740 38 58 238 114 301 111 689 690 693 691 611 609 655 351 611 610 609 351 278 269 371 268
+239 240 53 93 249 268 4 269 611 610 142 612 611 770 138 139 514 672 671 729 242 621 21 43 514 791 742 741 43 621 241 242
+441 443 361 528 673 241 36 43 760 711 761 243 615 289 125 742 764 756 351 656 785 764 771 407 764 653 351 655 764 786 748 652
+772 770 649 748 772 651 770 769 772 649 770 651 764 351 653 654 764 351 654 656 650 652 244 350 783 753 661 245 774 93 753 262
+774 262 94 93 261 627 734 661 774 303 773 631 774 303 309 783 774 93 246 632 721 628 661 789 721 660 789 661 721 789 660 461
+782 596 95 790 721 629 734 625 721 734 783 261 745 486 788 279 721 746 629 788 721 722 625 628 746 595 332 747 267 587 270 247
+482 584 267 249 587 250 725 267 267 1 587 247 267 255 248 249 267 271 587 1 267 249 248 271 587 270 250 267 585 557 553 475
+473 474 254 251 557 587 553 598 587 255 253 248 474 553 585 587 474 255 252 256 587 255 256 253 254 255 474 473 473 260 474 251
+553 474 435 259 255 256 474 587 473 258 374 679 473 585 475 374 259 273 258 257 258 273 259 473 259 257 88 273 259 343 275 273
+259 263 273 88 260 473 273 258 587 486 584 267 584 587 255 585 721 629 625 788 721 261 783 661 309 629 262 783 753 262 629 783
+746 745 660 303 774 262 309 94 774 93 239 246 259 275 559 97 553 559 259 435 263 273 343 259 259 97 553 474 435 473 474 475
+351 609 341 610 274 340 776 342 611 142 610 139 612 350 786 611 609 655 351 653 350 351 786 611 770 611 748 786 772 770 748 786
+764 655 350 650 717 758 761 264 717 761 282 281 717 761 763 282 717 283 264 281 717 762 605 763 283 417 281 282 283 417 265 281
+310 417 605 763 478 280 279 461 369 96 280 789 280 369 478 371 782 790 486 278 469 483 482 266 481 371 277 268 478 481 186 470
+478 481 371 277 279 278 482 486 486 788 625 487 486 483 482 279 486 625 579 487 486 625 725 579 486 483 266 482 586 483 486 745
+782 790 788 486 267 584 482 486 782 726 790 727 278 280 96 790 278 269 270 250 278 268 371 280 278 269 96 371 790 727 96 278
+481 482 249 584 268 278 481 280 481 278 268 249 247 278 269 270 249 269 4 271 249 268 436 4 269 278 247 249 278 267 482 486
+341 343 434 335 274 272 343 342 342 343 335 272 272 343 273 274 273 343 272 5 341 343 756 275 341 472 275 756 341 351 472 756
+756 609 340 341 274 343 275 756 274 472 756 275 274 472 776 756 756 776 340 276 340 343 342 274 481 268 277 184 278 482 481 280
+278 790 279 280 587 486 586 584 782 788 625 486 482 278 279 280 790 279 486 278 761 777 762 763 717 281 282 283 763 417 282 284
+763 417 284 285 605 417 310 503 763 310 417 285 346 687 767 286 687 686 346 688 687 180 414 766 775 731 180 687 346 686 687 286
+728 686 775 732 687 767 35 766 688 287 127 624 549 414 739 766 549 688 287 414 688 767 127 346 678 677 125 126 678 677 778 125
+678 126 148 614 125 148 32 31 667 125 619 288 125 289 148 31 476 292 72 290 559 472 275 372 476 97 72 222 764 476 771 407
+274 776 476 273 614 406 379 589 614 293 667 379 614 771 148 476 776 292 290 291 291 771 650 776 776 476 292 771 776 476 290 292
+771 291 292 776 667 148 125 288 614 476 148 293 755 286 294 765 723 740 311 706 755 294 705 765 755 294 733 705 754 705 731 730
+755 733 728 732 410 771 407 770 771 770 410 32 614 407 771 476 620 617 662 616 662 621 671 524 662 621 524 622 618 525 295 416
+662 563 724 565 675 778 676 729 778 675 667 791 620 379 670 344 675 663 665 670 668 671 729 676 593 592 594 302 595 301 302 49
+745 592 279 461 745 483 279 592 746 595 303 332 746 745 487 788 746 333 464 296 746 155 297 296 746 296 463 532 746 333 296 297
+487 463 746 464 532 528 450 441 593 299 527 300 582 441 298 466 593 595 747 301 593 301 299 300 593 595 301 302 746 783 309 303
+721 746 660 783 746 309 333 303 644 306 304 648 644 324 648 304 644 306 701 304 644 324 701 305 641 607 306 648 641 306 607 307
+696 645 698 638 308 695 641 505 746 309 783 629 418 310 417 503 503 310 762 759 605 310 762 503 662 724 515 416 723 724 709 565
+724 550 605 739 417 605 763 283 723 418 503 568 723 740 417 311 724 739 624 550 723 417 740 724 754 705 623 755 723 765 705 708
+740 723 623 705 724 708 710 765 754 623 724 766 529 459 501 646 447 503 312 501 646 529 447 503 133 310 418 503 503 312 759 447
+724 709 708 723 390 313 547 506 520 608 415 506 390 713 392 757 390 758 211 355 390 604 507 758 390 608 211 507 393 546 390 545
+543 393 356 542 393 392 396 542 546 67 382 543 545 382 546 387 338 314 90 437 314 338 316 437 314 92 338 339 316 338 314 92
+437 315 316 314 437 315 433 316 437 439 435 275 437 275 372 439 183 375 431 481 481 431 183 378 431 375 183 317 438 585 488 318
+565 319 320 566 599 321 566 320 565 566 568 319 319 321 320 566 639 348 578 347 235 348 639 349 680 639 322 349 639 347 647 322
+644 324 323 606 644 324 606 648 601 361 336 325 606 323 336 337 601 443 361 325 644 361 643 421 329 326 327 330 329 326 330 331
+468 328 329 334 329 326 441 450 528 443 450 441 334 330 331 329 746 332 303 333 327 330 334 329 335 343 342 341 336 337 323 602
+771 292 148 476 746 332 333 297 486 279 790 788 723 705 706 708 90 314 338 339 340 343 756 341 342 343 340 341 655 609 611 612
+620 670 621 344 345 346 688 287 688 346 345 686 347 348 349 639 679 258 259 473 764 786 350 351 585 474 255 473 756 343 340 274
+438 584 436 585 481 482 278 249 436 584 249 255 481 436 249 268 481 280 371 268 469 430 377 403 478 280 482 279 437 431 90 375
+437 430 314 431 403 438 481 430 500 385 352 45 699 386 352 385 575 699 538 500 500 353 151 45 500 353 497 498 534 500 300 527
+643 421 361 458 534 528 530 576 534 500 498 300 638 699 422 575 538 699 575 354 699 575 384 422 643 234 442 529 538 500 576 575
+517 538 576 575 699 575 354 383 575 397 542 392 575 384 542 383 447 312 757 392 390 393 355 547 521 523 77 448 521 523 448 445
+543 356 383 542 521 519 429 77 543 359 193 357 543 539 357 193 479 358 495 190 393 546 388 547 537 540 98 539 543 356 161 359
+582 218 215 360 582 218 360 223 586 318 266 380 586 318 488 585 582 581 583 223 716 480 580 488 586 318 585 584 532 582 441 328
+528 466 361 441 716 71 439 580 532 487 486 579 528 466 458 361 532 582 465 463 582 468 465 215 558 476 222 97 577 457 442 454
+513 409 198 362 432 472 610 407 432 364 363 196 432 409 410 407 513 744 126 409 513 409 362 744 513 409 126 410 432 363 364 199
+610 432 341 472 432 199 610 338 437 432 338 316 484 470 365 493 484 489 302 531 478 660 471 368 496 492 366 493 484 471 594 302
+478 660 594 471 478 367 369 368 496 484 153 531 482 483 469 478 478 481 277 186 478 371 369 370 479 600 380 485 478 481 280 371
+437 403 423 315 437 432 372 275 585 436 374 255 374 436 585 438 438 481 430 431 437 423 372 432 423 714 715 444 438 373 374 436
+438 481 375 436 469 584 438 318 436 584 438 481 438 481 431 375 423 403 438 71 403 438 71 480 423 403 425 315 437 430 403 315
+469 438 480 488 521 403 402 523 469 438 488 318 479 381 440 445 518 376 516 424 518 423 401 402 377 430 469 191 469 481 378 430
+398 77 451 448 716 557 488 580 530 582 528 532 528 441 582 466 593 532 213 747 329 532 450 441 620 589 614 379 469 377 479 403
+479 403 377 426 479 403 446 440 521 495 545 508 479 403 523 446 521 545 517 448 521 537 517 545 469 479 477 380 477 496 479 484
+496 358 63 495 477 153 496 484 469 481 470 378 496 358 366 63 537 353 491 45 496 353 498 497 496 537 497 490 545 495 521 537
+495 479 521 537 496 495 479 358 543 539 45 352 537 545 495 382 537 479 381 490 537 479 517 381 538 545 517 537 517 545 538 56
+543 545 354 538 543 537 382 545 638 699 575 500 699 500 150 385 699 575 383 384 638 641 645 642 699 500 385 352 699 384 383 536
+738 699 386 657 699 386 657 352 543 419 352 657 681 737 419 659 352 419 543 357 535 419 160 359 419 122 737 787 419 657 420 658
+390 545 546 387 388 546 393 543 390 545 389 397 390 713 355 393 390 546 313 387 390 545 429 389 390 520 429 545 390 545 542 393
+397 542 545 541 390 546 547 313 391 759 713 760 713 243 396 395 131 394 713 396 392 390 393 713 394 395 713 396 295 552 503 511
+575 451 398 447 646 451 575 447 447 575 397 398 447 575 392 397 398 451 575 448 447 552 757 759 759 312 503 310 447 575 399 132
+646 447 575 399 529 572 400 457 447 575 132 392 618 447 135 519 618 447 295 135 451 447 519 77 451 618 444 402 444 588 618 530
+588 714 715 405 557 577 530 582 618 447 503 295 588 559 714 405 716 557 588 530 423 432 613 408 423 432 433 424 518 403 376 425
+423 432 408 372 513 423 432 613 423 403 444 402 423 403 71 444 437 423 433 315 423 403 437 438 70 401 518 423 518 376 404 516
+518 423 402 403 437 423 439 372 518 516 404 509 71 439 438 423 513 427 428 362 618 588 444 715 715 70 588 405 620 564 618 588
+620 613 70 616 620 588 70 614 559 614 408 405 614 476 293 406 558 564 614 588 125 148 126 32 771 407 614 410 148 614 126 410
+678 614 148 667 614 476 472 407 559 614 472 408 513 677 742 778 514 513 784 743 518 70 784 617 613 423 70 513 70 423 613 405
+126 744 742 125 518 513 424 516 410 613 432 408 513 432 409 410 195 198 513 432 513 432 198 409 513 423 424 432 549 411 208 179
+549 550 524 624 549 202 412 413 784 549 413 144 784 515 671 524 549 688 414 766 604 608 507 206 784 512 671 515 549 688 766 624
+510 520 608 415 548 512 416 525 548 525 295 135 662 724 416 503 724 503 137 416 724 503 605 137 724 550 624 524 623 622 624 127
+623 624 724 766 723 503 417 724 724 765 710 622 417 503 723 418 724 503 417 605 563 724 709 710 754 724 739 766 543 536 535 657
+535 543 419 359 420 736 787 735 658 420 787 735 736 176 535 787 535 176 736 420 643 529 647 234 644 643 607 421 696 699 698 697
+646 422 575 638 638 699 150 698 638 699 697 422 574 638 575 500 638 500 526 150 574 638 526 601 643 646 575 638 548 549 509 510
+608 520 510 604 518 513 423 424 518 423 425 424 518 423 403 425 784 513 516 427 521 479 426 403 784 513 427 428 521 520 545 429
+784 549 412 413 403 430 437 438 437 438 435 439 437 432 316 433 437 438 431 375 437 438 430 431 438 475 679 435 437 423 432 433
+434 275 435 343 438 679 437 435 375 373 438 436 437 423 438 439 679 435 434 437 576 500 538 499 534 576 490 499 446 440 576 445
+576 575 448 517 576 530 485 440 586 600 534 485 577 528 466 458 534 153 497 600 582 441 528 532 442 234 643 458 361 443 441 325
+441 361 325 337 574 534 500 576 643 574 601 638 576 446 451 530 451 618 530 444 451 576 448 445 451 618 402 519 451 618 519 447
+451 530 446 444 451 446 576 445 530 618 577 588 577 618 530 451 577 528 576 530 529 451 447 618 451 576 575 448 643 577 528 576
+646 451 576 575 577 582 220 466 140 220 582 449 450 528 532 593 577 451 530 576 529 618 447 503 564 577 618 588 557 558 577 582
+529 577 576 451 558 614 406 476 564 614 589 406 564 226 453 591 564 226 452 453 558 564 588 577 529 564 577 618 572 564 529 565
+564 577 454 457 620 564 588 614 564 226 455 456 564 455 554 456 558 577 140 454 565 562 591 82 558 577 582 140 529 564 457 577
+529 577 457 442 529 577 451 618 442 643 577 458 503 501 529 502 643 577 458 528 529 459 460 502 565 529 503 618 529 459 504 460
+641 578 639 646 646 639 504 578 532 582 463 117 746 532 332 155 532 582 117 579 721 746 788 461 329 468 462 532 463 296 746 464
+532 582 468 465 532 329 328 441 577 528 582 466 532 530 582 586 745 486 487 788 532 468 462 467 532 468 467 465 462 468 329 334
+600 586 477 380 469 478 470 481 478 484 470 365 478 484 365 189 478 481 482 280 478 280 461 789 478 484 592 483 478 484 189 471
+368 661 478 660 478 660 661 789 478 484 594 592 584 482 469 481 478 484 471 594 97 275 476 273 557 558 583 553 476 472 275 559
+559 614 476 472 435 679 473 475 559 614 405 588 557 553 475 559 557 488 475 585 557 586 582 530 559 435 475 580 473 585 474 475
+558 559 614 476 469 584 318 266 469 479 484 477 403 479 533 440 533 479 403 469 469 403 438 481 469 478 484 470 469 479 494 484
+469 403 533 480 480 438 469 403 469 484 483 477 469 478 481 482 469 478 483 484 469 403 481 430 469 479 191 494 479 377 469 191
+485 586 533 530 528 582 530 577 266 486 584 586 584 486 266 482 486 487 532 745 745 593 586 483 557 586 587 598 586 716 557 488
+716 480 586 530 586 530 480 533 725 598 486 579 557 586 488 585 496 153 498 531 496 484 531 489 479 496 477 600 496 537 490 479
+496 484 489 492 496 537 63 64 496 537 64 98 496 537 98 353 353 497 537 491 496 479 490 600 496 484 492 493 496 479 484 494
+496 484 493 494 496 537 495 63 479 495 496 537 574 534 576 528 593 534 528 530 534 153 498 497 574 500 527 526 534 576 530 485
+485 490 576 381 534 500 497 498 534 500 499 491 534 500 491 497 534 576 499 500 575 422 646 399 646 529 451 447 646 529 576 451
+643 574 576 528 646 638 308 422 646 529 459 504 646 447 399 501 646 447 501 503 501 459 529 502 529 501 503 646 639 647 529 322
+641 639 647 646 646 529 504 639 641 505 347 578 641 505 307 347 390 520 134 506 390 548 604 552 390 551 552 389 548 518 135 519
+390 520 506 608 520 518 522 404 520 518 404 509 551 429 390 520 548 520 509 518 390 387 520 545 604 548 137 511 390 604 608 507
+520 545 387 508 521 520 508 545 521 518 426 522 784 548 509 518 548 520 510 509 604 510 548 520 548 549 550 515 604 548 550 137
+552 548 511 295 520 519 518 548 784 548 518 617 511 548 552 604 617 135 518 401 548 520 519 551 784 549 515 524 784 548 525 512
+514 671 784 676 784 513 677 616 784 548 512 515 784 548 617 525 784 518 513 423 784 513 428 743 784 513 616 70 784 513 70 423
+514 784 671 412 514 671 676 729 524 412 549 68 784 548 515 549 784 518 516 513 784 548 549 509 784 518 509 516 521 545 448 398
+521 545 398 429 190 495 521 508 521 479 523 445 521 537 479 517 519 429 520 521 521 479 381 517 521 479 445 381 521 518 519 402
+518 519 520 521 521 479 190 426 521 520 518 522 521 518 402 403 523 403 479 521 521 518 403 426 564 620 563 229 620 563 590 662
+662 724 622 524 565 529 618 564 723 707 565 709 662 724 524 515 662 618 416 525 503 416 618 295 574 638 500 526 574 534 527 500
+593 574 534 527 643 574 528 361 574 528 361 443 574 576 500 575 643 646 576 575 643 646 529 576 593 595 302 594 745 593 592 594
+593 301 531 302 746 532 747 332 532 593 530 586 593 586 483 477 477 586 600 593 450 593 532 213 593 301 300 531 745 593 532 586
+594 745 660 461 532 528 530 593 531 593 534 300 533 586 485 380 527 450 574 443 530 534 586 593 593 574 528 534 574 528 450 593
+419 543 535 657 543 356 536 383 543 537 545 538 419 359 543 357 543 537 539 540 543 537 45 539 543 545 541 354 543 537 538 159
+543 545 393 542 393 543 546 545 543 537 540 544 543 537 159 45 543 545 542 541 390 546 393 547 546 382 545 543 543 537 544 382
+604 548 549 550 604 549 206 603 390 604 758 759 717 604 605 762 604 549 603 739 390 604 759 552 390 551 548 552 604 549 739 550
+604 549 179 206 548 510 604 549 604 549 510 179 557 558 553 559 558 564 406 614 558 564 554 456 454 554 564 455 558 564 577 454
+558 559 476 97 558 555 556 560 558 555 223 556 558 559 97 553 558 582 223 74 558 476 73 72 558 225 456 554 557 558 559 588
+558 559 588 614 558 555 560 225 709 724 563 565 622 590 724 662 662 565 724 503 662 565 503 618 620 565 618 564 565 529 400 680
+565 561 562 232 563 620 564 565 529 564 572 457 565 573 567 566 565 564 591 571 620 621 590 344 565 566 567 568 565 561 569 570
+565 529 570 460 565 564 571 572 400 572 529 565 565 566 320 573 643 574 638 575 641 646 638 308 361 601 574 443 643 574 575 576
+643 638 601 642 643 646 647 529 643 529 577 576 577 643 442 529 639 578 641 347 643 528 458 361 586 579 581 582 581 579 586 598
+553 557 585 587 716 580 559 557 557 582 581 583 586 581 557 582 557 558 582 583 557 559 475 580 587 586 585 584 585 586 587 557
+255 587 584 267 557 577 588 530 557 558 588 577 620 662 618 565 620 668 662 621 620 662 565 563 620 614 589 564 620 614 667 379
+620 678 667 614 620 678 613 616 620 588 618 70 620 564 589 229 724 563 662 590 591 563 564 565 620 618 617 70 745 593 483 592
+745 593 594 595 745 593 747 532 486 532 586 745 746 463 487 532 660 745 594 595 486 483 586 266 745 593 595 747 586 532 486 579
+782 596 790 597 275 343 259 435 586 486 598 579 696 702 645 701 566 599 320 573 534 600 586 593 601 361 644 336 337 602 336 325
+336 323 606 644 717 604 758 603 603 604 605 717 361 336 606 644 759 717 604 758 605 604 603 739 264 603 605 717 641 643 607 644
+641 643 642 638 644 643 601 642 390 604 520 608 609 610 611 612 585 553 474 475 420 787 419 658 737 122 419 659 620 613 678 614
+678 614 613 126 664 729 663 791 148 125 18 615 675 667 791 663 675 670 667 663 620 668 670 667 620 614 70 613 662 618 525 617
+668 671 621 672 620 662 678 616 620 662 617 618 619 778 667 791 668 729 671 672 662 678 616 677 664 663 729 665 664 665 729 666
+675 665 666 670 667 675 778 678 668 672 674 729 620 668 667 678 620 668 678 662 620 621 670 668 668 662 676 677 749 767 751 750
+749 767 779 751 749 767 750 752 781 622 127 752 623 724 622 765 623 766 127 624 623 624 622 724 721 625 626 628 721 625 627 626
+721 625 734 627 721 782 788 625 721 789 722 628 721 746 783 629 721 783 734 629 774 239 630 309 774 239 94 630 774 309 303 631
+774 309 631 632 718 682 658 683 720 658 657 659 720 172 735 684 633 636 690 634 633 634 690 635 633 634 635 171 633 636 692 690
+633 636 171 692 637 658 685 682 637 787 685 719 637 682 787 683 641 643 638 646 641 643 647 607 647 639 641 347 641 646 308 640
+641 646 640 578 641 642 643 644 641 700 306 307 644 642 645 641 645 642 644 305 641 643 646 647 641 644 306 701 641 644 648 306
+641 644 607 648 785 650 748 649 785 650 649 651 785 650 651 769 764 652 650 350 764 786 652 350 785 771 650 769 764 650 654 653
+764 650 656 654 764 650 653 655 764 650 776 656 764 655 351 350 782 95 789 790 278 267 250 270 278 247 267 270 267 278 725 486
+725 278 250 727 247 249 267 1 720 718 658 681 738 681 386 684 720 420 657 658 738 681 659 737 721 261 627 734 721 783 660 661
+721 261 626 627 721 261 628 626 734 783 661 245 785 772 769 651 785 772 651 649 785 772 649 748 668 662 677 678 664 675 791 663
+664 675 663 665 668 675 729 669 675 670 666 669 664 675 665 666 668 621 670 672 668 675 670 667 668 675 667 678 677 675 678 778
+668 670 669 674 668 675 669 670 621 671 668 662 668 662 671 676 514 741 663 791 514 741 665 663 514 742 791 778 729 665 741 666
+672 673 729 514 514 742 743 741 514 673 412 672 729 741 673 674 514 412 743 784 664 675 666 729 668 675 676 729 668 675 677 676
+675 778 677 676 678 675 677 668 275 476 274 472 473 679 435 259 639 570 680 529 718 684 683 681 738 718 681 684 720 684 735 719
+718 684 682 683 718 684 685 682 718 684 719 685 720 719 658 718 180 731 35 687 686 687 286 728 687 775 728 686 687 688 767 766
+687 688 766 414 686 414 688 687 356 542 396 689 689 396 690 691 689 178 692 171 690 356 542 383 689 178 691 693 542 356 690 689
+689 693 692 178 396 178 131 130 393 390 392 542 696 695 697 694 696 695 694 702 696 645 638 641 308 697 638 696 696 638 699 697
+696 638 698 699 696 641 700 701 702 700 701 696 701 700 702 703 723 311 704 706 723 311 567 704 723 705 740 706 723 565 567 568
+723 565 568 503 723 708 707 709 723 765 623 705 723 708 724 765 708 709 724 710 760 761 712 777 760 761 711 712 760 243 169 711
+760 391 777 169 760 759 355 758 713 759 390 355 717 759 761 760 716 559 439 714 716 530 444 446 716 559 580 439 716 557 559 588
+716 530 446 480 714 444 588 715 586 480 716 488 557 716 586 530 514 671 672 412 720 735 420 658 761 759 717 762 396 392 393 713
+720 738 718 681 720 719 718 684 720 738 659 657 720 738 172 684 720 738 657 420 782 725 486 625 721 782 722 789 782 597 726 725
+721 782 625 722 623 765 723 724 782 725 727 278 782 597 790 726 782 725 278 486 782 725 726 727 755 775 732 728 35 731 775 687
+673 741 729 514 754 35 730 731 754 730 34 705 755 294 728 733 754 775 35 731 755 765 780 286 732 733 705 755 734 629 783 245
+721 661 628 261 661 783 734 261 746 788 461 745 461 788 789 790 746 461 660 745 764 476 407 472 764 476 776 771 785 772 748 786
+735 172 420 736 738 699 172 386 738 699 657 172 659 737 386 738 420 172 720 738 658 719 787 637 754 34 730 35 659 681 738 720
+755 768 765 623 674 729 741 669 770 769 771 772 755 754 775 728 755 754 728 767 754 37 740 705 754 740 58 739 754 724 740 739
+754 623 740 724 514 791 663 729 741 665 729 514 742 743 513 514 513 743 742 744 487 629 788 625 746 745 595 747 746 745 303 595
+746 745 747 532 746 745 532 487 748 764 650 785 749 781 752 750 749 781 750 751 749 781 751 779 768 623 754 766 755 768 780 765
+755 768 767 780 766 623 781 768 127 622 781 623 749 781 127 752 767 781 766 127 781 623 622 765 781 768 623 765 774 783 753 661
+774 783 262 753 754 768 767 755 764 472 756 776 764 472 351 756 390 759 713 757 760 761 717 758 717 758 759 760 761 762 717 763
+748 786 764 785 650 764 748 652 761 777 763 285 623 705 754 740 780 767 781 768 780 768 781 765 766 781 767 768 781 780 779 749
+781 127 767 749 783 661 773 660 767 768 754 766 754 623 768 755 764 407 786 351 785 772 771 769 770 771 785 772 756 656 776 276
+773 783 303 774 676 778 784 514 770 785 786 772 785 771 770 407 775 754 705 731 776 656 756 764 759 777 761 760 705 765 623 755
+623 723 740 724 784 778 676 677 787 658 683 681 779 780 781 765 781 767 780 749 789 782 788 721 773 661 783 774 303 783 773 660
+778 513 784 514 784 513 778 677 786 785 770 407 786 407 764 785 683 658 787 637 713 759 391 757 759 355 713 760 788 782 789 790
+705 754 775 755 791 675 729 778 729 675 791 664 604 717 759 762
+ 3 6 9 12 15 18 21 24
+27 30 33 36 39 42 45 48
+51 54 57 60 63 66 69 72
+75 78 81 84 87 90 93 96
+99 102 105 108 111 114 117 120
+123 126 129 132 135 138 141 144
+147 150 153 156 159 162 165 168
+171 174 177 180 183 186 189 192
+195 198 201 204 207 210 213 216
+219 222 225 228 231 234 237 240
+243 246 249 252 255 258 261 264
+267 270 273 276 279 282 285 288
+291 294 297 300 303 306 309 312
+315 318 321 324 327 330 333 336
+339 342 345 348 351 354 357 360
+363 366 369 372 375 378 381 384
+387 390 393 396 399 402 405 408
+411 414 417 420 423 426 429 432
+435 438 441 444 447 450 453 456
+459 462 465 468 471 474 477 480
+483 486 489 492 495 498 501 504
+507 510 513 516 519 522 525 528
+531 534 537 540 543 546 549 552
+555 558 561 564 567 570 573 576
+579 582 585 588 591 594 597 600
+603 606 609 612 615 618 621 624
+627 630 633 636 639 642 645 648
+651 654 657 660 663 666 669 672
+675 678 681 684 687 690 693 696
+699 702 705 708 711 714 717 720
+723 726 729 732 735 738 741 744
+747 750 753 756 759 762 765 768
+771 774 777 780 783 786 789 792
+795 798 801 804 807 810 813 816
+819 822 825 828 831 834 837 840
+843 846 849 852 855 858 861 864
+867 870 873 876 879 882 885 888
+891 894 897 900 903 906 909 912
+915 918 921 924 927 930 933 936
+939 942 945 948 951 954 957 960
+963 966 969 972 975 978 981 984
+987 990 993 996 999 1002 1005 1008
+1011 1014 1017 1020 1023 1026 1029 1032
+1035 1038 1041 1044 1047 1050 1053 1056
+1059 1062 1065 1068 1071 1074 1077 1080
+1083 1086 1089 1092 1095 1098 1101 1104
+1107 1110 1113 1116 1119 1122 1125 1128
+1131 1134 1137 1140 1143 1146 1149 1152
+1155 1158 1161 1164 1167 1170 1173 1176
+1179 1182 1185 1188 1191 1194 1197 1200
+1203 1206 1209 1212 1215 1218 1221 1224
+1227 1230 1233 1236 1239 1242 1245 1248
+1251 1254 1257 1260 1263 1266 1269 1272
+1275 1278 1281 1284 1287 1290 1293 1296
+1299 1302 1305 1308 1311 1314 1317 1320
+1323 1326 1329 1332 1335 1338 1341 1344
+1347 1350 1353 1356 1359 1362 1365 1368
+1371 1374 1377 1380 1383 1386 1389 1392
+1395 1398 1401 1404 1407 1410 1413 1416
+1419 1422 1425 1428 1431 1434 1437 1440
+1443 1446 1449 1452 1455 1458 1461 1464
+1467 1470 1473 1476 1479 1482 1485 1488
+1491 1494 1497 1500 1503 1506 1509 1512
+1515 1518 1521 1524 1527 1530 1533 1536
+1539 1542 1545 1548 1551 1554 1557 1560
+1563 1566 1569 1572 1575 1578 1581 1584
+1587 1590 1593 1596 1599 1602 1605 1608
+1611 1614 1617 1620 1623 1626 1629 1632
+1635 1638 1641 1644 1647 1650 1653 1656
+1659 1662 1665 1668 1671 1674 1677 1680
+1683 1686 1689 1692 1695 1698 1701 1704
+1707 1710 1713 1716 1719 1722 1725 1728
+1731 1734 1737 1740 1743 1746 1749 1752
+1755 1758 1761 1764 1767 1770 1773 1776
+1779 1782 1785 1788 1791 1794 1797 1800
+1803 1806 1809 1812 1815 1818 1821 1824
+1827 1830 1833 1836 1839 1842 1845 1848
+1851 1854 1857 1860 1863 1866 1869 1872
+1875 1878 1881 1884 1887 1890 1893 1896
+1899 1902 1905 1908 1911 1914 1917 1920
+1923 1926 1929 1932 1935 1938 1941 1944
+1947 1950 1953 1956 1959 1962 1965 1968
+1971 1974 1977 1980 1983 1986 1989 1992
+1995 1998 2001 2004 2007 2010 2013 2016
+2019 2022 2025 2028 2031 2034 2037 2040
+2043 2046 2049 2052 2055 2058 2061 2064
+2067 2070 2073 2076 2079 2082 2085 2088
+2091 2094 2097 2100 2103 2106 2109 2112
+2115 2118 2121 2124 2127 2130 2133 2136
+2139 2142 2145 2148 2151 2154 2157 2160
+2163 2166 2169 2172 2175 2178 2181 2184
+2187 2190 2193 2196 2199 2202 2205 2208
+2211 2214 2217 2220 2223 2226 2229 2232
+2235 2238 2241 2244 2247 2250 2253 2256
+2259 2262 2265 2268 2271 2274 2277 2280
+2283 2286 2289 2292 2295 2298 2301 2304
+2307 2310 2313 2316 2319 2322 2325 2328
+2331 2334 2337 2340 2343 2346 2349 2352
+2355 2358 2361 2364 2367 2370 2373 2376
+2379 2382 2385 2388 2391 2394 2397 2400
+2403 2406 2409 2412 2415 2418 2421 2424
+2427 2430 2433 2436 2439 2442 2445 2448
+2451 2454 2457 2460 2463 2466 2469 2472
+2475 2478 2481 2484 2487 2490 2493 2496
+2499 2502 2505 2508 2511 2514 2517 2520
+2523 2526 2529 2532 2535 2538 2541 2544
+2547 2550 2553 2556 2559 2562 2565 2568
+2571 2574 2577 2580 2583 2586 2589 2592
+2595 2598 2601 2604 2607 2610 2613 2616
+2619 2622 2625 2628 2631 2634 2637 2640
+2643 2646 2649 2652 2655 2658 2661 2664
+2667 2670 2673 2676 2679 2682 2685 2688
+2691 2694 2697 2700 2703 2706 2709 2712
+2715 2718 2721 2724 2727 2730 2733 2736
+2739 2742 2745 2748 2751 2754 2757 2760
+2763 2766 2769 2772 2775 2778 2781 2784
+2787 2790 2793 2796 2799 2802 2805 2808
+2811 2814 2817 2820 2823 2826 2829 2832
+2835 2838 2841 2844 2847 2850 2853 2856
+2859 2862 2865 2868 2871 2874 2877 2880
+2883 2886 2889 2892 2895 2898 2901 2904
+2907 2910 2913 2916 2919 2922 2925 2928
+2931 2934 2937 2940 2943 2946 2949 2952
+2955 2958 2961 2964 2967 2970 2973 2976
+2979 2982 2985 2988 2991 2994 2997 3000
+3003 3006 3009 3012 3015 3018 3021 3024
+3027 3030 3033 3036 3039 3042 3045 3048
+3051 3054 3057 3060 3063 3066 3069 3072
+3075 3078 3081 3084 3087 3090 3093 3096
+3099 3102 3105 3108 3111 3114 3117 3120
+3123 3126 3129 3132 3135 3138 3141 3144
+3147 3150 3153 3156 3159 3162 3165 3168
+3171 3174 3177 3180 3183 3186 3189 3192
+3195 3198 3201 3204 3207 3210 3213 3216
+3219 3222 3225 3228 3231 3234 3237 3240
+3243 3246 3249 3252 3255 3258 3261 3264
+3267 3270 3273 3276 3279 3282 3285 3288
+3291 3294 3297 3300 3303 3306 3309 3312
+3315 3318 3321 3324 3327 3330 3333 3336
+3339 3342 3345 3348 3351 3354 3357 3360
+3363 3366 3369 3372 3375 3378 3381 3384
+3387 3390 3393 3396 3399 3402 3405 3408
+3411 3414 3417 3420 3423 3426 3429 3432
+3435 3438 3441 3444 3447 3450 3453 3456
+3459 3462 3465 3468 3471 3474 3477 3480
+3483 3486 3489 3492 3495 3498 3501 3504
+3507 3510 3513 3516 3519 3522 3525 3528
+3531 3534 3537 3540 3543 3546 3549 3552
+3555 3558 3561 3564 3567 3570 3573 3576
+3579 3582 3585 3588 3591 3594 3597 3600
+3603 3606 3609 3612 3615 3618 3621 3624
+3627 3630 3633 3636 3639 3642 3645 3648
+3651 3654 3657 3660 3663 3666 3669 3672
+3675 3678 3681 3684 3687 3690 3693 3696
+3699 3702 3705 3708 3711 3714 3717 3720
+3723 3726 3729 3732 3735 3738 3741 3744
+3747 3750 3753 3756 3759 3762 3765 3768
+3771 3774 3777 3780 3783 3786 3789 3792
+3795 3798 3801 3804 3807 3810 3813 3816
+3819 3822 3825 3828 3831 3834 3837 3840
+3843 3846 3849 3852 3855 3858 3861 3864
+3867 3870 3873 3876 3879 3882 3885 3888
+3891 3894 3897 3900 3903 3906 3909 3912
+3915 3918 3921 3924 3927 3930 3933 3936
+3939 3942 3945 3948 3951 3954 3957 3960
+3963 3966 3969 3972 3975 3978 3981 3984
+3987 3990 3993 3996 3999 4002 4005 4008
+4011 4014 4017 4020 4023 4026 4029 4032
+4035 4038 4041 4044 4047 4050 4053 4056
+4059 4062 4065 4068 4071 4074 4077 4080
+4083 4086 4089 4092 4095 4098 4101 4104
+4107 4110 4113 4116 4119 4122 4125 4128
+4131 4134 4137 4140 4143 4146 4149 4152
+4155 4158 4161 4164 4167 4170 4173 4176
+4179 4182 4185 4188 4191 4194 4197 4200
+4203 4206 4209 4212 4215 4218 4221 4224
+4227 4230 4233 4236 4239 4242 4245 4248
+4251 4254 4257 4260 4263 4266 4269 4272
+4275 4278 4281 4284 4287 4290 4293 4296
+4299 4302 4305 4308 4311 4314 4317 4320
+4323 4326 4329 4332 4335 4338 4341 4344
+4347 4350 4353 4356 4359 4362 4365 4368
+4371 4374 4377 4380 4383 4386 4389 4392
+4395 4398 4401 4404 4407 4410 4413 4416
+4419 4422 4425 4428 4431 4434 4437 4440
+4443 4446 4449 4452 4455 4458 4461 4464
+4467 4470 4473 4476 4479 4482 4485 4488
+4491 4494 4497 4500 4503 4506 4509 4512
+4515 4518 4521 4524 4527 4530 4533 4536
+4539 4542 4545 4548 4551 4554 4557 4560
+4563 4566 4569 4572 4575 4578 4581 4584
+4587 4590 4593 4596 4599 4602 4605 4608
+4611 4614 4617 4620 4623 4626 4629 4632
+4635 4638 4641 4644 4647 4650 4653 4656
+4659 4662 4665 4668 4671 4674 4677 4680
+4683 4686 4689 4692 4695 4698 4701 4704
+4707 4710 4713 4716 4719 4722 4725 4728
+4731 4734 4737 4740 4743 4746 4749 4752
+4755 4758 4761 4764 4767 4770 4773 4776
+4779 4782 4785 4788 4791 4794 4797 4800
+4803 4806 4809 4812 4815 4818 4821 4824
+4827 4830 4833 4836 4839 4842 4845 4848
+4851 4854 4857 4860 4863 4866 4869 4872
+4875 4878 4881 4884 4887 4890 4893 4896
+4899 4902 4905 4908 4911 4914 4917 4920
+4923 4926 4929 4932 4935 4938 4941 4944
+4947 4950 4953 4956 4959 4962 4965 4968
+4971 4974 4977 4980 4983 4986 4989 4992
+4995 4998 5001 5004 5007 5010 5013 5016
+5019 5022 5025 5028 5031 5034 5037 5040
+5043 5046 5049 5052 5055 5058 5061 5064
+5067 5070 5073 5076 5079 5082 5085 5088
+5091 5094 5097 5100 5103 5106 5109 5112
+5115 5118 5121 5124 5127 5130 5133 5136
+5139 5142 5145 5148 5151 5154 5157 5160
+5163 5166 5169 5172 5175 5178 5181 5184
+5187 5190 5193 5196 5199 5202 5205 5208
+5211 5214 5217 5220 5223 5226 5229 5232
+5235 5238 5241 5244 5247 5250 5253 5256
+5259 5262 5265 5268 5271 5274 5277 5280
+5283 5286 5289 5292 5295 5298 5301 5304
+5307 5310 5313 5316 5319 5322 5325 5328
+5331 5334 5337 5340 5343 5346 5349 5352
+5355 5358 5361 5364 5367 5370 5373 5376
+5379 5382 5385 5388 5391 5394 5397 5400
+5403 5406 5409 5412 5415 5418 5421 5424
+5427 5430 5433 5436 5439 5442 5445 5448
+5451 5454 5457 5460 5463 5466 5469 5472
+5475 5478 5481 5484 5487 5490 5493 5496
+5499 5502 5505 5508 5511 5514 5517 5520
+5523 5526 5529 5532 5535 5538 5541 5544
+5547 5550 5553 5556 5559 5562 5565 5568
+5571 5574 5577 5580 5583 5586 5589 5592
+5595 5598 5601 5604 5607 5610 5613 5616
+5619 5622 5625 5628 5631 5634 5637 5640
+5643 5646 5649 5652 5655 5658 5661 5664
+5667 5670 5673 5676 5679 5682 5685 5688
+5691 5694 5697 5700 5703 5706 5709 5712
+5715 5718 5721 5724 5727 5730 5733 5736
+5739 5742 5745 5748 5751 5754 5757 5760
+5763 5766 5769 5772 5775 5778 5781 5784
+5787 5790 5793 5796 5799 5802 5805 5808
+5811 5814 5817 5820 5823 5826 5829 5832
+5835 5838 5841 5844 5847 5850 5853 5856
+5859 5862 5865 5868 5871 5874 5877 5880
+5883 5886 5889 5892 5896 5900 5904 5908
+5912 5916 5920 5924 5928 5932 5936 5940
+5944 5948 5952 5956 5960 5964 5968 5972
+5976 5980 5984 5988 5992 5996 6000 6004
+6008 6012 6016 6020 6024 6028 6032 6036
+6040 6044 6048 6052 6056 6060 6064 6068
+6072 6076 6080 6084 6088 6092 6096 6100
+6104 6108 6112 6116 6120 6124 6128 6132
+6136 6140 6144 6148 6152 6156 6160 6164
+6168 6172 6176 6180 6184 6188 6192 6196
+6200 6204 6208 6212 6216 6220 6224 6228
+6232 6236 6240 6244 6248 6252 6256 6260
+6264 6268 6272 6276 6280 6284 6288 6292
+6296 6300 6304 6308 6312 6316 6320 6324
+6328 6332 6336 6340 6344 6348 6352 6356
+6360 6364 6368 6372 6376 6380 6384 6388
+6392 6396 6400 6404 6408 6412 6416 6420
+6424 6428 6432 6436 6440 6444 6448 6452
+6456 6460 6464 6468 6472 6476 6480 6484
+6488 6492 6496 6500 6504 6508 6512 6516
+6520 6524 6528 6532 6536 6540 6544 6548
+6552 6556 6560 6564 6568 6572 6576 6580
+6584 6588 6592 6596 6600 6604 6608 6612
+6616 6620 6624 6628 6632 6636 6640 6644
+6648 6652 6656 6660 6664 6668 6672 6676
+6680 6684 6688 6692 6696 6700 6704 6708
+6712 6716 6720 6724 6728 6732 6736 6740
+6744 6748 6752 6756 6760 6764 6768 6772
+6776 6780 6784 6788 6792 6796 6800 6804
+6808 6812 6816 6820 6824 6828 6832 6836
+6840 6844 6848 6852 6856 6860 6864 6868
+6872 6876 6880 6884 6888 6892 6896 6900
+6904 6908 6912 6916 6920 6924 6928 6932
+6936 6940 6944 6948 6952 6956 6960 6964
+6968 6972 6976 6980 6984 6988 6992 6996
+7000 7004 7008 7012 7016 7020 7024 7028
+7032 7036 7040 7044 7048 7052 7056 7060
+7064 7068 7072 7076 7080 7084 7088 7092
+7096 7100 7104 7108 7112 7116 7120 7124
+7128 7132 7136 7140 7144 7148 7152 7156
+7160 7164 7168 7172 7176 7180 7184 7188
+7192 7196 7200 7204 7208 7212 7216 7220
+7224 7228 7232 7236 7240 7244 7248 7252
+7256 7260 7264 7268 7272 7276 7280 7284
+7288 7292 7296 7300 7304 7308 7312 7316
+7320 7324 7328 7332 7336 7340 7344 7348
+7352 7356 7360 7364 7368 7372 7376 7380
+7384 7388 7392 7396 7400 7404 7408 7412
+7416 7420 7424 7428 7432 7436 7440 7444
+7448 7452 7456 7460 7464 7468 7472 7476
+7480 7484 7488 7492 7496 7500 7504 7508
+7512 7516 7520 7524 7528 7532 7536 7540
+7544 7548 7552 7556 7560 7564 7568 7572
+7576 7580 7584 7588 7592 7596 7600 7604
+7608 7612 7616 7620 7624 7628 7632 7636
+7640 7644 7648 7652 7656 7660 7664 7668
+7672 7676 7680 7684 7688 7692 7696 7700
+7704 7708 7712 7716 7720 7724 7728 7732
+7736 7740 7744 7748 7752 7756 7760 7764
+7768 7772 7776 7780 7784 7788 7792 7796
+7800 7804 7808 7812 7816 7820 7824 7828
+7832 7836 7840 7844 7848 7852 7856 7860
+7864 7868 7872 7876 7880 7884 7888 7892
+7896 7900 7904 7908 7912 7916 7920 7924
+7928 7932 7936 7940 7944 7948 7952 7956
+7960 7964 7968 7972 7976 7980 7984 7988
+7992 7996 8000 8004 8008 8012 8016 8020
+8024 8028 8032 8036 8040 8044 8048 8052
+8056 8060 8064 8068 8072 8076 8080 8084
+8088 8092 8096 8100 8104 8108 8112 8116
+8120 8124 8128 8132 8136 8140 8144 8148
+8152 8156 8160 8164 8168 8172 8176 8180
+8184 8188 8192 8196 8200 8204 8208 8212
+8216 8220 8224 8228 8232 8236 8240 8244
+8248 8252 8256 8260 8264 8268 8272 8276
+8280 8284 8288 8292 8296 8300 8304 8308
+8312 8316 8320 8324 8328 8332 8336 8340
+8344 8348 8352 8356 8360 8364 8368 8372
+8376 8380 8384 8388 8392 8396 8400 8404
+8408 8412 8416 8420 8424 8428 8432 8436
+8440 8444 8448 8452 8456 8460 8464 8468
+8472 8476 8480 8484 8488 8492 8496 8500
+8504 8508 8512 8516 8520 8524 8528 8532
+8536 8540 8544 8548 8552 8556 8560 8564
+8568 8572 8576 8580 8584 8588 8592 8596
+8600 8604 8608 8612 8616 8620 8624 8628
+8632 8636 8640 8644 8648 8652 8656 8660
+8664 8668 8672 8676 8680 8684 8688 8692
+8696 8700 8704 8708 8712 8716 8720 8724
+8728 8732 8736 8740 8744 8748 8752 8756
+8760 8764 8768 8772 8776 8780 8784 8788
+8792 8796 8800 8804 8808 8812 8816 8820
+8824 8828 8832 8836 8840 8844 8848 8852
+8856 8860 8864 8868 8872 8876 8880 8884
+8888 8892 8896 8900 8904 8908 8912 8916
+8920 8924 8928 8932 8936 8940 8944 8948
+8952 8956 8960 8964 8968 8972 8976 8980
+8984 8988 8992 8996 9000 9004 9008 9012
+9016 9020 9024 9028 9032 9036 9040 9044
+9048 9052 9056 9060 9064 9068 9072 9076
+9080 9084 9088 9092 9096 9100 9104 9108
+9112 9116 9120 9124 9128 9132 9136 9140
+9144 9148 9152 9156 9160 9164 9168 9172
+9176 9180 9184 9188 9192 9196 9200 9204
+9208 9212 9216 9220 9224 9228 9232 9236
+9240 9244 9248 9252 9256 9260 9264 9268
+9272 9276 9280 9284 9288 9292 9296 9300
+9304 9308 9312 9316 9320 9324 9328 9332
+9336 9340 9344 9348 9352 9356 9360 9364
+9368 9372 9376 9380 9384 9388 9392 9396
+9400 9404 9408 9412 9416 9420 9424 9428
+9432 9436 9440 9444 9448 9452 9456 9460
+9464 9468 9472 9476 9480 9484 9488 9492
+9496 9500 9504 9508 9512 9516 9520 9524
+9528 9532 9536 9540 9544 9548 9552 9556
+9560 9564 9568 9572 9576 9580 9584 9588
+9592 9596 9600 9604 9608 9612 9616 9620
+9624 9628 9632 9636 9640 9644 9648 9652
+9656 9660 9664 9668 9672 9676 9680 9684
+9688 9692 9696 9700 9704 9708 9712 9716
+9720 9724 9728 9732 9736 9740 9744 9748
+9752 9756 9760 9764 9768 9772 9776 9780
+9784 9788 9792 9796 9800 9804 9808 9812
+9816 9820 9824 9828 9832 9836 9840 9844
+9848 9852 9856 9860 9864 9868 9872 9876
+9880 9884 9888 9892 9896 9900 9904 9908
+9912 9916 9920 9924 9928 9932 9936 9940
+9944 9948 9952 9956 9960 9964 9968 9972
+9976 9980 9984 9988 9992 9996 10000 10004
+10008 10012 10016 10020 10024 10028 10032 10036
+10040 10044 10048 10052 10056 10060 10064 10068
+10072 10076 10080 10084 10088 10092 10096 10100
+10104 10108 10112 10116 10120 10124 10128 10132
+10136 10140 10144 10148 10152 10156 10160 10164
+10168 10172 10176 10180 10184 10188 10192 10196
+10200 10204 10208 10212 10216 10220 10224 10228
+10232 10236 10240 10244 10248 10252 10256 10260
+10264 10268 10272 10276 10280 10284 10288 10292
+10296 10300 10304 10308 10312 10316 10320 10324
+10328 10332 10336 10340 10344 10348 10352 10356
+10360 10364 10368 10372 10376 10380 10384 10388
+10392 10396 10400 10404 10408 10412 10416 10420
+10424 10428 10432 10436 10440 10444 10448 10452
+10456 10460 10464 10468 10472 10476 10480 10484
+10488 10492 10496 10500 10504 10508 10512 10516
+10520 10524 10528 10532 10536 10540 10544 10548
+10552 10556 10560 10564 10568 10572 10576 10580
+10584 10588 10592 10596 10600 10604 10608 10612
+10616 10620 10624 10628 10632 10636 10640 10644
+10648 10652 10656 10660 10664 10668 10672 10676
+10680 10684 10688 10692 10696 10700 10704 10708
+10712 10716 10720 10724 10728 10732 10736 10740
+10744 10748 10752 10756 10760 10764 10768 10772
+10776 10780 10784 10788 10792 10796 10800 10804
+10808 10812 10816 10820 10824 10828 10832 10836
+10840 10844 10848 10852 10856 10860 10864 10868
+10872 10876 10880 10884 10888 10892 10896 10900
+10904 10908 10912 10916 10920 10924 10928 10932
+10936 10940 10944 10948 10952 10956 10960 10964
+10968 10972 10976 10980 10984 10988 10992 10996
+11000 11004 11008 11012 11016 11020 11024 11028
+11032 11036 11040 11044 11048 11052 11056 11060
+11064 11068 11072 11076 11080 11084 11088 11092
+11096 11100 11104 11108 11112 11116 11120 11124
+11128 11132 11136 11140 11144 11148 11152 11156
+11160 11164 11168 11172 11176 11180 11184 11188
+11192 11196 11200 11204 11208 11212 11216 11220
+11224 11228 11232 11236 11240 11244 11248 11252
+11256 11260 11264 11268 11272 11276 11280 11284
+11288 11292 11296 11300 11304 11308 11312 11316
+11320 11324 11328 11332 11336 11340 11344 11348
+11352 11356 11360 11364 11368 11372 11376 11380
+11384 11388 11392 11396 11400 11404 11408 11412
+11416 11420 11424 11428 11432 11436 11440 11444
+11448 11452 11456 11460 11464 11468 11472 11476
+11480 11484 11488 11492 11496 11500 11504 11508
+11512 11516 11520 11524 11528 11532 11536 11540
+11544 11548 11552 11556 11560 11564 11568 11572
+11576 11580 11584 11588 11592 11596 11600 11604
+11608 11612 11616 11620 11624 11628 11632 11636
+11640 11644 11648 11652 11656 11660 11664 11668
+11672 11676 11680 11684 11688 11692 11696 11700
+11704 11708 11712 11716 11720 11724 11728 11732
+11736 11740 11744 11748 11752 11756 11760 11764
+11768 11772 11776 11780 11784 11788 11792 11796
+11800 11804 11808 11812 11816 11820 11824 11828
+11832 11836 11840 11844 11848 11852 11856 11860
+11864 11868 11872 11876 11880 11884 11888 11892
+11896 11900 11904 11908 11912 11916 11920 11924
+11928 11932 11936 11940 11944 11948 11952 11956
+11960 11964 11968 11972 11976 11980 11984 11988
+11992 11996 12000 12004 12008 12012 12016 12020
+12024 12028 12032 12036 12040 12044 12048 12052
+12056 12060 12064 12068 12072 12076 12080 12084
+12088 12092 12096 12100 12104 12108 12112 12116
+12120 12124 12128 12132 12136 12140 12144 12148
+12152 12156 12160 12164 12168 12172 12176 12180
+12184 12188 12192 12196 12200 12204 12208 12212
+12216 12220 12224 12228 12232 12236 12240 12244
+12248 12252 12256 12260 12264 12268 12272 12276
+12280 12284 12288 12292 12296 12300 12304 12308
+12312 12316 12320 12324 12328 12332 12336 12340
+12344 12348 12352 12356 12360 12364 12368 12372
+12376 12380 12384 12388 12392 12396 12400 12404
+12408 12412 12416 12420 12424 12428 12432 12436
+12440 12444 12448 12452 12456 12460 12464 12468
+12472 12476 12480 12484 12488 12492 12496 12500
+12504 12508 12512 12516 12520 12524 12528 12532
+12536 12540 12544 12548 12552 12556 12560 12564
+12568 12572 12576 12580 12584 12588 12592 12596
+12600 12604 12608 12612 12616 12620 12624 12628
+12632 12636 12640 12644 12648 12652 12656 12660
+12664 12668 12672 12676 12680 12684 12688 12692
+12696 12700 12704 12708 12712 12716 12720 12724
+12728 12732 12736 12740 12744 12748 12752 12756
+12760 12764 12768 12772 12776 12780 12784 12788
+12792 12796 12800 12804 12808 12812 12816 12820
+12824 12828 12832 12836 12840 12844 12848 12852
+12856 12860 12864 12868 12872 12876 12880 12884
+12888 12892 12896 12900 12904 12908 12912 12916
+12920 12924 12928 12932 12936 12940 12944 12948
+12952 12956 12960 12964 12968 12972 12976 12980
+12984 12988 12992 12996 13000 13004 13008 13012
+13016 13020 13024 13028 13032 13036 13040 13044
+13048 13052 13056 13060 13064 13068 13072 13076
+13080 13084 13088 13092 13096 13100 13104 13108
+13112 13116 13120 13124 13128 13132 13136 13140
+13144 13148 13152 13156 13160 13164 13168 13172
+13176 13180 13184 13188 13192 13196 13200 13204
+13208 13212 13216 13220 13224 13228 13232 13236
+13240 13244 13248 13252 13256 13260 13264 13268
+13272 13276 13280 13284 13288 13292 13296 13300
+13304 13308 13312 13316 13320 13324 13328 13332
+13336 13340 13344 13348 13352 13356 13360 13364
+13368 13372 13376 13380 13384 13388 13392 13396
+13400 13404 13408 13412 13416 13420 13424 13428
+13432 13436 13440 13444 13448 13452 13456 13460
+13464 13468 13472 13476 13480 13484 13488 13492
+13496 13500 13504 13508 13512 13516 13520 13524
+13528 13532 13536 13540 13544 13548 13552 13556
+13560 13564 13568 13572 13576 13580 13584 13588
+13592 13596 13600 13604 13608 13612 13616 13620
+13624 13628 13632 13636 13640 13644 13648 13652
+13656 13660 13664 13668 13672 13676 13680 13684
+13688 13692 13696 13700 13704 13708 13712 13716
+13720 13724 13728 13732 13736 13740 13744 13748
+13752 13756 13760 13764 13768 13772 13776 13780
+13784 13788 13792 13796 13800 13804 13808 13812
+13816 13820 13824 13828 13832 13836 13840 13844
+13848 13852 13856 13860 13864 13868 13872 13876
+13880 13884 13888 13892 13896 13900 13904 13908
+13912 13916 13920 13924 13928 13932 13936 13940
+13944 13948 13952 13956 13960 13964 13968 13972
+13976 13980 13984 13988 13992 13996 14000 14004
+14008 14012 14016 14020 14024 14028 14032 14036
+14040 14044 14048 14052 14056 14060 14064 14068
+14072 14076 14080 14084 14088 14092 14096 14100
+14104 14108 14112 14116 14120 14124 14128 14132
+14136 14140 14144 14148 14152 14156 14160 14164
+14168 14172 14176 14180 14184 14188 14192 14196
+14200 14204 14208 14212 14216 14220 14224 14228
+14232 14236 14240 14244 14248 14252 14256 14260
+14264 14268 14272 14276 14280 14284 14288 14292
+14296 14300 14304 14308 14312 14316 14320 14324
+14328 14332 14336 14340 14344 14348 14352 14356
+14360 14364 14368 14372 14376 14380 14384 14388
+14392 14396 14400 14404 14408 14412 14416 14420
+14424 14428 14432 14436 14440 14444 14448 14452
+14456 14460 14464 14468 14472 14476 14480 14484
+14488 14492 14496 14500 14504 14508 14512 14516
+14520 14524 14528 14532 14536 14540 14544 14548
+14552 14556 14560 14564 14568 14572 14576 14580
+14584 14588 14592 14596 14600 14604 14608 14612
+14616 14620 14624 14628 14632 14636 14640 14644
+14648 14652 14656 14660 14664 14668 14672 14676
+14680 14684 14688 14692 14696 14700 14704 14708
+14712 14716 14720 14724 14728 14732 14736 14740
+14744 14748 14752 14756 14760 14764 14768 14772
+14776 14780 14784 14788 14792 14796 14800 14804
+14808 14812 14816 14820 14824 14828 14832 14836
+14840 14844 14848 14852 14856 14860 14864 14868
+14872 14876 14880 14884 14888 14892 14896 14900
+14904 14908 14912 14916 14920 14924 14928 14932
+14936 14940 14944 14948 14952 14956 14960 14964
+14968 14972 14976 14980 14984 14988 14992 14996
+15000 15004 15008 15012 15016 15020 15024 15028
+15032 15036 15040 15044 15048 15052 15056 15060
+15064 15068 15072 15076 15080 15084 15088 15092
+15096 15100 15104 15108 15112 15116 15120 15124
+15128 15132 15136 15140 15144 15148 15152 15156
+15160 15164 15168 15172 15176 15180 15184 15188
+15192 15196 15200 15204 15208 15212 15216 15220
+15224 15228 15232 15236 15240 15244 15248 15252
+15256 15260 15264 15268 15272 15276 15280 15284
+15288 15292 15296 15300 15304 15308 15312 15316
+15320 15324 15328 15332 15336 15340 15344 15348
+15352 15356 15360 15364 15368 15372 15376 15380
+15384 15388 15392 15396 15400 15404 15408 15412
+15416 15420 15424 15428 15432 15436 15440 15444
+15448 15452 15456 15460 15464 15468 15472 15476
+15480 15484 15488 15492 15496 15500 15504 15508
+15512 15516 15520 15524 15528 15532 15536 15540
+15544 15548 15552 15556 15560 15564 15568 15572
+15576 15580 15584 15588 15592 15596 15600 15604
+15608 15612 15616 15620 15624 15628 15632 15636
+15640 15644 15648 15652 15656 15660 15664 15668
+15672 15676 15680 15684 15688 15692 15696 15700
+15704 15708 15712 15716 15720 15724 15728 15732
+15736 15740 15744 15748 15752 15756 15760 15764
+15768 15772 15776 15780 15784 15788 15792 15796
+15800 15804 15808 15812 15816 15820 15824 15828
+15832 15836 15840 15844 15848 15852 15856 15860
+15864 15868 15872 15876 15880 15884 15888 15892
+15896 15900 15904 15908 15912 15916 15920 15924
+15928 15932 15936 15940 15944 15948 15952 15956
+15960 15964 15968 15972 15976 15980 15984 15988
+15992 15996 16000 16004 16008 16012 16016 16020
+16024 16028 16032 16036 16040 16044 16048 16052
+16056 16060 16064 16068 16072 16076 16080 16084
+16088 16092 16096 16100 16104 16108 16112 16116
+16120 16124 16128 16132 16136 16140 16144 16148
+16152 16156 16160 16164 16168 16172 16176 16180
+16184 16188 16192 16196 16200 16204 16208 16212
+16216 16220 16224 16228 16232 16236 16240 16244
+16248 16252 16256 16260 16264 16268 16272 16276
+16280 16284 16288 16292 16296 16300 16304 16308
+16312 16316 16320 16324 16328 16332 16336 16340
+16344 16348 16352 16356 16360 16364 16368 16372
+16376 16380 16384 16388 16392 16396 16400 16404
+16408 16412 16416 16420 16424 16428 16432 16436
+16440 16444 16448 16452 16456 16460 16464 16468
+16472 16476 16480 16484 16488 16492 16496 16500
+16504 16508 16512 16516 16520 16524 16528 16532
+16536 16540 16544 16548 16552 16556 16560 16564
+16568 16572 16576 16580 16584 16588 16592 16596
+16600 16604 16608 16612 16616 16620 16624 16628
+16632 16636 16640 16644 16648 16652 16656 16660
+16664 16668 16672 16676 16680 16684 16688 16692
+16696 16700 16704 16708 16712 16716 16720 16724
+16728 16732 16736 16740 16744 16748 16752 16756
+16760 16764 16768 16772 16776 16780 16784 16788
+16792 16796 16800 16804 16808 16812 16816 16820
+16824 16828 16832 16836 16840 16844 16848 16852
+16856 16860 16864 16868 16872 16876 16880 16884
+16888 16892 16896 16900 16904 16908 16912 16916
+16920 16924 16928 16932 16936 16940 16944 16948
+16952 16956 16960 16964 16968 16972 16976 16980
+16984 16988 16992 16996 17000 17004 17008 17012
+17016 17020 17024 17028 17032 17036 17040 17044
+17048 17052 17056 17060 17064 17068 17072 17076
+17080 17084 17088 17092 17096 17100 17104 17108
+17112 17116 17120 17124 17128 17132 17136 17140
+17144 17148 17152 17156 17160 17164 17168 17172
+17176 17180 17184 17188 17192 17196 17200 17204
+17208 17212 17216 17220 17224 17228 17232 17236
+17240 17244 17248 17252 17256 17260 17264 17268
+17272 17276 17280 17284 17288 17292 17296 17300
+17304 17308 17312 17316 17320 17324 17328 17332
+17336 17340 17344 17348
+ 5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10
+
+
+
+
diff --git a/geos-processing/tests/data/mesh1b.vtu b/geos-processing/tests/data/mesh1b.vtu
new file mode 100755
index 00000000..caa19ed5
--- /dev/null
+++ b/geos-processing/tests/data/mesh1b.vtu
@@ -0,0 +1,2876 @@
+
+
+
+
+
+ 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0
+ 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0
+0 0
+
+
+ 1
+1 2 1 1 2 3 4 3
+4 3 1 3 3 2 1 1
+5 2 5 1 6 2 2 5
+2 5 5 7 2 7 5 6
+1 7 2 1 7 7 7 2
+2 7 5 5 5 2 8 6
+8 2 9 9 4 9 10 9
+3 3 3 10 11 9 3 3
+10 9 3 9 6 7 6 7
+6 8 7 5 3 12 11 5
+9 9 11 3 10 3 4 1
+7 1 11 4 9 3 1 2
+11 3 2 11 1 2 11 3
+1 2 2 2 5 1 1 5
+6 1 5 1 1 1 1 6
+1 7 6 2 2 1 1 1
+1 7 1 1 1 1 6 1
+5 1 2 1 1 1 1 1
+1 5 1 1 6 2 1 6
+1 1 1 7 1 1 1 1
+6 1 1 1 1 1 1 1
+1 1 1 1 2 1 1 1
+5 1 1 6 1 1 1 1
+1 11 3 4 3 11 4 9
+11 4 9 9 11 11 12 9
+12 11 9 11 12 11 9 4
+9 11 11 11 9 3 11 11
+9 11 9 11 3 11 3 11
+12 11 11 11 11 11 11 11
+12 12 3 11 3 11 12 9
+11 11 11 2 7 8 3 4
+10 3 4 2 7 3 4 4
+9 2 7 3 4 11 2 7
+11 4 2 1 3 3 10 10
+3 10 9 2 8 2 8 3
+10 5 2 8 3 12 10 2
+5 12 3 12 2 2 5 3
+12 2 3 10 9 12 12 2
+8 7 2 7 3 4 10 3
+2 3 2 10 2 9 12 2
+5 1 11 1 3 2 3 2
+3 2 2 10 3 2 7 3
+4 3 1 7 3 2 2 7
+3 4 3 2 5 11 3 1
+1 1 2 5 11 12 12 3
+11 3 11 2 11 2 1 2
+1 11 3 1 11 3 3 3
+12 12 2 5 2 2 2 8
+2 9 3 2 5 3 12 3
+12 2 1 2 2 3 3 3
+11 11 2 1 1 2 11 11
+11 3 11 3 1 2 11 3
+3 2 1 11 2 1 11 3
+3 11 1 1 1 1 2 9
+1 2 1 2 2 9 1 2
+3 2 2 3 2 3 3 2
+3 11 3 2 11 2 2 2
+2 3 3 2 3 3 2 2
+2 2 2 12 9 3 2 2
+3 8 3 9 4 3 2 3
+10 4 3 9 3 9 2 2
+3 2 2 8 3 2 3 3
+6 7 2 3 3 9 3 1
+9 2 3 3 3 3 8 2
+1 1 2 3 1 2 1 3
+2 2 2 9 3 2 2 6
+2 6 3 3 9 2 3 3
+2 2 8 2 3 2 2 3
+2 3 3 10 3 12 3 10
+3 12 3 3 12 12 3 12
+11 9 12 9 12 9 11 12
+11 3 12 3 12 3 11 9
+9 3 12 3 12 12 9 10
+9 12 3 3 3 12 12 12
+12 12 3 9 3 3 12 9
+3 12 9 11 12 11 12 12
+12 12 12 12 12 9 5 8
+8 5 1 5 6 5 6 6
+5 6 5 8 6 5 6 5
+1 2 5 2 2 5 1 1
+1 5 1 5 5 1 2 2
+5 5 5 8 2 5 2 8
+2 2 5 5 5 2 5 6
+5 5 8 8 6 5 5 6
+6 5 5 5 5 6 5 5
+5 5 5 6 6 5 6 6
+5 5 5 2 8 5 5 5
+1 2 2 5 5 2 5 5
+8 2 5 2 2 5 2 1
+5 5 5 5 5 5 2 5
+5 5 6 2 5 5 5 5
+5 5 7 7 7 8 7 8
+1 6 6 7 8 6 7 7
+2 2 2 2 7 6 7 7
+8 8 7 2 2 1 7 7
+1 6 7 2 2 1 7 6
+7 7 7 2 7 6 7 6
+2 7 7 2 2 2 7 7
+1 1 7 7 6 6 7 6
+6 7 2 2 2 2 2 2
+7 2 7 7 7 7 7 7
+7 7 7 7 7 7 7 7
+6 1 7 7 2 7 7 4
+3 4 9 11 9 9 10 4
+3 3 3 3 4 4 11 4
+11 3 3 3 4 9 4 3
+3 4 10 4 10 3 4 4
+3 4 4 4 4 3 4 4
+4 3 4 4 4 4 4 9
+4 4 4 4 4 9 4 4
+8 8 6 8 6 7 8 6
+5 2 2 8 2 5 5 5
+8 8 8 5 8 2 2 8
+2 7 8 2 8 8 8 7
+8 8 2 8 8 2 2 8
+8 7 8 7 8 2 6 8
+8 8 8 8 8 6 8 7
+8 6 8 8 8 8 8 8
+8 2 2 6 7 8 2 7
+8 5 8 8 8 8 8 6
+8 8 2 2 2 8 2 8
+8 2 8 8 8 3 12 12
+9 3 10 10 9 10 9 10
+10 9 10 10 9 3 4 10
+3 10 10 3 10 12 10 10
+9 3 3 10 3 10 10 10
+12 10 10 10 10 10 9 3
+10 10 10 3 10 9 10 4
+4 10 9 10 10 10 10 10
+10 10 10 10 10 10 11 12
+12 9 12 12 12 3 9 11
+11 11 9 3 11 11 11 11
+9 9 9 9 4 9 9 12
+12 9 4 9 9 12 9 9
+10 10 9 12 12 10 3 9
+10 10 9 4 10 4 10 9
+4 4 9 10 9 9 10 10
+4 9 4 4 4 4 4 9
+9 4 4 9 9 4 4 9
+10 9 9 9 4 9 10 9
+9 9 9 10 3 12 12 9
+9 9 9 9 3 3 3 9
+9 3 4 9 3 4 4 3
+9 9 3 9 9 3 3 9
+12 12 3 9 9 3 3 3
+3 3 9 3 11 9 9 3
+3 3 9 9 9 3 3 9
+9 9 3 9 3 9 3 9
+3 3 3 3 9 3 9 3
+3 3 9 9 9 9 9 3
+3 9 3 3 9 9 9 9
+9 7 6 1 6 6 6 1
+1 1 7 7 6 7 7 6
+6 7 7 6 8 6 6 7
+1 6 6 5 5 1 6 1
+1 5 5 5 7 7 6 6
+6 1 6 6 1 8 6 6
+6 7 7 6 7 6 8 8
+6 6 8 6 8 8 8 8
+6 1 6 6 6 5 5 6
+6 6 6 6 6 6 6 8
+8 6 6 5 5 6 6 6
+8 5 5 8 2 6 5 5
+5 8 8 8 8 6 6 6
+6 1 1 6 6 5 6 6
+1 6 1 6 6 7 7 7
+7 6 1 6 6 6 6 7
+2 2 6 2 7 6 6 6
+6 8 6 2 2 6 6 2
+6 2 6 5 6 6 1 6
+6 6 5 2 6 6 2 6
+8 6 6 1 6 6 6 5
+6 6 6 6 2 6 6 6
+2 6 6 6 6 6 6 2
+6 2 12 12 12 3 7 6
+7 7 7 7 7 7 1 7
+7 8 8 8 8 9 9 9
+9 9 3 9 3 9 9 9
+9 9 3 9 9 6 6 6
+6 6 6 6 2 2 7 7
+6 8 8 6 7 2 8 8
+6 6 6 6 2 6 9 9
+9 9 10 9 9 9 9 11
+3 3 3 9 9 4 9 9
+9 10 9 9 9 9 9 9
+6 6 8 8 8 8 8 6
+2 7 6 7 2 7 6 1
+6 6 2 5 6 5 6 6
+9 9 3 9 12 12 9 11
+3 9 10 9 10 9 9 5
+6 5 5 5 7 7 2 2
+2 2 6 2 11 9 9 9
+3 9 12 3 9 9 9 2
+5 2 7 7 7 7 12 9
+3 9 12 9 9 3 4 9
+9 4 9 2 7 8 9 6
+7 7 9 3 4 9 9 9
+3 3 9 9 2 2 2 5
+2 5 5 5 5 6 6 6
+6 5 6 8 2 8 5 5
+5 6 5 2 6 5 6 6
+5 6 7 7 2 7 7 7
+7 2 7 8 5 8 5 8
+2 8 8 8 8 8 6 8
+8 6 8 8 8 8 6 8
+8 6 8 10 9 4 3 10
+9 3 9 9 9 3 11 9
+12 12 10 3 9 12 3 3
+9 3 9 3 9 3 9 7
+6 6 7 6 7 6 6 6
+6 6 6 7 5 6 8 6
+6 6 6 6 6 6 7 6
+6 9 9 6 5 8 8 6
+3 2 2 4 9 9 9 1
+1 1 1 1 1 2 2 2
+2 1 1 2 2 2 1 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 1 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 2 1 2 2 2 2
+2 1 1 1 2 2 1 2
+1 2 1 2 2 1 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1 2 2 1
+2 2 2 1 2 2 1 2
+1 1 2 1 2 2 2 2
+1 1 2 1 2 1 2 2
+1 1 2 1 2 2 2 1
+1 1 2 1 2 2 1 2
+1 2 1 2 1 1 1 2
+1 1 2 2 1 2 1 1
+2 2 1 1 2 2 1 1
+2 2 2 1 1 2 1 1
+2 2 1 1 1 2 1 1
+1 1 1 2 2 2 2 2
+2 1 2 1 1 1 2 2
+1 2 2 2 2 2 2 2
+1 1 1 1 1 2 2 1
+1 2 2 2 1 1 1 1
+2 2 2 2 2 2 1 1
+1 1 1 2 1 2 2 2
+2 2 1 2 2 2 2 1
+1 1 2 1 1 1 2 2
+2 2 2 2 2 1 1 1
+1 1 1 2 1 1 1 1
+1 2 1 1 2 2 1 1
+1 1 1 2 1 2 2 1
+2 2 2 1 2 2 2 2
+2 1 2 1 1 2 1 2
+2 1 1 1 2 2 2 2
+1 1 1 2 2 2 1 1
+1 1 1 2 2 1 1 2
+1 2 2 2 2 1 2 1
+2 2 2 2 1 2 2 2
+1 1 2 1 1 1 1 2
+1 2 2 1 1 1 2 2
+2 2 1 2 1 2 2 2
+2 1 1 2 1 1 1 1
+2 1 1 1 2 2 1 1
+1 2 2 1 1 1 1 1
+2 2 2 1 2 2 1 1
+1 1 2 1 1 2 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+2 2 2 2 2 2 2 2
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 2 1 1
+1 1 1 1 1 1 1 1
+1 1 1 1 1 1 1 1
+1 1 1 2 2 2 2 2
+2 2 2 2 1 1 1 1
+1 1 1 1 1 1 1 1
+1 2 2 2 2 2 2 2
+2 2 2 2 2 2 2 2
+2 2 2 2 1
+
+
+ 9 0 0 9 9 3.502532958984375
+9 9 9 9 0 9
+9 9 6.251266479492188 8.999999999999996 0.8359832604650297 6.723450909092142
+8.999999999999996 6.293161288260501 5.11217895660125 9 0 3.502532958984375
+9 1.799999999999999 3.502532958984375 8.177845845051273 4.050000000000001 0
+9 9 0 9 9 1.751266479492188
+0 0 3.502532958984375 0.9000000000000012 9 3.502532958984375
+0 9 3.502532958984375 0 9 0
+9 8.1 3.502532958984375 9 7.199999999999999 3.502532958984375
+6.300000000000001 9 3.502532958984375 9 6.752283610666082 2.625933399103364
+9 6.299999999999999 3.502532958984375 9 6.325806601975986 1.713624568765537
+9 5.399999999999999 3.502532958984375 9 2.699999999999998 3.502532958984375
+9 3.599999999999999 3.502532958984375 9 4.499999999999998 3.502532958984375
+8.177845845051271 4.950000000000001 3.502532958984375 8.999999999999996 4.949999999999996 4.324687113933102
+8.999999999999996 3.599573699913536 5.137585661664579 9 3.158621025056943 2.651934647618114
+9 2.248039948293297 2.665595466014997 8.999999999999996 2.68019223803895 5.148217820251414
+8.999999999999996 2.217756231877171 4.359653137142738 8.999999999999996 1.665282257658973 5.274179327423395
+8.999999999999996 5.399999999999995 5.146841268881834 8.999999999999996 4.049999999999999 4.324687113933102
+8.999999999999996 5.849999999999998 4.324687113933104 8.999999999999996 4.01249625414772 5.901383452613061
+1.800000000000002 9 3.502532958984375 7.199999999999999 9 0
+9 9 2.626899719238281 9 8.189157955191284 2.235077043665196
+8.1 9 3.502532958984375 7.611645216840162 9 2.693983819566046
+8.181229301155238 9 2.220458696966295 9 7.32210713867422 1.749390562731385
+3.599999999999999 9 0 0 9 9
+7.738009174972064 9 4.321149101795649 9 9 4.418777465820312
+5.400000000000001 9 3.502532958984375 7.200000000000001 9 3.502532958984375
+7.735145132194498 8.192335273734621 3.502532958984375 8.999999999999996 8.164000755257213 5.778011509193902
+9 9 5.33502197265625 8.999999999999996 7.334037282752933 7.230596511087146
+9 9 7.167510986328125 2.18707295128528e-18 8.100000000000001 3.502532958984375
+2.700000000000002 9 3.502532958984375 3.600000000000001 9 3.502532958984375
+4.500000000000001 9 3.502532958984375 9 8.100000000000001 9
+4.950000000000001 8.177845845051271 3.502532958984375 9 9 8.083755493164062
+5.400000000000004 7.355691690102542 3.502532958984375 2.709248697054655 7.391727441334007 3.502532958984375
+1.385871337505962 8.215609982538957 3.502532958984375 1.730150806845259 7.324027116328562 3.502532958984375
+5.850000000000001 9 4.324687113933102 3.6 7.355691690102548 3.502532958984375
+4.965594110718341 6.507460970834256 3.502532958984375 6.319805761079415 9 5.148221435302199
+0 0 4.418777465820312 9 1.800000000000001 9
+0 0 9 1.800000000000001 0 3.502532958984375
+9 0 7.167510986328125 9 0 6.251266479492188
+7.199999999999999 0 9 8.219675828624311 0 4.913829097393842
+9 0 5.33502197265625 9 0.8999999999999986 3.502532958984375
+8.099999999999998 0 3.502532958984375 8.999999999999996 1.261990521009734 4.321148250875595
+7.199999999999998 0 3.502532958984375 8.999999999999996 2.327532132102036 5.940675240585001
+5.4 1.644308309897455 3.502532958984375 5.81164735236738 0 5.859251804340831
+0 0 0 0.9000000000000002 0 3.502532958984375
+9.817987233071469e-18 0.9000000000000002 3.502532958984375 0 0 1.751266479492188
+3.15009950265889 0 2.629941330339408 2.700000000000001 0 3.502532958984375
+1.392479195400391 0 2.714420866840616 3.600000000000001 0 3.502532958984375
+4.5 0 3.502532958984375 6.299999999999999 0 3.502532958984375
+5.399999999999999 0 3.502532958984375 4.950000000000001 0.8221541549487279 3.502532958984375
+9 0 2.626899719238281 9 0 1.751266479492187
+9 0.8999999999999995 0 0.8136465509939548 9 5.793144226074219
+0 9 4.418777465820312 0 9 7.167510986328125
+3.128892672869206e-16 2.219947661410139 4.35828661324544 8.886346537770051e-18 2.700000000000001 3.502532958984375
+1.387778780781446e-17 3.600000000000001 3.502532958984375 0 0 8.083755493164062
+0 0 6.251266479492188 0.9000000000000004 9 9
+6.085114273362506e-18 5.400000000000001 3.502532958984375 0 5.400000000000001 9
+0 0 7.167510986328125 0.8359832604650297 0 6.723450909092142
+1.600233341274599 0 6.287558707578944 2.336159267275482 0 5.933956202572475
+5.823950847330327e-17 7.737997889647492 4.321158287783087 7.685469237304378e-17 7.334622209084504 5.274251638139043
+1.387778780781446e-17 1.8 3.502532958984375 0 0 2.626899719238281
+0.8196276711094974 0 2.234598634425147 1.387778780781446e-17 6.300000000000002 3.502532958984375
+0.8221541549487279 5.850000000000003 3.502532958984375 0.8186941576853588 9 2.220492146699293
+0 9 1.751266479492187 0 9 2.626899719238281
+1.387778780781446e-17 7.200000000000001 3.502532958984375 0.8481912099242113 6.770042546765302 3.502532958984375
+0 4.500000000000002 3.502532958984375 0.9 0 0
+0 8.100000000000001 0 9.439780055951931e-15 7.614860419315227 2.704270512688835
+0.8081405131799846 7.734720626411981 3.502532958984375 8.370492598850587e-15 6.752290032538334 2.625939155495052
+6.752815430379814 9 0.8292424785167327 9 9 0.8756332397460938
+8.1 9 0 8.188852590377454 9 1.266146654647823
+7.618270583633556 9 0.790408972751127 7.734393058871635 8.20412665188975 0
+9 4.499999999999998 0 9 7.199999999999999 0
+9 7.616889470969293 0.7979354629603463 8.196413367430877 7.619416926333484 0
+9 3.597804286391479 1.715925144769908 9 3.599999999999999 0
+9 2.248700946382007 0.8819782261599798 9 6.299999999999999 0
+9 5.841226347143078 0.8506013849838432 9 5.399999999999999 0
+8.177845845051273 4.949999999999998 0 9 2.699999999999998 0
+7.199999999999999 0 0 6.299999999999999 9 0
+4.949999999999996 9 0.822154154948727 9 1.8 0
+9 0.8187748769847776 1.282082442278652 8.189302099854938 0 1.265902696797298
+5.399999999999999 9 0 0.8221541549487279 4.950000000000001 0
+0 6.300000000000002 0 0 5.400000000000001 0
+1.381942042263111 9 0.7904224948159282 0 9 0.8756332397460936
+4.499999999999998 9 0 1.8 0 0
+0 1.800000000000001 0 3.15 0 0.8221541549487288
+6.300000000000002 0 0 6.751728210625917 0 0.8369364746260803
+7.618192484488804 0 0.7903658745587183 8.1 0 0
+9 0 0.8756332397460936 8.196217419583279 1.379945195506388 0
+1.394804841618308 0 0.8209827069905771 0.8262675200081002 0 1.29764058256637
+0 0 0.8756332397460938 1.005540670973232e-15 0.8111472735615113 1.266146563567167
+0 0.9 0 0 4.500000000000002 0
+0 3.600000000000001 0 0 2.700000000000001 0
+6.136417354026386e-15 4.950111268335313 0.8725601690241014 2.699999999999998 9 0
+1.693166479530808 9 1.71262181566817 0 7.2 0
+0.8999999999999995 9 0 1.8 9 0
+2.247687381757848 9 0.8292867605041518 1.014690003487421e-14 8.185278364149353 1.278288443772258
+5.03087841406891e-15 4.058297617382092 0.8429107327589076 7.306735077048973 0 1.71207169114507
+4.950035806856173 0 0.8725615042826238 6.322241157368904 0 1.710572699542961
+3.150000000000002 0.8221541549487297 0 2.678209061813275 9 1.697750567599654
+3.149999999999997 9 0.822154154948727 5.849999999999998 9 0.8221541549487288
+8.177845845051273 3.149999999999999 0 9 3.140226626924873 0.8504419682654003
+9 4.057308668212663 0.8506095167839316 9 4.498399114513601 1.800010517283264
+6.528784969301348 5.841829740494001 0 7.390671138305633 6.307573544257441 0
+6.300000000000001 1.644308309897456 3.502532958984375 6.707432314425258 6.626319373669748 0
+1.644308309897458 5.400000000000004 3.502532958984375 3.28861661979492 4.499999999999991 3.502532958984375
+6.299999999999999 0 9 4.499999999999998 0 9
+0.9000000000000004 0 9 5.399999999999999 0 9
+5.849999999999998 0 8.177845845051271 9 2.700000000000001 9
+0 6.300000000000002 9 9 3.600000000000001 9
+8.999999999999996 4.500000000000002 7.355691690102546 3.599999999999999 9 9
+2.249999999999998 0 8.177845845051273 2.699999999999998 0 9
+1.800000000000001 0 9 1.262682859208278 0 8.198699456396938
+0 0.9 9 3.599999999999999 0 9
+4.949999999999996 0 8.177845845051273 4.949999999999998 0.8221541549487279 9
+0 1.800000000000001 9 0 2.700000000000001 9
+0 4.500000000000002 9 0 3.600000000000001 9
+2.699687439299589e-16 3.15 8.177845845051271 9 6.300000000000002 9
+8.999999999999996 6.780107197571405 8.144145948880645 9 7.2 9
+8.1 9 9 7.199999999999999 9 9
+9 4.500000000000002 9 9 5.400000000000001 9
+8.999999999999996 4.050000000000002 8.177845845051273 8.999999999999996 5.850000000000001 8.177845845051271
+6.299999999999999 9 9 4.499999999999998 9 9
+5.399999999999999 9 9 0 7.2 9
+2.699999999999998 9 9 1.800000000000001 9 9
+0 8.100000000000001 9 1.662994070347827 9 7.251951101759925
+1.261175279789846 9 8.183378849840555 2.466462464846184 5.850000000000007 3.502532958984375
+5.849999999999998 9 8.177845845051271 4.949999999999996 9 8.177845845051273
+4.949680507752596 9 6.63837120676034 3.599999999999998 9 7.355691690102542
+1.869014381053562e-16 4.949999999999999 8.177845845051273 1.661346116492055e-16 5.399999999999999 7.355691690102541
+2.492019174738081e-16 3.600000000000003 7.355691690102541 2.284350910176574e-16 4.050000000000002 8.177845845051273
+3.571351759997481e-16 1.261175315389243 8.183378891217448 3.129527490613384e-16 2.218572063720798 8.147156677240091
+8.999999999999996 5.40039312626325 7.364611788135246 8.151769405700305 6.770061453867674 9
+4.049999999999999 9 8.177845845051273 8.999999999999996 4.949999999999999 8.177845845051273
+8.999999999999996 2.706989272767377 7.390115107262245 8.999999999999996 3.15 8.177845845051271
+3.599999999999998 0 7.355691690102542 3.149999999999997 0 8.177845845051273
+8.999999999999996 2.377621447130771 6.705940348781539 7.619416926333484 0.8035866325691288 9
+8.999999999999996 2.249999999999998 8.177845845051273 3.149418807812761 0 6.60017202119052
+5.841829740494001 2.471215030698652 9 6.626319373669745 2.292567685574747 9
+3.169240769925523 6.524412878993601 3.502532958984375 4.090592114588162 9 6.596074696537302
+3.169236758142278 6.524412909492083 9 8.999999999999996 5.838631019705881 6.639513393038047
+6.667901369707694 9 5.934276435262946 2.359530656001406 9 6.713599267082081
+2.689101743696412 9 7.386848209743892 3.149023165904749 9 2.621948174295266
+3.150000000000002 8.177845845051275 3.502532958984375 7.737601458953271 0 8.198232489830723
+9 0.9 9 8.1 0 9
+9 0 8.083755493164062 8.999999999999996 0.7990566280348386 7.59448786599711
+8.999999999999996 0.8136465509939548 5.793144226074219 8.163741631545861 0 6.723323157728117
+9 0 4.418777465820312 0 9 6.251266479492188
+0 9 5.33502197265625 0.7806888691991265 9 4.914105718368745
+3.603293115959499e-17 8.219196103166968 4.914116201841944 3.793088926112475e-16 0.7806891661818174 4.914105855785795
+0.7808024117998897 0 4.914115427192536 0 0 5.33502197265625
+2.292614782422479 2.373815886019945 0 2.391136688362659 6.694825646499693 0
+3.150000000000004 2.466462464846191 0 5.6034873781668 3.397272255629511 0
+4.934860096398886 4.027435447737417 0 6.749999999999998 9 8.177845845051273
+5.832316353102067 6.525340723528727 9 4.499999999999998 3.288616619794906 9
+2.492553407191698 4.035184491647881 9 4.937893796247414 4.984547009515971 9
+3.064543316186292e-16 2.359387409551024 6.713621657010831 4.5 0 5.146841268881833
+0.8221541549487279 4.950000000000001 9 2.46646246484619 5.850000000000007 0
+1.01516122171462e-14 8.189079576683849 2.233965154845459 2.248112216360795 9 2.620702741153845
+2.250000000000003 8.177845845051271 3.502532958984375 0.8082099007164762 7.734693941865935 0
+9 1.694068875762207 1.790056210709047 9 1.381809378200034 2.712119566812706
+9 1.388272215592432 0.8085468800678299 8.192335273734621 1.264854867805503 3.502532958984375
+7.614367554786917 0.7840773181430389 3.502532958984375 6.753215943037485 0 2.620593737449624
+4.46204095058634e-15 3.599429099257462 1.702064968980743 0.8221541549487297 3.150000000000002 0
+2.785727494971588e-15 2.247184353313637 0.8292420715973154 1.644308309897458 3.600000000000005 0
+3.149999999999997 2.466462464846184 9 1.477439511164972e-16 5.798510444395207 5.900338655297849
+1.873159616293102e-16 4.941017617151413 5.862831030988056 6.60700701579113 0 5.831508935829884
+8.199618461226668 0 7.594985479837401 8.999999999999996 1.262695385536164 8.198678401041034
+8.204126651889748 1.265606941128365 9 8.999999999999996 4.905133804331903 5.902638104991828
+8.999999999999996 4.986405972010397 6.59766266851749 7.355691690102542 5.4 9
+6.507456982194213 4.966398565568239 9 8.999999999999996 3.149999999999997 4.324687113933102
+9 5.860570121008122 2.652182532854836 8.177845845051271 6.750000000000002 3.502532958984375
+9 7.614698550535265 2.704340361733125 6.321073696230174 9 1.697699675927016
+9 8.1 0 9 8.189393797725302 1.268569596562759
+6.307671312081789 7.391003778863753 3.502532958984375 6.750000000000002 8.177845845051273 0
+7.241468532884815e-15 5.841531453268649 0.8505720120260953 6.699775823834638e-15 5.404560004238222 1.799819127284836
+0.8221541549487297 4.950000000000001 3.502532958984375 5.576604929617121e-15 4.498523048312412 1.800146163089394
+3.288616619794904 4.500000000000002 0 5.400000000000002 9 5.146841268881834
+4.073937215115137 4.896049627957062 9 4.049999999999999 9 4.324687113933102
+3.599675952197703 9 5.137698219146628 3.16427015443869 9 5.860128101335576
+8.999999999999996 1.600249006510131 6.287582334777834 8.999999999999996 0.7807940111769565 4.914112022550661
+7.273947446557143 1.674288754741407 3.502532958984375 8.177845845051273 3.150000000000002 3.502532958984375
+8.177845845051273 4.050000000000001 3.502532958984375 8.999999999999996 4.499999999999998 5.146841268881829
+8.177845845051275 5.850000000000001 3.502532958984375 7.355691690102546 5.400000000000004 3.502532958984375
+4.973643379934225 4.949136918187648 3.502532958984375 5.711383380205078 4.499999999999993 3.502532958984375
+8.999999999999996 5.815440049583025 5.934878130029515 9 4.949999999999996 2.680378804035648
+9 4.042523827180387 2.659580853102861 9 2.677821826888309 1.791937162509726
+7.391003778863752 2.692328687918213 3.502532958984375 6.528787866445139 3.158203951309305 3.502532958984375
+7.325711245258594 7.273947446557147 3.502532958984375 5.850000000000003 8.177845845051273 3.502532958984375
+4.950147492394088 9 2.622759330763099 5.399999999999997 7.355691690102542 0
+4.967549139333093 6.507400635169903 0 8.999999999999996 7.276899676491421 5.187029494568771
+8.999999999999996 6.749999999999998 4.324687113933102 8.999999999999996 8.199567551492635 4.906619158874051
+8.219205930530917 9 4.914112126427263 8.215922681856966 7.614367554786917 3.502532958984375
+6.783702206183331 0 4.35670402016348 7.403014916110735 0 6.259445269787509
+7.738690122314669 0 4.320029168768409 8.186353449006043 0 5.793144226074219
+4.500232569573358 9 1.6896170065001 4.049848090700435 9 2.622670575932171
+4.049999999999999 9 0.822154154948727 4.050000000000001 8.177845845051273 0
+5.850758906397619 9 2.622161644945423 4.949999999999998 8.177845845051273 0
+5.85 8.177845845051271 0 4.949999999999999 0 2.680378804035648
+4.050000000000004 0.8221541549487279 0 4.058304197930138 0 0.8428989279942947
+4.041716771905712 0 2.659644362045393 4.050000000000001 0.8221541549487279 3.502532958984375
+5.833096124498512 2.47450553391766 0 3.150000000000002 0.8221541549487279 3.502532958984375
+4.032571921598912 2.492601799143239 0 1.262001048173426 0 4.321155845753163
+2.217800900630873 0 4.359730808609976 3.570963764119096e-16 1.26201607045426 4.320595478478094
+3.598666000007425 9 1.689569584428195 4.050000000000001 8.177845845051273 3.502532958984375
+3.149999999999999 8.177845845051273 0 5.399999999999995 0 7.355691690102541
+7.355691690102544 3.6 0 8.157444284755618 2.237792751913814 0
+7.353970554853589 2.689898036280775 0 5.116887144460041 0.9699409631610939 0.8405286634697674
+4.964733222891972 2.492549632609289 0 4.042634148495316 1.334904117401961 2.129215961157497
+6.290835165519434 1.608134880349942 0 7.809940956190811 3.967395363069223 1.732879124339991
+7.355691690102548 4.499999999999998 0 7.355691690102546 6.299999999999999 3.502532958984375
+9 5.404523665229148 1.799838722758963 6.507395263582676 4.966407653000068 0
+6.507454368651299 4.033509109900422 0 5.85 2.466462464846184 3.502532958984375
+5.711383380205097 4.499999999999996 0 6.707310761334632 2.37396459911008 3.502532958984375
+8.15377681722863 2.226164146760556 3.502532958984375 7.355691690102542 4.499999999999998 3.502532958984375
+6.533537535153821 5.850000000000001 3.502532958984375 6.626035400889922 6.707310761334638 3.502532958984375
+7.96208799617022 1.883003662655822 5.266240462131999 7.970999531404276 4.820938271730211 5.169902949206974
+4.5 3.288616619794917 3.502532958984375 4.499999999999998 9 5.146841268881834
+4.949999999999999 9 4.324687113933104 6.773835853239445 8.153776817228632 3.502532958984375
+7.334720714968483 9 5.274186515526972 8.186353449006043 9 5.793144226074219
+8.999999999999996 6.622910945386642 5.796841815974647 8.164026766773862 9 6.724547569955259
+6.782243718888755 9 4.359653962035019 6.623481890094904 9 6.706072449107227
+8.999999999999996 7.73749026383391 4.304095373735455 7.399811326801679 9 6.287636172185421
+4.964001834433843 6.507483562036326 9 7.090473235204542 6.752996394771514 4.713890568410704
+5.837298062237601 7.932881212497764 4.712666759722818 7.61410711245437 8.215588081876673 9
+6.750000000000002 8.177845845051273 9 5.85 8.177845845051271 9
+8.999999999999996 8.186353449006045 6.709388732910158 7.269301061450413 7.324225303351637 9
+8.191761331277155 7.734685376968955 9 7.276165080403961 9 7.316038725543564
+7.73747335827439 9 8.198432973632073 8.199479839482617 9 7.595893255776534
+3.15 9 4.3246871139331 1.023481998853111e-16 6.782198929353069 4.359731880475424
+6.290296517366825 9 7.390202442487272 5.830587493740094 9 5.860309845415465
+1.666398934487027 9 5.269914266182697 2.680256466694075 9 5.148332098884941
+2.219947663515075 9 4.358286538375744 5.841796048690703 6.528787866445139 3.502532958984375
+5.604136654449995 5.603839359577371 3.502532958984375 4.948394279371323 9 5.946016427429315
+4.023374318653804 9 5.907025018591067 5.399999999999997 7.355691690102542 9
+5.822406416936873 9 6.558838992358694 4.048849606762485 7.632272084799333 4.778353844095546
+5.399999999999995 9 7.355691690102541 6.288316617277992 7.391877139954325 9
+4.949999999999998 8.177845845051273 9 6.144470597909364 7.828818388286819 6.108523267751453
+7.35397045354104 6.310082527289014 9 4.949243757917081 7.821371777463613 7.108065119791132
+3.15 0 4.324687113933102 4.032619594876701 2.492581504695079 3.502532958984375
+3.6 0 5.146841268881829 4.099812624215296 0 5.903143704122105
+1.726061095000514 1.674301601908078 3.502532958984375 4.5 1.644308309897458 3.502532958984375
+4.949999999999999 0 4.324687113933102 4.049999999999999 0 4.324687113933102
+5.849999999999998 0 4.324687113933102 6.709628262419981 0 6.619266800370628
+7.333155391756586 0 5.258818467557074 5.399998782387277 0 5.130161840151898
+4.983861207394446 0 5.902568767191882 7.02435603157911 1.066604484485514 4.645939734868544
+6.316105101755278 0 5.107105498313732 4.950986175498707 1.367591092231864 4.778569437527512
+2.262383638222602 0 2.651142977154722 2.250000000000002 0.8221541549487279 3.502532958984375
+1.385632865645023 0.7840782710301175 3.502532958984375 2.250000000000001 0.8221541549487288 0
+3.754857049141567e-17 8.186353449006043 5.793144226074219 3.857891345865422e-17 8.164026766773862 6.724547569955259
+3.174544638152097 9 6.560107506702646 1.26201591862338 9 4.320595151469462
+2.492019174738081e-16 3.600000000000003 5.146841268881834 2.699687439299589e-16 3.15 4.324687113933104
+2.91646704896766e-16 2.680256466694074 5.148332098884945 2.690367753090915e-16 3.170194991289394 5.860431069526319
+2.283906145980186e-16 4.050963767327655 5.946186415410706 1.453677851930548e-16 5.850000000000001 4.324687113933104
+1.869014381053562e-16 4.949999999999999 4.324687113933102 1.967981314884922 7.414093705075968 4.461402076102305
+1.237045312092714e-16 6.319424845114707 5.148868848276997 2.284350910176574e-16 4.050000000000002 4.324687113933102
+2.492550128080967 4.966683152957035 9 1.644308309897458 5.4 9
+1.646048581551515 6.310090144152493 9 4.499999999999996 5.711383380205097 9
+1.665366703324821 0 5.274230493078946 2.680572132186256 0 5.148863565674953
+0.8136465509939548 0 5.793144226074219 3.417641184659571e-16 1.594253453486617 6.265353121182388
+2.227285614992981 0.848208769141567 9 1.07820560589405e-16 6.663617386716211 5.934384046634641
+1.869715672587539e-16 4.948480359091187 6.552684034173113 1.458110581553121e-16 5.840394640537061 6.594603659942676
+1.661346116492055e-16 5.399999999999999 5.146841268881834 2.076682645615068e-16 4.500000000000002 5.146841268881829
+2.912383567478283e-16 2.689105034259754 7.386842568981676 3.076088082468661e-16 2.33437085327539 5.937204522582435
+3.384345744741552e-16 1.666401925448595 5.269917270212348 3.385920657312671e-16 1.662989219812037 7.251955224101323
+3.777879586315982e-16 0.813646550993953 6.709388732910158 3.777879586315981e-16 0.8136465509939548 5.793144226074219
+1.265340979333769 0.8083059238284518 9 3.793499048283358e-16 0.7798004652660229 7.588885749121868
+4.031626301063632 4.051014451112357 3.502532958984375 1.712864088505812e-15 1.381729327793543 0.790408880410153
+0.8035870212178864 1.38058322059687 0 2.099968729585668e-15 1.693998023888176 1.712490658130463
+1.721068634023537e-15 1.388347752010112 2.693984350998123 1.01499072080023e-15 0.8187704183765003 2.22045837399228
+0.8076655997323066 1.264855409212239 3.502532958984375 1.726056085817312 1.672773929483424 0
+2.700000000000001 1.644308309897459 0 1.265607570583821 0.7958744860005247 0
+1.738265821776716 0 1.783306577302728 1.260307254975383 1.290205587880089 1.267898936654205
+1.60935675641035 2.692443328238967 0 2.243821390912933 0 0.8510361113153238
+5.010324768641584e-15 4.041717449188607 2.659523587678974 0.8221541549487279 4.050000000000001 3.502532958984375
+3.321284730324483e-15 2.679206452302616 1.697628533441581 0.8221541549487279 3.150000000000002 3.502532958984375
+0.8418648579713848 2.23844229067208 0 1.992489919181732 1.217369704353358 2.243940501251569
+2.789043472195432e-15 2.249859278318619 2.620452477576313 2.492539528964916 4.03500237686012 0
+3.904905085433274e-15 3.15 0.8221541549487288 0.8462241685509806 2.226164745713426 3.502532958984375
+0.8221541549487279 4.050000000000004 0 1.644308309897455 4.500000000000002 0
+2.492563066798555 4.034020818435383 3.502532958984375 1.358165138386808 3.146305877517642 1.260172770892353
+2.471252370435407 3.158677941652682 0 4.880590217488306 4.913578065457267 0
+1.367471707723563 4.950768986071829 1.276209971846721 6.610470169072327 2.304830170587466 0
+7.262717180048801 1.672994869075557 0 7.810779003487799 2.007290890328484 2.114741533368118
+7.612021850990101 0 2.694181213237593 5.4 1.644308309897458 0
+7.734073685572545 0.7961324740693421 0 6.750000000000002 0.8221541549487252 0
+5.85 0.8221541549487279 3.502532958984375 6.853903416682458 1.207077518263668 1.430566211365389
+5.86083591842203 0 2.652265452727054 5.840416559212299 0 0.8504608643334883
+6.75 0.822154154948727 3.502532958984375 9 0.811159836019538 2.236388903874039
+8.181278588629141 0 2.220431659649437 1.73041623978976 7.324183433460863 0
+1.385794755087254 8.215593917331269 0 2.250000000000001 8.177845845051275 0
+1.388122069508546 9 2.694251589881064 2.237399376130869 7.633769317297789 1.266640263365508
+9.441452021437055e-15 7.616209156650172 0.7979981878933327 0.8481693740205758 6.772696540605361 0
+0.811057011566092 9 1.266080271527871 9.076834463644209e-15 7.322080290027534 1.74935874165185
+4.499999999999998 7.355691690102542 3.502532958984375 4.684403388455717 7.904809310857763 1.928367125635865
+4.499999999999998 7.355691690102548 0 3.6 7.355691690102544 0
+6.750009380309389 9 2.62046242734959 7.850842406677832 7.221412012267607 2.409870062963366
+6.232394761638372 7.180136050300382 2.53169045583236 5.402229524745811 9 1.689627968019826
+7.306001189108732 9 1.712491174983638 9 6.758559749027269 0.8469846939706702
+4.036509662974769 6.507494808264449 0 8.158135142028618 6.761557709327921 0
+7.273952865953808 7.327240017365964 0 6.873821228009541 6.261058569302966 1.191548303305008
+7.129173317651279 7.849188145736039 1.13360760876498 6.300000000000002 7.355691690102542 0
+5.85 6.533537535153808 0 8.177845845051271 5.85 0
+7.355691690102542 5.399999999999997 0 9 4.949986675970459 0.8725581576213219
+2.390893144036216 6.694913401294983 3.502532958984375 3.174950601667219 7.380468179485771 2.262045825417349
+1.646027211848777 6.310082415674415 3.502532958984375 1.644308309897458 4.499999999999998 3.502532958984375
+7.265067258589187e-15 5.860568019930939 2.652186226986258 6.136279419966573e-15 4.949999999999999 2.680378804035646
+3.395816419503175 5.60355816348719 0 2.492575257816727 4.967474545272637 0
+4.398654362174267 6.290452099937791 1.850814369453527 1.478679428574594 4.004132489883712 2.495278452248179
+2.492554886892543 4.966560957873712 3.502532958984375 4.015220127479317 4.949777192601458 0
+3.902088797826263e-15 3.147728163484645 2.622234087649684 6.507442330340908 4.034577778373006 3.502532958984375
+4.474829027832032 3.822445126596042 2.23746826146113 4.499999999999991 5.711383380205078 3.502532958984375
+5.495497250600507 6.995553215901628 0.9202371554631721 5.600700775239792 5.601318128446502 0
+4.96640350803025 2.492567061200845 3.502532958984375 5.492637532960263 1.939253142585001 2.346484510836783
+6.32846591882542 2.696069593697236 1.031291499364739 4.499999999999996 5.711383380205097 0
+4.071840417178954 4.1057388913643 0 4.500000000000002 1.644308309897455 0
+5.843960356407516 0 6.55358473250369 4.910323096188646 0 6.596477600269889
+5.85 0.8221541549487297 9 6.761557709327921 0.841864857971383 9
+5.849473081195691 1.263501717848643 7.630355104643722 6.300000000000001 0 7.355691690102542
+6.749999999999998 0 8.177845845051273 7.27859427728943 0 7.312031691753818
+8.999999999999996 3.18466138275474 6.563301021180507 8.999999999999996 4.096788817700876 6.596046552031122
+8.177845845051273 4.050000000000004 9 0.8221541549487288 2.250000000000001 9
+0.7844262724598039 1.385815339812601 9 1.72278176119794 0 7.316203381456706
+0.7990333032803036 0 7.594495546215601 1.003977635763048 7.701882061203724 5.560450549000308
+2.333895343713335 9 5.937303744212896 0.813646550993953 9 6.709388732910158
+2.709236727384038 7.391728354161424 9 0.8426000112233183 6.761124816992657 9
+2.250000000000001 8.177845845051275 9 1.26583261791989 8.203757833086939 9
+3.149999999999997 9 8.177845845051273 2.273422470612569 7.870878758733352 7.359516324188068
+3.149999999999999 8.177845845051273 9 1.453677851930548e-16 5.850000000000001 8.177845845051271
+1.295269995456483 5.596479036343183 6.170812759074069 2.46646246484619 5.850000000000007 9
+1.038341322807534e-16 6.750000000000001 8.177845845051275 0.8221541549487297 5.850000000000003 9
+1.737426136051458 7.326756175867176 9 2.390955833641873 6.694835283174674 9
+2.84172903089311 7.797310911474558 5.706930741641156 4.034480682077556 6.507396143436956 3.502532958984375
+1.367487126851482 4.950759670442364 4.782612354384808 2.989444149362254 6.080113815277365 4.875312060476139
+0.8221541549487297 3.150000000000002 9 2.076682645615068e-16 4.500000000000002 7.355691690102546
+1.112510611413525 5.090027364835593 7.773352344516906 2.688673895198963e-16 3.173865441625116 6.560095743409965
+0.8221541549487279 4.050000000000004 9 1.202603559286049 1.884841397928709 6.732645035066986
+2.158709966606393 1.021754206368015 6.145023641925381 8.177845845051273 4.950000000000001 9
+7.355691690102548 4.500000000000002 9 8.999999999999996 6.319705176460237 7.354210196145489
+3.626145564923389 7.762171117012558 6.834805054331691 4.499999999999998 9 7.355691690102546
+4.050000000000001 8.177845845051273 9 4.499999999999998 7.355691690102548 9
+3.6 7.355691690102544 9 2.705669981115502 0 7.399555690881755
+3.149999999999999 0.8221541549487279 9 4.499999999999998 1.644308309897455 9
+4.499999999999998 0 7.355691690102546 4.010959289453773 0 6.596141256793707
+2.370788543568366 0 6.707291627856273 3.149106413733451 1.368730307272833 4.771455137544587
+3.205666220324549 0 5.899842333751687 8.999999999999996 1.723183118641225 7.315871628697508
+8.999999999999996 3.600000000000003 7.355691690102541 8.999999999999996 3.161685037624311 5.860600967171491
+8.177845845051271 2.250000000000001 9 8.177845845051271 3.150000000000002 9
+7.327240017365961 1.726047134046194 9 7.743509431577898 1.215974916510627 7.137174721633494
+7.817845980652187 2.752701533026581 6.884198803662804 8.020741956655929 4.12147652379528 7.488237733261571
+7.355691690102542 3.600000000000005 9 4.651157597442394 1.201469441622003 6.133474938797397
+5.399999999999997 1.644308309897458 9 4.050000000000001 0.8221541549487279 9
+3.6 1.644308309897458 9 2.070406311254981 1.15164870546425 7.968799558026949
+4.049999999999999 0 8.177845845051273 4.656083992526365 4.647170639811237 4.731761280612926
+2.281308586311856e-16 4.056592464871869 6.638375381031729 2.474658912592733 3.167649269635714 9
+3.473245692788399 1.041803753529335 7.020962948104424 2.689895358780513 1.645992820548521 9
+1.108915849154337 3.374574452099298 5.309687165269679 2.304604338934406 2.389740032856024 9
+6.50741949810309 4.967417655615911 3.502532958984375 6.873880007765726 2.738999640775012 4.693967949322342
+5.601464553323696 3.398261994662313 3.502532958984375 4.91526626252087 4.102564379689747 3.502532958984375
+7.355691690102548 3.600000000000001 3.502532958984375 6.53353753515381 3.150000000000002 9
+6.307573544257441 1.609328861694367 9 7.355691690102542 2.700000000000001 9
+6.127171028945219 1.348351802888359 6.063870648583562 6.31112719193005 3.887586712678371 4.825296751464324
+7.830189990412929 3.584428471334689 5.640013334052828 6.339452289192708 2.633860251211294 7.805792112255518
+6.507400693996837 4.032626538823147 9 6.53353753515381 5.850000000000009 9
+6.610256303416595 6.69540728465453 9 7.820032996782768 6.288457543962556 5.61756770065773
+8.04947794158355 5.38252900192215 7.09091113777123 8.999999999999996 7.399728128189468 6.217636047768021
+7.892191921128402 7.889718993825028 5.510181568848432 8.999999999999996 6.672293880279013 6.561822582415857
+8.999999999999996 7.737927554779582 8.181768935824422 8.999999999999996 8.219299867467184 7.588415496666283
+7.410728271539825 7.481325824898019 7.630934666918519 6.8840541614543 5.485425107384142 7.983045039200181
+8.177845845051271 5.850000000000003 9 2.99420838381468 2.997941331611047 7.70087140664915
+4.704925489724342 2.795007087719198 5.172231113119711 4.500112462735338 2.204334657450456 7.696515572764579
+2.756074421382376 4.500039145453402 4.834116654777253 2.993331079466882 2.995087430251663 4.805049393126858
+5.601297766944301 3.399317797011063 9 4.033509255210724 2.492540338781825 9
+4.966407577872396 2.49260482666634 9 1.976979554597179 6.156431803110182 0.9397800430372977
+1.409999223502332 6.798348655184969 2.209991989164708 2.709287412804358 7.391690845735047 0
+1.645990485326495 6.31010452907295 0 0.8221541549487297 5.850000000000003 0
+8.377017215305287e-15 6.757553295378954 0.8471200937044934 7.841757282648762e-15 6.325771023856475 1.713741112305517
+1.644308309897458 5.4 0 3.170055790036303 6.524338484710283 0
+3.600000000000001 1.644308309897455 3.502532958984375 3.39824804842036 3.398094542216297 0
+2.700000000000001 1.644308309897457 3.502532958984375 3.150000000000002 2.466462464846184 3.502532958984375
+3.099295653956634 4.38929992018157 2.572013274946655 2.626252340734906 2.582236258989259 1.032239083454186
+3.575910380863518 5.313506536834836 1.574410986059732 5.41992969478902 5.14393781051181 2.094688442278605
+6.222049100566897 3.53322077222503 2.196722086813038 6.804120221723131 5.409590929096396 2.216962739844684
+3.134583617495037 4.129761100787105 1.008464675355297 4.500000000000002 3.288616619794904 0
+6.533537535153815 3.149999999999995 0 5.258854677441573 4.046588832347953 0.8455863111745281
+4.137369315836552 3.768272067746595 7.553087139982122 3.396793633049016 3.395975580271701 9
+3.288616619794904 4.500000000000002 9 4.02725685369121 4.050793625486284 9
+4.034361577230934 6.507400845492809 9 6.276250216845985 5.573813560052242 5.217553535127384
+4.546788183059729 6.323443973607348 4.91702768469791 5.464009850058902 6.219887749320682 7.701527815909152
+4.464847337083542 5.196389910214907 7.407663070223562 3.39803327691704 5.600890609698826 9
+2.987683583073269 6.211154271940465 7.468063073448845 5.603941122468417 5.603178393639569 9
+5.711383380205101 4.500000000000002 9 4.910613135063196 4.117165881802151 9
+0.7798005455501507 9 7.588885714399234 3.694232695870272e-17 8.199490246306144 7.595896557198724
+0 9 8.083755493164062 1.594252752911451 9 6.265353068531246
+1.030937860178811 7.972020536548865 7.534587946626601 1.165594959393021 6.679332256911397 7.85708839074245
+0.803902847157981 7.619237153795071 9 5.826317718007953e-17 7.737485008294541 8.198452623255248
+2.21857168820862 9 8.147157268886199 1.094565142758638e-16 6.628167619730345 6.707444745638181
+7.953873271574112e-17 7.276461268761527 7.316317193852594 7.38457556197159e-17 7.399823386638358 6.287612012627287
+1.644308309897458 3.600000000000005 9 1.608121865127062 2.711691055038122 9
+1.205682270038791 3.213689042772414 7.8818944862586 2.756375629651225 4.500338519398791 7.670595357327233
+1.644308309897455 4.500000000000002 9 2.069292309936515 3.112420292912163 8.412252197247177
+1.675599573812254 1.730940263980246 9 4.089419371100893 4.893560838576121 3.502532958984375
+2.196179589221646 5.409344882567336 2.216575254654793 3.398537943758818 5.600367600610427 3.502532958984375
+3.537659954436555 2.730968222264597 2.131061227113373 2.126217594568764 2.739288252504493 2.311221714438931
+2.471248957023914 3.158677233991366 3.502532958984375 1.609022482842115 2.692344551262586 3.502532958984375
+2.292733087431425 2.374090842495852 3.502532958984375 1.250066057570371e-16 6.291209963667521 7.399559733429648
+1.644308309897455 3.600000000000001 3.502532958984375 3.396390532107535 3.39598314835346 3.502532958984375
+1.346251653052568 1.493161374368202 4.787729376031738 2.140008714669771 3.239441465431151 4.147818569857803
+4.500000000000002 0 0 5.400000000000001 0 0
+5.850000000000003 0.8221541549487297 0 5.404128821635139 0 1.799956266520455
+4.950000000000001 0.8221541549487279 0 4.498781867297469 0 1.800209859900734
+3.600000000000001 0 0 3.205865101891692 0.8479390395564509 0.7870897827722807
+3.600000000000005 1.644308309897458 0 3.601837126517962 0 1.70243216925798
+2.700000000000001 0 0 2.690618475546087 0 1.710450318913054
+6.188355729947944 4.169119069286622 7.300136425555972
+
+
+ 729 450 53 368 6 34 364 341 419 675 234 55 442 16 17 383 17 16 428 419 365 492 92 825
+493 93 75 492 75 93 276 606 65 270 85 358 464 463 428 64 346 463 208 124 350 53 450 4
+381 54 49 49 54 382 66 13 134 293 103 647 732 235 61 452 61 235 464 463 70 66 38 13
+499 13 38 67 134 129 357 498 646 466 357 356 384 486 386 401 93 95 476 95 93 253 244 243
+472 243 244 81 7 292 82 292 7 7 81 82 289 76 288 332 288 76 685 684 631 98 96 482
+96 98 99 493 75 93 404 93 75 782 105 647 295 104 499 785 647 103 538 120 90 405 90 120
+798 312 511 524 261 527 57 134 13 313 748 752 540 495 196 176 167 131 541 131 167 548 324 618
+200 147 9 66 134 13 67 129 134 608 129 67 326 183 554 320 157 102 175 102 157 360 372 426
+481 99 399 192 324 554 327 556 325 807 555 549 302 560 300 701 225 682 643 644 223 682 225 221
+221 225 269 670 230 312 669 503 504 273 681 268 48 49 382 16 42 1 40 1 42 40 16 1
+252 470 458 205 603 597 9 147 142 142 147 202 538 90 120 536 120 90 57 13 134 178 177 131
+359 292 80 79 80 292 5 289 689 102 0 174 173 174 0 0 102 173 361 23 426 8 81 83
+81 8 321 319 81 8 321 8 81 321 8 426 19 21 340 35 25 24 362 24 25 370 24 25
+362 25 24 27 22 25 26 25 22 52 16 42 16 1 42 49 42 1 1 16 49 61 2 63
+236 63 2 2 61 236 4 450 56 439 4 56 53 54 381 442 49 16 262 265 241 240 217 238
+640 238 217 286 288 3 287 3 288 287 286 3 361 426 23 23 426 8 32 23 8 333 288 286
+33 83 359 290 80 77 387 77 80 290 359 80 5 76 289 5 290 77 241 239 233 736 233 239
+53 4 54 437 54 4 380 17 20 341 20 17 36 22 27 380 20 36 23 32 339 35 27 25
+55 234 732 733 63 56 455 56 63 241 233 234 265 238 239 673 239 238 716 361 362 267 215 217
+693 217 215 638 85 270 690 267 240 272 215 267 285 636 78 333 73 272 333 289 288 266 272 267
+218 240 265 742 274 744 363 35 28 218 690 240 675 262 241 28 35 339 34 27 363 363 27 35
+34 36 27 438 379 6 675 279 262 358 290 5 358 5 689 360 426 372 33 290 358 270 689 266
+731 55 729 468 465 459 438 731 729 336 218 262 306 725 779 368 438 6 37 28 691 691 28 31
+639 218 336 639 335 37 100 7 81 82 81 7 7 100 82 321 426 8 8 30 319 576 566 82
+569 416 816 158 174 101 159 101 174 152 418 9 9 142 152 420 21 150 344 137 10 138 10 137
+138 344 10 136 39 140 144 344 143 145 143 344 41 40 11 44 11 40 148 153 157 411 157 153
+172 154 171 563 412 766 320 102 158 148 157 320 201 147 153 200 153 147 23 8 426 8 23 30
+150 149 151 603 151 149 605 151 142 152 142 151 369 25 22 26 22 25 340 369 22 52 42 16
+342 19 17 11 44 139 319 575 81 318 148 320 201 153 148 203 370 369 370 25 369 605 150 151
+345 11 137 139 137 11 594 140 139 52 16 383 45 41 345 374 341 419 45 342 41 594 136 140
+410 200 9 203 369 420 203 202 146 371 148 318 90 12 89 72 89 12 12 90 72 90 89 12
+121 12 89 121 90 12 494 75 89 94 89 75 399 95 96 483 96 95 399 96 95 398 95 96
+323 84 566 815 818 814 570 98 97 484 97 98 82 84 322 84 82 566 575 100 81 82 322 84
+386 82 84 575 101 100 576 100 101 321 322 82 81 292 83 538 89 90 555 120 107 552 107 120
+546 618 324 547 108 130 546 130 108 557 752 161 350 112 130 506 130 112 128 123 129 123 128 457
+128 129 123 135 128 123 14 13 104 13 14 57 57 14 104 57 14 13 127 13 14 14 57 127
+52 42 51 48 51 42 60 376 50 62 60 50 392 50 376 64 463 346 62 50 60 434 60 50
+407 59 60 389 60 59 284 69 407 284 407 69 165 132 15 188 15 132 188 165 15 283 315 197
+555 107 120 106 120 107 75 89 403 89 75 494 404 75 403 401 95 93 92 93 95 108 107 618
+107 108 549 555 120 538 401 93 493 401 493 93 552 618 107 549 107 555 46 185 408 185 46 198
+316 58 38 462 38 58 32 8 83 318 575 319 33 32 83 81 82 321 494 538 89 480 555 807
+480 555 538 401 399 95 401 95 399 549 547 108 546 108 618 810 547 609 574 570 97 483 482 96
+574 97 570 573 815 194 99 96 399 691 31 85 484 98 482 99 570 98 206 570 574 574 97 84
+384 84 97 574 84 97 323 97 84 316 58 284 316 284 58 48 42 49 342 17 16 383 16 17
+442 17 380 379 381 442 379 442 380 435 51 18 440 18 51 41 16 40 44 40 42 18 590 51
+435 18 51 19 20 17 341 17 20 45 19 342 16 52 383 41 342 16 17 383 341 36 20 22
+364 22 20 380 36 6 19 340 20 374 383 341 379 380 6 340 22 20 364 20 22 341 383 17
+586 69 407 389 376 60 392 590 18 375 18 50 68 50 18 586 407 69 62 60 407 124 123 112
+505 112 123 350 130 547 749 163 161 350 547 130 66 316 38 461 646 460 66 13 38 580 38 13
+18 435 375 43 51 590 435 52 51 438 729 379 375 435 18 346 435 375 375 62 50 364 419 341
+364 341 20 420 369 340 420 340 21 20 341 364 34 6 36 339 35 24 22 364 26 370 29 24
+146 29 370 203 146 370 22 26 364 24 29 23 361 24 23 339 24 23 361 23 24 371 29 146
+427 26 362 362 26 25 25 26 362 37 363 28 30 23 29 24 362 361 31 28 339 371 30 29
+716 362 361 361 362 24 371 318 30 31 339 32 31 32 33 31 33 85 37 335 363 716 372 361
+410 766 412 335 34 363 714 423 373 638 37 691 619 716 427 712 365 428 422 410 418 368 34 335
+365 427 26 427 362 716 427 716 362 365 364 419 364 365 419 64 375 62 429 463 346 609 547 350
+608 129 124 586 407 62 45 595 21 429 346 374 429 419 374 45 21 19 374 419 341 374 435 346
+374 52 435 67 316 66 360 322 574 425 360 206 206 574 360 206 574 570 86 570 99 481 399 99
+86 570 206 608 124 208 65 69 284 65 316 67 156 166 160 393 160 166 586 62 64 616 208 609
+810 547 549 609 350 208 501 502 106 810 549 547 302 755 560 823 169 825 754 399 401 481 754 399
+481 399 754 548 554 324 542 94 492 480 494 538 302 300 540 808 480 807 544 560 327 757 808 756
+477 754 481 477 754 757 477 757 754 558 609 810 503 525 502 276 251 606 558 806 811 276 65 69
+801 621 366 307 744 743 479 631 488 373 372 716 432 477 811 624 86 481 400 563 766 423 86 624
+638 691 85 400 416 563 428 365 419 70 663 586 616 558 609 209 811 558 803 616 251 521 809 257
+712 427 365 368 279 731 368 731 438 428 419 429 712 428 365 639 336 335 619 427 712 638 639 37
+714 619 373 373 423 425 716 619 373 464 712 428 623 204 602 464 428 712 714 624 423 621 663 803
+278 648 680 803 251 616 780 724 742 367 619 714 367 712 619 418 421 422 367 712 464 621 70 464
+209 616 803 801 803 209 209 803 616 706 504 520 715 714 367 432 714 624 432 477 624 532 209 811
+532 432 811 715 801 532 715 532 801 57 134 128 133 57 128 67 134 66 315 38 580 128 134 129
+191 132 165 316 38 58 315 58 38 67 66 316 284 58 59 283 59 58 343 199 136 70 586 64
+343 590 392 595 143 149 597 149 143 136 155 39 347 39 155 138 140 39 141 138 39 345 137 344
+139 140 138 345 41 11 52 51 42 43 42 51 435 51 52 43 44 42 374 435 52 45 345 144
+344 138 141 145 344 141 375 50 18 392 18 50 64 375 346 13 127 125 190 189 185 579 185 189
+314 127 57 317 188 132 186 125 584 65 316 284 65 67 316 316 66 38 186 580 125 13 125 580
+164 584 188 186 584 164 186 164 190 67 65 606 579 408 185 284 59 407 65 284 69 389 388 376
+391 46 408 663 69 586 199 160 155 394 155 160 144 143 595 594 44 43 45 144 595 593 199 343
+388 156 593 388 593 376 198 46 390 406 390 388 283 197 406 186 197 315 747 408 579 295 294 104
+296 104 294 497 293 105 105 293 647 732 61 63 454 236 237 447 237 236 264 243 219 678 219 243
+650 654 648 790 246 247 650 247 246 111 47 784 248 784 47 47 111 248 250 247 111 651 111 247
+436 440 48 442 381 49 375 50 62 51 48 440 64 346 375 429 374 346 437 382 54 733 56 450
+729 53 379 729 55 450 439 437 4 55 733 450 455 63 454 455 439 56 55 732 733 65 284 316
+284 59 58 456 58 59 38 462 499 499 104 13 118 57 104 67 66 134 460 103 295 57 128 134
+128 57 118 284 407 59 407 60 59 355 59 60 456 462 58 355 356 456 586 62 407 355 456 59
+246 219 654 219 246 652 250 111 784 783 497 105 789 248 245 788 245 248 434 355 60 305 237 242
+448 242 237 452 236 61 63 236 454 18 440 68 68 434 50 252 242 244 449 244 242 305 454 237
+453 439 455 252 305 242 253 470 252 264 253 243 650 246 654 678 654 219 282 498 255 660 650 648
+680 648 654 255 264 652 677 253 264 677 264 255 70 64 586 792 497 783 249 250 782 453 305 458
+71 68 440 732 63 733 64 70 463 70 586 663 282 255 652 466 356 433 663 586 69 433 353 465
+433 356 355 496 497 793 65 67 606 460 295 499 608 606 67 608 67 129 461 462 456 276 65 606
+379 53 381 71 353 68 71 440 436 459 353 71 441 280 443 280 71 436 468 459 280 276 663 69
+357 461 356 466 498 357 465 466 433 513 444 779 621 70 663 254 470 677 277 466 465 459 71 280
+281 249 785 254 465 468 277 465 254 72 403 89 298 403 72 298 72 299 297 299 72 644 114 109
+531 109 114 333 286 73 334 73 286 744 700 683 212 109 74 224 74 109 224 212 74 220 221 269
+223 109 212 494 75 493 401 756 493 480 494 493 480 493 756 223 212 222 530 222 212 686 643 681
+516 299 110 529 110 299 527 261 260 115 114 644 570 99 98 86 99 570 5 77 76 291 76 77
+285 332 637 333 689 289 271 287 78 285 78 287 99 399 96 754 401 399 333 272 689 226 211 213
+227 213 211 214 210 636 93 404 476 476 483 95 754 756 401 226 213 214 701 683 227 704 211 226
+518 682 221 574 84 322 79 387 80 292 82 386 321 82 81 360 322 321 360 574 322 384 386 84
+97 484 384 33 359 290 33 358 85 485 637 385 643 223 220 743 683 702 702 682 709 409 226 214
+744 274 700 700 274 718 515 478 476 757 756 754 481 99 86 624 481 86 624 477 481 423 86 206
+425 206 360 331 485 385 423 624 86 87 490 487 514 516 116 116 115 643 514 298 516 514 403 298
+688 515 117 515 514 117 515 404 514 686 116 643 630 409 635 630 488 631 630 87 488 273 117 686
+685 268 684 275 718 274 270 358 689 331 630 485 685 631 479 477 811 757 273 688 117 331 87 630
+820 824 169 99 98 96 395 96 98 816 818 815 158 102 174 568 569 154 131 180 88 178 88 180
+178 131 88 494 89 538 401 493 756 754 401 756 570 97 98 572 98 97 195 171 573 575 158 101
+193 323 566 318 158 575 321 82 322 360 321 322 574 322 84 545 824 167 495 167 824 122 121 89
+494 493 75 480 538 555 122 177 91 90 121 537 122 91 121 537 121 91 480 493 494 94 122 89
+542 177 122 480 756 493 93 92 492 492 94 75 397 820 169 823 92 398 477 481 754 398 92 95
+96 395 398 195 572 323 86 206 570 97 323 572 98 572 395 576 82 100 159 576 101 193 159 172
+568 173 102 193 566 576 195 323 193 817 395 572 542 492 825 819 398 395 414 629 567 819 823 398
+819 397 823 655 113 256 658 245 216 649 216 245 256 113 230 230 113 312 223 644 109 260 228 224
+642 224 228 295 103 294 124 129 123 124 112 350 457 128 118 134 128 129 460 785 103 460 646 785
+293 294 103 294 293 496 296 118 104 119 457 118 782 784 105 783 105 784 480 807 555 549 108 107
+501 107 108 807 549 555 516 298 299 120 106 405 538 90 89 405 72 90 526 106 502 529 297 526
+115 516 110 526 405 106 72 405 297 547 130 108 509 108 130 501 106 107 609 350 547 810 609 547
+509 501 108 260 224 109 115 110 114 528 114 110 643 115 644 506 509 130 609 208 350 651 248 111
+784 248 789 123 457 505 505 506 112 505 457 508 658 789 245 792 789 658 113 216 659 216 113 655
+116 516 115 529 528 110 113 659 312 655 658 216 232 229 261 232 259 231 531 260 109 114 528 531
+310 524 527 667 259 258 251 608 208 616 251 208 777 657 278 686 117 116 523 506 522 608 251 606
+514 116 117 503 500 504 281 785 646 119 118 296 508 457 119 519 119 793 519 508 119 329 508 519
+661 648 278 791 519 793 669 258 524 806 808 757 806 810 807 806 807 808 520 504 330 616 609 558
+521 519 791 521 791 809 209 616 558 669 524 310 669 525 503 706 258 669 124 123 129 584 125 126
+164 188 189 578 189 188 124 112 123 610 123 112 750 162 187 583 187 162 610 112 611 181 161 163
+184 181 163 192 554 182 556 161 181 533 168 326 533 180 168 534 168 180 555 538 120 91 177 178
+179 91 178 350 130 112 611 112 130 181 184 192 608 124 129 314 191 126 584 126 165 191 165 126
+582 187 132 317 132 187 549 108 547 608 208 124 90 537 536 549 555 107 120 536 552 535 552 536
+123 610 135 125 127 126 314 126 127 128 135 133 585 133 135 750 187 582 751 135 610 749 161 752
+608 67 606 585 750 582 348 184 163 348 162 750 124 350 112 130 546 611 541 180 131 179 537 91
+176 131 177 542 176 177 351 324 192 539 550 534 191 582 132 585 314 133 57 133 314 751 585 135
+535 536 537 349 184 348 349 610 611 351 184 349 349 751 610 351 611 546 351 349 611 595 149 150
+136 199 155 343 136 594 598 145 141 139 138 137 199 156 160 156 199 593 428 463 429 347 141 39
+420 150 605 605 142 202 595 150 21 143 145 597 428 429 419 146 202 201 371 146 201 201 202 147
+371 201 148 149 597 603 151 603 152 152 603 604 153 200 411 172 173 154 568 154 173 394 347 155
+156 390 166 388 390 156 601 347 394 159 173 172 157 411 175 411 564 175 390 46 166 391 166 46
+393 394 160 751 348 750 348 163 162 749 162 163 190 164 189 584 165 188 751 750 585 276 69 663
+391 393 166 533 179 180 326 168 183 550 183 168 545 167 176 535 533 326 542 545 176 535 179 533
+167 495 541 545 825 169 542 825 545 539 534 541 183 182 554 182 183 325 168 534 550 548 326 554
+548 535 326 397 814 820 396 820 814 823 397 169 397 194 814 819 194 397 573 171 170 573 170 815
+816 815 170 818 816 567 194 817 573 154 170 171 170 154 569 569 816 170 193 172 171 159 174 173
+102 175 568 179 178 180 541 534 180 196 820 396 824 196 495 824 545 169 540 196 822 192 182 181
+556 181 182 183 550 325 182 325 556 557 161 556 351 192 184 558 810 609 198 190 185 186 190 197
+162 749 583 583 317 187 188 317 578 578 579 189 613 752 557 616 609 208 558 806 810 806 807 810
+553 755 352 563 416 564 425 372 360 195 193 171 564 568 175 423 206 86 819 817 194 817 195 573
+757 754 756 396 629 822 396 822 196 432 624 477 540 300 539 302 540 822 251 608 606 803 251 276
+301 577 748 197 198 406 197 190 198 621 663 70 589 391 408 377 393 588 464 70 463 373 716 372
+419 429 374 619 427 716 373 619 716 623 421 204 712 365 427 203 420 605 601 207 598 207 205 598
+367 464 712 619 712 427 715 367 714 378 602 377 204 604 205 204 205 207 425 206 423 373 425 423
+602 204 207 602 601 377 602 207 601 621 803 663 596 378 588 70 64 463 596 588 589 251 208 608
+753 313 612 616 208 251 801 209 803 714 423 624 715 714 432 303 400 766 714 373 423 553 327 560
+806 811 757 806 757 808 806 808 807 627 596 612 424 303 422 765 414 303 303 414 400 623 602 378
+367 366 464 621 464 366 553 613 557 801 532 209 715 432 532 304 765 303 627 378 596 628 617 352
+628 755 765 628 304 561 636 210 78 633 78 210 272 73 215 692 215 73 334 287 271 73 334 692
+267 217 240 266 689 272 690 266 267 684 704 226 214 213 210 632 210 213 409 684 226 704 225 211
+701 211 225 633 271 78 635 214 636 223 222 220 212 224 530 260 261 228 632 633 210 213 227 632
+635 409 214 240 238 265 215 692 693 649 659 216 250 790 247 249 790 250 265 239 241 217 693 640
+262 218 265 639 690 218 253 252 244 677 470 253 652 264 219 681 643 220 220 222 221 518 221 222
+222 530 518 474 736 337 234 233 235 263 235 233 337 736 673 675 241 234 228 261 229 641 228 229
+642 530 224 800 518 530 704 269 225 268 269 704 701 227 211 684 268 704 641 642 228 524 232 261
+232 231 229 666 229 231 666 641 229 259 230 231 670 231 230 258 259 232 524 258 232 259 256 230
+667 256 259 670 666 231 732 234 235 233 736 263 235 263 452 236 452 447 453 454 305 458 305 252
+237 447 448 238 640 673 673 736 239 471 448 451 242 448 449 472 678 243 244 449 472 660 649 788
+655 809 658 792 658 809 245 788 649 660 788 651 652 246 790 282 652 790 247 650 651 651 788 248
+512 659 649 791 792 809 282 790 249 281 282 249 667 520 257 803 276 251 679 472 467 468 441 458
+277 254 677 277 677 255 257 256 667 257 809 655 257 655 256 667 258 706 795 794 707 795 666 794
+711 795 707 709 682 518 336 262 279 731 279 675 474 263 736 474 451 263 277 255 498 772 680 679
+698 674 640 638 690 639 724 698 717 638 266 690 719 693 692 694 271 633 273 686 681 273 268 685
+268 681 269 409 631 684 700 718 632 638 270 266 275 694 718 694 633 718 694 334 271 275 719 694
+630 635 485 432 624 714 432 811 477 743 744 683 532 811 432 273 685 688 769 328 707 702 328 743
+711 707 328 769 743 328 669 310 525 711 709 800 367 714 619 714 373 619 717 275 274 717 719 275
+367 619 712 338 337 674 803 663 276 444 679 467 277 498 466 278 680 772 777 278 772 336 279 368
+725 474 337 338 674 724 725 338 779 468 280 441 464 428 463 471 306 467 308 794 798 209 558 811
+510 798 511 520 329 521 510 308 798 726 474 725 731 675 55 306 726 725 513 779 309 661 512 660
+281 498 282 657 510 511 777 510 657 657 661 278 513 777 772 801 209 532 770 510 777 742 717 274
+742 724 717 715 432 714 780 742 781 780 779 338 769 307 743 771 781 307 354 781 771 770 354 771
+801 366 621 309 781 354 621 366 464 58 315 283 389 283 406 59 283 389 62 407 60 636 285 637
+334 286 287 285 287 288 288 332 285 291 385 637 291 332 76 77 387 291 291 637 332 83 292 359
+486 387 79 79 292 386 496 293 497 496 296 294 529 526 517 299 297 529 544 539 300 544 300 560
+301 748 313 753 612 596 301 747 577 477 757 811 402 302 822 757 756 808 402 755 302 402 822 629
+553 352 613 553 560 755 424 421 623 715 366 367 424 422 421 424 304 303 471 451 726 471 726 306
+769 771 307 770 308 510 770 769 308 513 309 354 513 772 444 621 464 70 808 807 480 688 478 515
+479 311 478 770 777 354 310 517 525 500 523 504 307 781 742 479 488 311 715 366 801 520 330 329
+521 329 519 794 670 798 657 512 661 659 512 511 798 670 312 251 276 606 511 312 659 748 577 583
+748 749 752 301 313 753 613 313 752 585 191 314 577 317 583 747 579 577 753 589 747 577 578 317
+318 319 30 372 361 426 360 426 321 716 361 372 410 412 200 412 564 411 193 576 159 422 766 410
+564 416 569 400 567 416 548 552 535 544 550 539 544 327 325 540 539 495 402 765 755 557 327 553
+628 352 755 711 800 795 711 328 709 500 502 501 330 523 522 329 330 522 500 509 523 310 527 517
+717 698 719 373 425 372 291 387 385 694 719 692 331 490 87 425 360 372 331 486 490 694 692 334
+336 368 335 725 337 338 726 451 474 464 366 367 436 437 443 444 306 779 441 443 453 309 779 780
+780 338 724 604 603 205 598 205 597 374 52 383 598 597 145 139 44 594 343 594 590 204 421 604
+345 344 144 374 346 435 598 347 601 348 751 349 327 557 556 351 546 324 613 612 313 352 612 613
+433 434 353 513 354 777 433 355 434 461 456 356 357 646 461 490 486 384 79 386 486 360 321 426
+372 426 361 427 362 26 715 367 366 309 780 781 365 26 364 203 605 202 418 410 9 412 411 200
+152 604 418 373 372 425 303 766 422 593 392 376 346 375 435 394 377 601 378 377 588 627 623 378
+374 341 383 436 48 382 374 383 52 487 490 484 488 87 487 490 384 484 486 385 387 389 406 388
+588 391 589 64 62 375 588 393 391 343 392 593 753 596 589 377 394 393 86 99 481 819 395 817
+567 629 818 400 414 567 823 825 92 402 414 765 480 538 494 514 404 403 526 297 405 747 589 408
+328 702 709 311 483 478 487 482 311 567 816 416 481 477 624 817 572 195 418 604 421 424 623 561
+627 561 623 365 26 427 365 364 26 424 561 304 628 765 304 463 428 429 432 715 532 586 64 62
+353 434 68 429 346 463 436 382 437 443 437 439 439 453 443 441 453 458 444 467 306 448 447 451
+471 449 448 447 452 451 453 455 454 452 263 451 496 793 119 69 65 276 329 522 508 460 499 462
+353 459 465 280 436 443 444 772 679 462 461 460 801 621 803 679 680 678 470 254 468 467 449 471
+679 678 472 468 458 470 467 472 449 515 476 404 479 478 688 808 480 756 478 483 476 487 484 482
+311 482 483 488 487 311 423 206 425 486 331 385 496 119 296 785 249 647 793 497 792 281 646 498
+500 501 509 806 558 810 503 502 500 525 526 502 508 522 505 522 506 505 523 509 506 511 512 657
+517 527 528 529 517 528 709 518 800 520 521 257 667 706 520 669 504 706 504 523 330 800 641 795
+517 526 525 527 531 528 800 530 642 527 260 531 535 537 179 539 541 495 542 122 94 807 549 810
+548 618 552 544 325 550 616 209 558 628 561 617 564 412 563 564 569 568 574 206 360 318 320 158
+577 579 578 580 186 315 406 198 390 585 582 191 276 663 803 598 141 347 590 594 43 350 124 208
+532 811 209 352 617 612 808 756 480 627 612 617 621 801 803 561 627 617 366 715 801 402 629 414
+631 409 630 700 632 227 718 633 632 637 485 635 227 683 700 635 636 637 724 674 698 719 698 693
+698 640 693 674 673 640 674 337 673 641 800 642 681 220 269 249 782 647 512 649 660 792 783 789
+660 651 650 648 661 660 794 666 670 795 641 666 680 654 678 702 683 701 702 701 682 479 688 685
+794 308 707 744 307 742 747 301 753 748 583 749 769 770 771 782 250 784 784 789 783 791 793 792
+769 707 308 558 811 806 549 807 810 757 811 806 194 815 814 818 629 396 818 396 814 196 824 820
+730 729 53 450 727 368 34 6 445 364 419 341 734 675 55 234 730 53 381 382 383 442 17 16 763 428 365 419 551 492 825 92
+492 493 75 93 607 276 65 606 696 270 358 85 812 710 106 502 592 464 428 463 592 590 591 600 592 64 463 346 664 208 350 124
+592 392 375 590 437 53 4 450 49 54 382 381 645 129 118 134 499 66 134 13 496 293 647 103 452 732 61 235 774 464 70 463
+499 66 13 38 507 67 129 134 676 357 646 498 662 466 356 357 489 384 386 486 476 401 95 93 472 253 243 244 82 81 292 7
+332 289 288 76 699 685 631 684 99 98 482 96 404 493 93 75 413 814 397 396 782 105 647 497 118 295 499 104 645 785 103 647
+786 645 497 647 813 808 757 741 405 538 90 120 668 798 511 312 671 524 527 261 499 57 13 134 745 313 752 748 821 540 196 495
+541 176 131 167 559 548 618 324 200 147 9 202 580 66 13 134 746 67 134 129 746 608 67 129 325 326 554 183 175 320 102 157
+565 360 426 372 571 816 171 573 565 361 426 30 415 481 399 99 559 192 554 324 410 202 418 417 559 327 325 556 805 807 549 555
+759 302 300 560 701 225 682 269 703 643 223 644 221 225 269 682 786 782 647 497 670 230 312 256 668 655 256 312 708 701 682 269
+710 669 504 503 708 273 268 681 48 49 382 381 40 16 1 42 475 252 458 470 599 205 597 603 142 147 202 9 536 538 120 90
+127 57 134 13 541 178 131 177 79 489 359 386 79 359 80 292 695 5 689 289 173 102 174 0 361 30 23 426 321 8 83 81
+321 319 8 81 321 32 8 426 591 19 340 21 362 35 24 25 362 370 25 24 152 202 417 418 26 27 25 22 599 340 21 591
+52 442 16 42 49 16 42 1 236 61 63 2 439 4 56 450 53 54 381 382 442 49 16 42 728 262 241 265 640 240 238 217
+287 286 3 288 430 321 426 32 361 339 426 23 32 23 8 426 285 333 286 288 430 33 359 83 387 290 77 80 359 83 489 430
+387 290 80 359 291 5 289 76 291 5 77 290 452 454 61 732 736 241 233 239 437 53 54 4 734 452 234 732 341 380 20 17
+26 36 27 22 364 380 36 20 339 32 426 23 362 35 25 27 734 55 732 234 455 733 56 63 263 241 234 233 673 265 239 238
+722 716 362 361 693 267 217 215 696 638 270 85 697 690 240 267 693 272 267 215 285 271 636 78 334 333 272 73 285 333 288 289
+696 266 267 272 697 218 265 240 723 742 744 274 722 363 28 35 697 218 240 690 728 675 241 262 722 28 339 35 430 339 32 426
+431 34 363 27 431 363 35 27 431 34 27 36 730 437 53 382 727 438 6 379 728 675 262 279 695 358 5 290 695 358 689 5
+713 360 372 426 430 33 358 290 722 361 430 713 696 270 266 689 734 731 729 55 473 468 459 465 727 438 729 731 728 336 262 218
+775 306 779 725 727 368 6 438 722 37 691 28 722 691 31 28 697 639 336 218 431 639 37 335 82 100 81 7 321 319 426 8
+319 30 426 8 576 566 82 321 571 569 816 416 159 158 101 174 152 202 418 9 152 142 202 9 599 420 150 21 138 344 10 137
+347 136 140 39 145 144 143 344 44 41 11 40 411 148 157 153 569 172 171 154 626 563 766 412 568 320 158 102 565 320 158 571
+411 148 320 157 200 201 153 147 30 23 426 8 565 319 30 426 603 150 151 149 152 605 142 151 26 369 22 25 26 340 22 369
+52 40 42 16 341 342 17 19 139 44 345 11 321 319 81 575 565 318 320 148 411 201 148 153 417 203 369 370 362 370 369 25
+603 605 151 150 139 345 137 11 600 594 139 140 52 342 16 383 44 45 345 41 591 374 419 341 591 45 41 342 600 594 140 136
+410 200 9 202 604 417 152 605 417 203 420 369 417 203 146 202 565 371 318 148 72 90 89 12 121 90 12 89 94 494 89 75
+483 399 96 95 398 399 95 96 323 322 84 566 413 815 814 818 484 570 97 98 566 82 322 84 321 575 81 100 386 82 84 322
+576 575 100 101 386 321 82 322 386 81 83 292 121 538 90 89 405 538 403 89 552 555 107 120 615 546 324 618 546 547 130 108
+562 557 161 752 506 350 130 112 457 128 129 123 135 128 123 129 57 14 104 13 127 57 13 14 48 52 51 42 62 60 50 376
+392 375 50 376 446 64 346 463 434 62 60 50 389 407 60 59 469 284 407 69 607 284 69 407 188 165 15 132 607 283 197 315
+645 118 499 134 106 555 120 107 494 75 403 89 543 179 177 537 404 494 75 403 92 401 93 95 549 108 618 107 106 555 538 120
+492 401 493 93 476 401 93 493 552 549 618 107 501 549 555 107 198 46 408 185 469 355 407 284 462 316 38 58 321 32 83 8
+565 318 319 575 430 33 83 32 386 321 83 81 321 82 100 81 576 321 100 575 494 538 89 403 805 480 807 555 812 405 106 538
+812 480 538 555 551 415 92 825 398 401 95 399 476 401 399 95 509 549 108 547 546 549 108 618 615 810 609 547 484 574 97 570
+483 99 482 96 572 574 570 97 413 573 194 815 395 99 399 96 430 691 85 31 484 99 98 482 572 99 98 570 489 206 574 570
+384 574 84 97 323 574 97 84 283 316 284 58 507 469 65 284 456 316 58 284 48 442 42 49 383 342 16 17 383 442 380 17
+730 48 382 381 730 379 442 381 445 379 380 442 440 435 18 51 52 41 40 16 44 52 40 42 435 18 51 590 591 383 342 52
+341 19 17 20 591 45 342 19 383 52 442 16 16 342 52 41 341 383 380 17 364 36 22 20 727 380 6 36 341 19 20 340
+591 374 341 383 727 379 6 380 364 340 20 22 591 341 19 342 341 383 17 342 607 586 407 69 389 62 376 60 592 375 435 590
+392 375 590 18 68 375 50 18 469 586 69 407 434 62 407 60 505 124 112 123 506 350 547 130 749 348 163 161 611 350 130 547
+462 66 38 316 662 461 460 646 580 66 38 13 375 435 590 18 43 435 51 590 440 435 51 52 730 438 379 729 68 375 18 435
+446 346 375 435 375 62 50 376 591 364 341 419 591 341 340 19 364 341 20 340 763 420 340 369 21 340 599 420 364 341 380 20
+727 34 36 6 445 364 341 380 361 339 24 35 26 364 36 22 361 370 24 29 431 26 364 36 417 146 370 29 417 203 370 146
+22 340 26 364 763 26 340 364 361 24 23 29 361 339 23 24 417 371 146 29 417 427 362 26 362 26 25 369 417 362 370 369
+362 26 27 25 431 362 26 27 722 37 28 363 361 30 29 23 361 362 35 24 722 31 339 28 722 361 362 35 565 371 29 30
+417 716 361 362 417 361 29 370 361 362 24 370 417 565 29 361 565 371 30 318 565 361 30 29 430 31 32 339 430 361 722 339
+430 31 33 32 430 31 85 33 431 37 363 335 713 716 361 372 626 410 412 766 431 335 363 34 713 714 373 423 722 638 691 37
+721 619 427 716 773 712 428 365 417 422 418 410 431 368 335 34 431 365 26 427 763 417 369 26 417 427 716 362 722 427 362 716
+431 362 27 35 431 722 362 35 445 365 419 364 431 727 36 364 763 364 419 365 431 26 36 27 763 26 369 340 587 375 376 62
+446 64 62 375 592 429 346 463 615 609 350 547 746 208 610 802 505 608 124 129 758 620 532 811 587 392 376 375 587 586 62 407
+591 45 21 595 592 429 374 346 445 429 374 419 591 45 19 21 445 374 341 419 445 341 383 380 591 43 590 435 445 374 346 435
+591 52 342 41 591 44 41 45 445 442 52 730 445 374 435 52 507 67 66 316 574 322 565 360 565 425 206 360 565 321 319 426
+565 576 575 321 430 321 32 83 491 484 482 99 489 206 360 574 571 206 570 574 565 322 574 571 625 86 99 570 491 481 99 399
+491 86 206 570 664 608 208 124 469 65 284 69 507 65 67 316 393 156 160 166 587 586 64 62 664 616 609 208 710 810 549 547
+802 609 208 350 710 501 106 502 615 810 547 549 759 302 560 755 821 823 825 169 415 754 401 399 687 481 399 754 415 481 754 399
+812 502 525 710 559 548 324 554 551 542 492 94 805 756 480 551 551 480 538 494 812 494 403 538 759 302 540 300 805 808 807 480
+559 544 327 560 687 757 756 808 687 477 481 754 804 477 757 754 699 487 311 491 687 477 754 757 740 558 810 609 710 503 502 525
+802 276 606 251 813 558 811 806 607 276 69 65 761 801 366 621 739 307 743 744 699 479 488 631 713 373 716 372 804 432 811 477
+625 624 481 86 626 400 766 563 625 423 624 86 696 638 85 691 626 400 563 416 445 428 419 365 614 70 586 663 615 616 609 558
+758 209 558 811 802 803 251 616 656 521 257 809 773 712 365 427 417 604 421 763 728 368 335 431 728 368 731 279 727 368 438 731
+445 428 429 419 763 712 365 428 697 639 335 336 721 619 712 427 722 638 37 639 762 714 373 619 762 372 425 565 713 373 425 423
+721 716 373 619 773 464 428 712 599 623 602 204 763 464 712 428 738 714 423 624 774 621 803 663 778 278 680 648 665 803 616 251
+826 780 742 724 762 367 714 619 761 367 619 712 422 421 417 418 773 367 464 712 614 621 464 70 758 209 803 616 740 801 209 803
+740 209 616 803 656 706 520 504 721 715 367 714 620 432 624 714 738 432 624 477 740 532 811 209 620 532 811 432 705 621 803 774
+705 715 532 801 620 715 801 532 133 57 128 134 746 67 66 134 315 66 38 580 133 128 129 134 317 191 165 132 315 316 58 38
+746 67 316 66 283 284 59 58 600 343 136 199 587 70 64 586 592 343 392 590 597 595 149 143 347 136 39 155 141 138 39 140
+141 345 344 137 141 139 138 140 44 345 11 41 43 52 42 51 44 52 41 40 594 139 345 600 591 44 52 41 591 43 52 44
+43 435 52 51 43 44 42 52 591 374 52 435 591 594 45 600 600 45 144 345 141 138 137 344 141 344 144 145 392 375 18 50
+592 64 346 375 125 127 134 13 579 190 185 189 314 127 57 134 317 188 132 165 746 186 584 125 607 65 284 316 746 580 66 315
+607 65 316 67 38 66 315 316 746 186 125 580 580 125 134 13 746 580 125 134 746 186 315 581 317 164 188 584 581 186 164 584
+581 186 190 164 607 67 606 65 579 198 408 185 389 284 407 59 607 65 69 284 587 389 376 388 408 46 390 391 607 663 586 69
+614 463 587 70 394 199 155 160 597 144 595 143 591 594 43 44 600 45 595 144 600 593 592 343 600 593 343 199 587 388 593 156
+587 388 376 593 390 46 408 198 607 587 406 389 587 406 388 390 607 283 406 197 581 186 315 197 746 314 134 125 581 747 579 408
+296 295 104 294 105 293 647 497 454 732 63 61 447 454 237 236 678 264 219 243 653 650 648 654 650 790 247 246 248 111 784 47
+651 250 111 247 730 436 48 440 48 52 42 442 730 48 442 52 48 442 49 381 68 375 62 50 440 48 52 51 446 64 375 346
+730 52 440 48 445 429 346 374 437 53 382 54 439 733 450 56 730 729 379 53 734 729 450 55 439 437 4 450 734 55 450 733
+454 63 733 455 455 439 56 733 734 55 733 732 734 455 733 439 507 65 316 284 456 284 58 59 499 462 66 38 499 57 104 13
+118 499 57 104 507 67 134 66 645 460 295 103 118 57 134 128 355 284 59 407 662 507 461 456 355 407 59 60 507 456 316 462
+456 462 58 316 469 355 456 356 469 586 407 62 355 456 59 284 469 64 62 446 652 246 654 219 789 250 784 111 783 782 497 105
+788 789 245 248 434 355 60 407 469 434 407 355 448 305 242 237 452 454 236 61 454 236 61 63 68 440 435 18 68 434 50 62
+446 68 440 435 446 68 62 434 449 252 244 242 447 305 237 454 734 453 455 439 449 252 242 305 475 253 252 470 472 264 243 253
+650 652 246 654 676 778 680 648 678 652 654 219 676 282 255 498 653 660 648 650 676 680 654 648 676 255 652 264 475 677 264 253
+676 677 255 264 774 70 586 64 786 792 783 497 786 249 782 250 443 439 734 730 734 453 458 305 445 346 463 446 446 71 440 68
+733 63 454 732 446 469 434 62 446 68 375 62 774 64 463 70 774 70 663 586 676 282 652 255 469 466 433 356 469 663 69 586
+446 433 465 353 469 433 355 356 469 434 62 407 645 496 793 497 507 65 606 67 507 499 462 66 645 460 499 295 507 608 67 606
+507 608 129 67 507 461 456 462 469 65 276 507 507 276 606 65 507 461 460 662 730 439 450 437 730 379 381 53 446 71 68 353
+446 71 436 440 446 459 71 353 473 441 443 280 473 475 458 470 473 436 443 730 473 280 436 71 473 468 280 459 469 276 69 663
+662 357 356 461 676 466 357 498 469 465 433 466 775 513 779 444 774 621 663 70 475 254 677 470 475 277 465 466 473 459 280 71
+653 281 785 249 473 254 468 465 475 277 254 465 405 72 89 403 297 298 72 403 297 298 299 72 531 644 109 114 334 333 73 286
+739 744 683 700 224 212 74 109 220 221 269 682 224 223 212 109 404 494 493 75 687 401 493 756 812 480 493 494 812 480 756 493
+530 223 222 212 703 686 681 643 529 516 110 299 812 405 538 403 671 527 260 261 531 115 644 114 484 570 98 99 491 86 570 99
+695 291 5 289 291 5 76 77 695 285 637 332 695 333 289 689 285 271 78 287 483 99 96 399 687 754 399 401 491 483 99 482
+695 333 689 272 227 226 213 211 632 214 636 210 476 404 493 93 476 483 95 399 687 476 399 483 687 754 401 756 632 226 214 213
+739 701 227 683 701 704 226 211 518 220 682 221 384 574 322 84 79 387 80 359 386 82 81 292 386 321 81 82 489 386 321 83
+489 360 321 322 489 360 322 574 384 386 84 322 384 484 574 97 430 33 290 359 430 33 85 358 695 485 385 637 695 291 290 5
+430 695 387 290 206 491 713 423 703 643 220 223 703 220 269 682 739 743 702 683 703 702 709 682 708 703 269 682 634 409 214 226
+723 744 700 274 700 227 739 634 723 700 718 274 687 515 476 478 687 757 754 756 687 476 401 399 491 481 86 99 491 624 86 481
+738 624 481 477 491 483 399 99 489 384 484 574 491 423 206 86 491 489 484 570 489 425 360 206 489 331 385 720 489 484 570 574
+720 331 385 485 491 423 86 624 720 87 487 490 491 484 99 570 672 514 116 516 672 116 643 115 812 687 515 404 812 756 808 687
+812 514 516 298 812 404 403 494 812 514 298 403 672 688 117 515 672 515 117 514 812 515 514 404 687 476 404 493 672 686 643 116
+634 630 635 409 699 478 688 687 699 630 631 488 720 630 488 87 672 273 686 117 699 688 685 708 708 685 684 268 723 275 274 718
+695 270 689 358 720 331 485 630 699 685 479 631 741 477 757 811 672 273 117 688 720 331 630 87 821 820 169 824 395 99 96 98
+413 816 815 818 568 158 174 102 568 569 154 172 178 131 88 180 122 494 538 89 551 401 756 493 415 754 756 401 572 570 98 97
+571 195 573 171 625 99 481 415 576 575 101 158 571 193 566 323 575 158 565 318 571 625 206 565 566 321 322 82 565 360 322 321
+571 566 322 323 84 322 323 574 495 545 167 824 122 121 89 538 551 94 494 122 94 494 75 493 551 480 555 538 537 122 91 177
+537 121 538 90 537 122 121 91 551 480 494 493 94 122 89 494 543 542 122 177 551 492 493 94 551 480 493 756 551 492 92 401
+492 92 401 93 492 94 75 493 821 397 169 820 415 823 398 92 415 477 754 481 415 398 399 401 398 92 95 401 398 395 399 96
+415 395 99 399 571 195 323 572 625 86 570 206 572 323 574 97 571 572 574 570 98 99 572 395 625 572 99 395 576 321 82 100
+565 576 321 566 159 576 101 158 571 193 172 159 568 173 102 174 571 193 576 566 571 159 158 576 568 320 571 158 571 195 193 323
+625 817 572 395 551 542 825 492 415 819 395 398 414 413 629 567 415 819 398 823 625 415 817 395 415 819 823 397 651 789 111 250
+655 113 256 312 649 658 216 245 230 113 312 256 260 223 109 644 642 260 224 228 496 295 294 103 505 124 123 129 118 499 134 57
+505 124 350 112 118 128 129 457 129 128 118 134 645 460 103 785 645 460 785 646 496 293 103 294 296 118 104 295 645 119 118 457
+783 782 105 784 250 783 782 786 786 783 782 497 812 480 555 807 501 549 107 108 710 807 555 549 529 516 299 298 405 106 538 120
+405 538 89 90 405 72 90 89 812 526 502 106 812 529 526 297 671 529 516 528 528 115 110 516 812 526 106 405 297 405 403 72
+812 297 405 403 509 547 108 130 710 807 810 813 710 501 555 106 501 106 107 555 664 609 547 350 664 810 547 609 710 509 549 501
+509 501 108 549 260 224 109 223 528 115 114 110 671 643 644 115 506 509 130 547 664 506 547 509 664 609 350 208 651 789 248 111
+789 248 111 784 505 457 129 123 505 506 112 350 664 508 608 505 129 505 508 457 664 505 350 506 788 658 245 789 787 792 658 789
+655 113 659 216 672 116 115 516 529 528 110 516 787 668 655 809 655 113 312 659 655 658 216 659 641 232 261 229 799 797 308 737
+670 232 231 259 531 260 109 644 531 528 115 114 671 531 528 115 668 670 312 256 671 310 527 524 796 667 258 259 511 659 787 668
+664 251 208 608 664 616 208 251 778 777 278 657 668 655 312 659 672 686 116 117 671 260 644 703 671 531 115 644 250 789 783 786
+664 656 330 504 664 505 608 124 664 523 522 506 664 505 124 350 665 608 606 251 664 710 509 547 664 506 350 547 671 528 516 115
+672 514 117 116 710 503 504 500 710 509 547 549 812 672 687 741 812 297 403 298 653 281 646 785 645 119 296 118 645 496 103 295
+645 508 119 457 508 505 129 608 507 508 129 608 645 519 793 119 645 519 119 508 653 646 676 662 656 329 519 508 778 661 278 648
+645 791 793 519 796 669 524 258 757 808 813 806 813 740 710 741 813 806 807 810 813 806 808 807 656 520 330 504 740 616 558 609
+656 521 791 519 664 507 608 508 656 521 809 791 740 209 558 616 671 669 310 524 710 669 503 525 796 706 669 258 135 124 129 123
+191 584 126 125 578 164 189 188 610 124 123 112 583 750 187 162 610 350 112 611 184 181 163 161 556 192 182 554 556 192 161 181
+550 533 326 168 534 533 168 180 536 555 120 538 537 122 538 121 537 122 551 538 179 91 178 177 611 350 112 130 192 184 161 181
+746 608 129 124 314 191 126 125 191 584 165 126 317 582 132 187 546 549 547 108 615 546 549 547 746 608 124 208 536 537 538 90
+551 122 537 543 805 552 549 618 552 549 107 555 552 536 555 120 551 552 536 555 543 535 536 552 135 610 124 123 746 135 610 124
+745 583 749 750 314 125 126 127 314 125 127 134 133 135 129 128 746 133 135 129 746 585 135 133 583 750 582 187 746 751 610 135
+562 749 752 161 746 608 606 67 746 585 582 750 348 184 163 161 749 348 750 162 802 746 349 610 112 350 610 124 611 546 547 130
+615 611 547 350 541 178 180 131 179 537 91 177 541 176 177 131 543 542 177 176 562 351 192 324 543 539 534 550 746 582 191 581
+317 191 132 582 746 585 133 314 314 133 134 57 746 314 133 134 746 751 135 585 746 314 125 191 543 535 537 536 543 537 177 122
+562 349 348 184 802 349 611 610 562 351 349 184 746 349 610 751 615 351 546 611 562 351 611 349 562 192 184 161 603 595 150 149
+347 136 155 199 600 343 594 136 600 141 139 345 600 598 141 145 141 139 137 138 141 139 345 137 600 144 141 345 393 199 160 156
+593 199 393 156 592 428 429 463 347 141 39 140 600 347 140 141 599 420 605 150 417 202 152 605 152 605 202 142 599 595 21 150
+597 145 144 143 600 144 597 145 592 428 419 429 417 146 201 202 417 371 201 146 200 201 147 202 565 371 148 201 603 597 595 149
+599 603 595 150 151 605 603 152 604 603 605 152 411 200 201 153 417 200 565 412 568 172 154 173 394 347 155 199 391 156 166 390
+587 388 156 390 600 347 199 136 600 601 394 347 568 159 172 173 175 411 320 157 571 568 158 159 565 411 148 320 571 411 175 564
+391 390 166 46 393 394 160 199 622 393 199 394 745 751 750 348 562 348 161 184 749 348 162 163 578 190 189 164 317 584 188 165
+317 191 584 165 746 751 585 750 587 391 156 393 607 276 663 69 607 390 581 406 581 390 408 198 391 393 166 156 541 533 180 179
+550 326 183 168 495 545 176 167 543 535 326 533 543 541 177 179 543 542 176 545 543 535 533 179 541 495 176 167 821 545 169 825
+551 542 545 825 543 539 541 534 325 183 554 182 550 534 533 168 559 548 554 326 543 548 326 535 543 550 534 533 396 397 820 814
+821 823 169 397 413 397 814 194 413 819 397 194 804 624 415 625 816 573 170 171 816 573 815 170 413 818 567 816 413 194 573 817
+569 154 171 170 569 816 170 171 571 193 171 172 568 159 173 174 568 159 174 158 568 175 320 102 571 320 568 175 543 541 495 176
+543 541 176 177 541 179 178 177 541 179 180 178 543 541 179 533 541 534 180 533 821 196 396 820 821 824 495 196 821 824 169 545
+821 540 822 196 556 192 181 182 559 325 550 326 325 550 326 183 556 325 554 182 559 556 554 192 562 557 556 161 562 351 184 192
+615 558 609 810 581 579 198 408 579 198 185 190 581 578 317 164 581 186 197 190 583 749 750 162 583 317 187 582 578 317 164 188
+578 579 189 190 581 578 190 579 581 301 753 745 582 750 745 746 581 578 164 190 581 583 582 317 581 317 582 191 581 317 584 164
+581 317 191 584 562 613 557 752 802 616 208 609 615 558 810 806 559 562 192 324 559 562 556 192 805 559 615 759 805 806 810 807
+764 553 352 755 565 411 200 201 571 563 564 416 565 425 360 372 571 195 171 193 571 569 172 171 571 564 175 568 571 568 172 569
+625 423 86 206 413 819 194 817 571 817 573 195 804 757 756 754 415 396 822 629 821 396 196 822 396 413 415 397 804 432 477 624
+543 495 545 176 821 551 495 545 759 540 539 300 759 302 822 540 745 562 348 749 745 749 348 750 802 251 606 608 802 803 276 251
+607 663 803 614 745 301 748 577 581 197 406 198 587 391 408 390 581 197 198 190 607 408 587 589 614 621 70 663 408 391 587 589
+622 377 588 393 587 391 390 156 614 464 463 70 622 600 394 199 587 393 593 622 565 411 201 148 762 373 372 716 571 412 563 626
+592 419 374 429 565 200 417 201 417 200 202 201 417 619 716 427 762 626 417 565 762 373 716 619 599 623 204 421 763 712 427 365
+417 203 605 420 599 605 603 150 599 601 598 207 141 144 600 145 600 595 599 597 600 141 140 139 597 144 600 595 600 347 136 140
+599 207 598 205 761 367 712 464 763 619 427 712 762 715 714 367 622 378 377 602 599 204 205 604 599 204 207 205 625 425 423 206
+762 373 423 425 599 602 207 204 622 602 377 601 599 602 601 207 614 621 663 803 622 596 588 378 614 801 803 760 463 64 587 70
+614 596 589 588 802 251 608 208 745 753 612 313 802 616 251 208 758 801 803 209 625 714 624 423 626 413 414 567 620 715 432 714
+626 303 766 400 762 714 423 373 559 553 560 327 804 806 757 811 805 806 808 757 805 806 807 808 614 627 612 596 767 424 422 303
+767 765 303 414 626 303 400 414 622 623 378 602 761 367 464 366 761 621 366 464 562 553 557 613 758 801 209 532 764 617 612 760
+620 715 532 432 767 304 303 765 622 627 596 378 767 617 761 561 764 628 352 617 760 614 607 803 764 628 765 755 767 628 561 304
+633 636 78 210 285 334 286 333 692 272 215 73 285 695 636 271 285 334 271 287 695 285 334 271 692 334 272 73 640 267 240 217
+696 266 272 689 695 272 334 333 696 690 267 266 739 684 226 704 632 214 210 213 634 409 226 684 701 704 211 225 633 271 78 636
+634 635 636 214 518 223 220 222 530 224 223 212 530 260 223 224 642 260 228 261 632 633 210 636 634 632 214 636 632 227 226 213
+634 632 227 226 634 635 214 409 673 240 265 238 693 692 272 215 696 693 692 272 787 649 788 658 649 658 659 216 651 250 247 790
+786 249 250 790 673 265 241 239 640 693 267 217 728 262 265 218 697 640 693 267 697 639 218 690 472 253 244 252 475 677 253 470
+678 652 219 264 676 678 652 654 703 518 220 682 703 681 220 643 518 220 221 222 518 530 223 222 703 518 530 223 735 474 337 736
+263 234 235 233 735 337 673 736 734 675 234 241 641 228 229 261 642 530 224 260 703 800 530 518 701 704 225 269 708 268 704 269
+701 227 211 226 708 684 704 268 739 701 226 227 641 642 228 261 796 524 261 232 666 232 229 231 666 641 229 232 670 259 231 230
+796 258 232 259 796 524 232 258 670 259 230 256 668 667 259 256 670 666 231 232 796 670 232 666 452 732 235 234 263 736 241 233
+734 263 736 241 734 452 263 234 734 447 452 454 452 263 234 235 734 452 732 454 734 448 447 305 447 452 454 236 734 453 305 454
+734 458 252 305 448 447 305 237 673 640 240 238 673 736 239 241 697 673 640 240 728 673 241 736 734 471 451 448 449 448 305 242
+472 678 243 264 472 449 252 244 475 472 264 678 475 472 449 252 787 660 788 649 787 655 658 809 787 655 659 658 787 792 809 658
+649 788 658 245 786 660 651 788 650 652 790 246 653 282 790 652 651 650 790 247 786 651 250 789 786 651 650 790 651 788 248 789
+787 512 649 659 787 791 809 792 653 282 249 790 653 281 249 282 786 651 790 250 257 520 668 667 665 803 251 276 475 734 252 449
+475 679 467 472 475 472 252 253 473 468 458 441 475 277 677 254 676 277 255 677 668 257 667 256 656 668 520 706 668 257 655 809
+668 257 256 655 796 667 706 258 796 668 259 670 668 670 256 259 799 795 707 794 796 795 794 666 796 670 259 232 796 671 641 261
+799 711 707 795 796 666 232 641 703 642 260 530 703 709 518 682 671 641 261 642 796 671 261 524 728 336 279 262 728 731 675 279
+734 474 736 263 734 474 263 451 734 728 241 736 676 277 498 255 676 475 264 678 676 678 264 652 475 472 253 264 676 772 679 680
+697 698 640 674 697 638 639 690 826 724 717 698 697 673 240 265 728 697 265 673 696 638 690 266 696 719 692 693 696 697 693 267
+697 640 267 240 720 635 695 634 634 632 636 633 695 694 633 271 708 273 681 686 708 273 685 268 708 268 269 681 739 708 684 704
+708 701 269 704 699 409 684 631 699 409 630 634 634 700 632 718 634 632 226 214 739 634 226 684 696 638 266 270 723 275 718 694
+634 694 718 633 695 694 271 334 723 275 694 719 334 272 695 692 696 695 272 692 720 630 485 635 738 432 714 624 738 432 477 811
+739 743 683 744 738 532 432 811 708 273 688 685 737 769 707 328 737 702 743 328 737 711 328 707 737 769 328 743 671 669 525 310
+703 711 800 709 721 367 619 714 826 674 698 697 721 714 619 373 723 717 274 275 723 717 275 719 721 367 712 619 735 338 674 337
+657 511 787 668 665 803 276 663 775 444 467 679 676 277 466 498 778 278 772 680 778 777 772 278 728 336 368 279 735 725 337 474
+735 338 724 674 735 725 779 338 473 468 441 280 773 464 463 428 775 471 467 306 799 308 798 794 740 209 811 558 656 710 706 504
+668 510 511 798 656 520 521 329 797 510 798 308 735 726 725 474 734 731 55 675 775 306 725 726 776 513 309 779 787 661 660 512
+653 281 282 498 668 657 511 510 778 777 657 510 778 657 278 661 776 513 772 777 740 801 532 209 797 770 777 510 723 742 274 717
+826 742 717 724 738 715 714 432 826 780 781 742 735 780 338 779 738 715 532 705 768 769 743 307 768 771 307 781 768 354 771 781
+768 770 771 354 705 801 621 366 776 309 354 781 705 621 464 366 283 315 316 58 607 389 406 283 607 283 315 316 389 283 284 59
+607 389 283 284 587 389 62 376 389 62 60 407 636 695 285 637 285 334 287 286 285 287 288 286 285 332 289 288 695 285 332 289
+695 291 387 290 695 291 637 385 291 332 76 289 291 387 290 77 695 291 332 637 359 292 386 83 79 387 489 486 489 359 387 79
+386 292 359 79 496 293 497 647 645 496 497 647 496 296 294 295 645 496 295 296 812 529 517 526 812 529 297 298 529 297 298 299
+559 325 326 554 759 544 300 539 759 544 560 300 745 301 313 748 614 753 596 612 581 301 577 747 804 477 811 757 759 402 822 302
+804 414 415 402 805 757 808 756 759 402 302 755 620 414 767 625 415 402 629 822 764 553 613 352 764 553 755 560 767 424 623 421
+761 715 367 366 767 424 421 422 767 424 303 304 734 449 448 305 734 471 726 451 734 449 305 252 458 252 475 734 775 471 306 726
+737 707 308 799 768 769 307 771 797 770 510 308 797 770 308 769 776 513 354 309 775 513 444 772 774 621 70 464 664 656 710 740
+812 808 480 807 687 688 515 478 699 479 478 311 797 770 354 777 671 310 525 517 710 500 504 523 739 307 742 768 768 307 742 781
+699 479 311 488 705 715 801 366 656 520 329 330 656 521 519 329 796 794 798 670 787 657 661 512 787 659 511 512 668 798 312 670
+665 251 606 276 668 511 659 312 746 349 745 562 746 135 124 129 745 748 583 577 745 748 752 749 746 133 129 134 745 301 753 313
+745 613 752 313 745 750 582 583 746 585 314 191 746 191 125 584 607 315 746 316 581 577 583 317 746 580 134 66 581 747 577 579
+746 315 66 316 581 753 747 589 581 577 317 578 565 318 30 319 565 372 426 361 565 360 321 426 417 716 372 361 202 200 417 410
+417 410 200 412 571 412 411 564 565 321 575 319 571 193 159 576 626 422 410 766 571 411 320 175 565 566 321 322 571 564 569 416
+571 568 159 172 626 400 416 567 571 323 322 574 543 548 535 552 821 551 825 415 559 543 548 326 543 544 539 550 559 615 562 324
+559 544 325 327 821 495 551 540 543 540 495 539 804 402 755 765 559 543 326 550 559 557 553 327 764 628 755 352 796 706 710 669
+799 711 795 800 737 711 709 328 710 500 501 502 664 330 522 523 656 329 522 330 710 500 523 509 812 525 517 671 671 310 517 527
+723 717 719 698 387 695 489 385 713 373 372 425 695 291 385 387 696 694 692 719 634 695 633 636 720 331 87 490 695 291 289 332
+713 425 372 360 695 633 636 271 489 331 490 486 695 285 289 333 695 285 333 334 695 694 334 692 335 368 728 336 735 725 338 337
+734 726 474 451 705 464 367 366 730 436 443 437 445 727 380 379 431 727 34 36 775 444 779 306 734 441 453 443 776 309 780 779
+735 780 724 338 723 739 720 634 358 270 696 695 696 697 638 722 826 722 431 697 431 722 35 363 721 826 431 773 430 722 31 339
+722 361 35 339 763 591 364 340 591 364 340 341 591 341 342 383 599 604 205 603 599 598 597 205 591 374 383 52 600 598 145 597
+345 139 594 44 600 343 590 594 599 204 604 421 591 43 435 52 144 344 141 345 592 374 435 346 600 598 601 347 610 208 746 124
+208 350 610 802 745 348 349 751 745 349 746 751 802 611 350 610 562 749 161 348 562 556 192 161 559 327 556 557 802 611 615 350
+764 559 759 615 615 351 324 546 745 613 313 612 613 612 764 352 446 459 465 473 446 433 353 434 776 513 777 354 469 433 434 355
+469 355 284 456 662 461 356 456 662 357 461 646 358 696 430 695 489 490 384 486 489 695 387 430 430 722 691 31 430 695 290 358
+489 79 486 386 430 387 359 290 696 722 638 691 489 386 322 321 430 360 426 321 713 372 361 426 721 431 722 427 773 445 365 428
+721 705 773 367 431 722 427 362 431 722 363 37 431 727 368 34 431 639 722 37 431 427 26 362 431 727 364 365 365 727 773 431
+705 715 366 367 776 309 781 780 731 368 728 727 431 365 364 26 417 362 369 26 763 26 427 417 763 417 420 369 417 361 370 362
+417 203 202 605 418 202 410 9 417 565 371 29 201 371 417 565 200 411 565 412 152 417 604 418 599 763 605 420 417 565 361 372
+626 417 565 412 425 372 762 373 767 303 422 766 374 419 592 591 763 592 428 419 592 591 435 374 587 592 375 64 587 593 376 392
+592 346 435 375 622 394 601 377 622 378 588 377 761 763 592 464 622 614 592 587 622 627 378 623 446 445 346 435 445 727 365 364
+773 727 365 445 445 440 52 435 730 445 379 727 445 374 383 341 364 727 380 445 380 727 364 36 730 48 381 442 730 436 382 48
+445 383 442 380 445 374 52 383 491 487 484 490 720 488 487 87 489 490 484 384 489 384 574 322 387 385 489 486 720 489 695 385
+720 489 430 695 713 489 360 430 489 384 322 386 489 430 321 360 489 430 83 321 387 359 489 430 489 83 359 386 720 713 430 489
+587 64 463 592 587 389 388 406 587 389 407 62 587 408 607 390 587 588 589 391 587 64 375 62 587 592 392 375 592 392 593 587
+587 588 391 393 593 392 592 343 614 753 589 596 622 377 393 394 481 99 625 86 415 819 817 395 822 396 415 821 413 567 818 629
+804 805 756 757 396 415 821 397 620 804 414 625 415 398 395 399 626 400 567 414 415 398 401 92 551 415 756 401 415 823 92 825
+821 551 759 540 821 759 822 540 804 402 765 414 812 672 515 687 812 480 494 538 812 514 403 404 812 526 405 297 812 687 808 741
+813 812 808 741 812 404 494 493 812 687 493 756 812 106 555 538 812 529 298 516 407 586 607 587 614 607 663 586 607 587 390 406
+614 589 587 588 581 747 408 589 607 587 389 407 607 581 408 589 607 389 284 407 607 283 316 284 607 746 67 316 606 67 607 746
+761 617 614 627 739 708 738 699 634 699 409 684 708 672 688 273 671 812 741 672 737 328 709 702 491 687 481 399 491 687 399 483
+491 311 478 483 491 487 311 482 626 417 412 410 411 412 571 565 625 565 425 206 626 422 417 410 413 571 626 625 414 413 626 625
+413 571 573 816 571 567 416 816 415 481 624 477 625 415 395 99 413 625 415 817 762 625 565 425 571 625 570 206 571 817 195 572
+571 625 565 626 629 413 414 415 413 625 414 415 625 572 570 99 571 626 416 567 763 592 464 428 763 417 427 619 762 763 619 417
+599 622 763 592 421 604 417 418 763 592 419 591 762 417 421 763 421 417 762 422 761 622 592 763 767 424 561 623 599 763 340 591
+763 417 605 420 761 627 623 561 605 604 417 763 419 364 763 591 427 26 763 365 763 365 26 364 761 619 763 712 767 762 626 422
+421 762 767 422 762 417 619 716 417 422 626 762 762 625 423 714 620 762 714 625 767 424 304 561 762 417 716 372 762 417 372 565
+762 625 425 423 762 625 626 565 414 804 620 765 767 628 304 765 373 716 721 713 713 491 206 489 713 206 425 489 720 491 713 489
+696 720 430 695 713 722 361 716 430 426 361 339 361 426 430 713 713 489 425 360 713 430 360 426 473 773 445 727 446 773 463 445
+721 773 431 427 428 445 463 773 463 445 428 429 773 431 427 365 738 713 723 720 705 738 741 532 696 713 722 430 696 720 713 430
+721 705 826 773 728 773 727 431 738 741 811 477 738 714 713 423 737 708 741 738 738 741 532 811 481 477 687 738 708 687 738 699
+705 826 738 721 367 464 705 773 774 705 621 464 532 715 738 432 721 738 715 714 62 64 469 586 446 353 68 434 446 469 433 434
+465 433 446 469 463 346 445 429 463 64 774 446 446 68 435 375 446 445 435 440 730 446 436 440 475 473 465 254 774 473 446 469
+473 730 727 445 730 436 437 382 730 437 450 53 730 443 439 437 730 727 379 438 729 438 727 730 734 439 443 453 473 436 446 71
+730 446 440 445 730 445 442 379 734 441 458 453 440 52 730 445 52 442 445 383 473 734 443 441 775 471 475 467 775 444 306 467
+473 730 445 446 734 448 451 447 734 471 448 449 473 446 445 773 734 447 454 305 734 730 439 450 775 473 727 773 734 727 729 731
+734 447 451 452 734 455 454 733 734 453 454 455 734 439 733 450 734 452 451 263 645 496 119 793 276 65 469 69 507 469 284 456
+507 456 284 316 656 329 508 522 665 662 469 507 645 118 295 499 645 296 295 118 645 129 508 457 507 462 316 66 507 460 462 499
+656 796 797 668 507 134 499 66 508 129 645 507 473 446 469 465 458 475 473 734 465 459 446 353 473 734 441 458 473 446 459 71
+473 734 730 443 446 436 473 730 443 436 473 280 734 473 730 727 775 444 679 772 676 475 466 277 475 774 469 473 662 469 356 466
+662 507 456 469 653 661 660 787 656 645 662 507 662 469 456 356 460 461 507 462 786 792 791 787 645 662 507 460 774 473 773 446
+776 705 773 826 774 446 64 469 774 446 773 463 774 705 464 773 774 469 64 586 774 773 464 463 774 469 586 663 676 774 662 469
+803 621 705 801 665 469 663 276 676 665 662 774 665 507 469 276 676 679 678 680 470 473 254 468 475 473 469 465 676 662 466 469
+471 449 475 467 475 679 472 678 473 468 470 458 475 466 469 676 254 473 470 475 775 475 734 473 775 774 473 773 475 449 471 734
+475 467 449 472 776 775 779 826 775 475 473 774 735 775 826 779 728 773 826 735 776 774 773 705 775 475 772 679 475 471 775 734
+735 474 734 726 775 734 471 726 775 475 679 467 676 775 475 772 687 476 493 401 687 515 404 476 687 741 757 808 687 741 477 757
+687 738 477 741 688 478 699 479 708 672 741 687 812 687 404 493 812 808 756 480 687 478 476 483 491 687 738 481 491 699 478 311
+491 738 624 481 491 687 699 738 720 491 487 699 491 487 482 484 491 311 483 482 491 687 478 699 491 738 423 624 720 491 738 713
+491 489 570 206 311 487 699 488 491 687 483 478 425 206 713 423 491 738 713 423 491 489 490 484 720 491 490 487 695 635 720 485
+634 720 630 699 720 699 488 630 720 695 485 385 720 489 331 490 385 331 489 486 720 699 487 488 720 491 489 490 720 491 699 738
+723 739 826 738 551 492 401 493 805 543 759 551 551 94 493 494 551 415 401 92 495 543 551 540 551 122 494 538 551 536 538 555
+543 759 540 539 645 496 647 103 645 496 296 119 129 134 645 507 786 785 647 249 786 793 792 497 498 646 653 281 645 662 460 646
+645 507 499 460 118 129 645 457 499 134 507 645 710 501 549 555 710 500 509 501 813 806 810 558 811 813 757 741 500 502 710 503
+812 710 555 106 812 525 502 526 606 608 665 507 664 710 504 523 664 508 505 522 664 522 505 506 656 645 507 508 665 774 705 803
+664 656 665 507 664 523 506 509 664 710 523 509 664 547 810 710 811 813 740 558 676 775 774 475 778 668 510 657 797 778 668 510
+787 511 657 512 776 775 772 513 797 737 710 740 768 740 776 705 776 768 705 826 676 778 772 680 676 653 662 778 786 787 645 653
+656 787 645 791 741 812 671 710 708 687 699 688 672 687 688 515 812 672 514 515 812 672 516 514 812 671 516 672 671 517 528 527
+671 529 528 517 703 709 800 518 656 787 809 668 656 645 519 791 656 520 257 521 664 656 507 508 656 665 507 662 656 653 662 645
+656 645 508 519 664 656 508 522 664 656 522 330 776 665 774 705 520 706 668 667 706 504 710 669 664 504 330 523 664 656 504 710
+796 671 524 669 525 669 671 710 812 671 710 525 737 671 741 672 703 671 672 643 796 800 795 641 741 671 737 710 812 517 529 671
+812 517 525 526 671 642 261 260 642 641 703 671 671 672 643 115 671 527 528 531 671 672 115 516 812 516 671 529 703 800 642 530
+703 260 644 223 671 527 531 260 703 530 260 223 740 705 741 532 740 741 811 532 656 665 778 740 656 797 778 668 664 740 609 616
+740 710 810 813 740 803 705 801 532 801 740 705 664 665 616 251 768 705 738 741 543 541 533 534 543 535 179 537 543 550 533 326
+536 551 537 543 537 551 536 538 543 551 552 536 543 759 539 544 543 551 540 759 543 539 495 541 543 551 542 545 542 551 122 94
+122 551 542 543 805 543 552 548 559 543 544 759 551 543 495 545 615 562 324 351 805 807 810 549 615 562 351 611 764 562 613 553
+615 611 546 547 615 546 618 549 559 615 324 618 805 548 552 618 805 615 618 549 559 544 550 325 559 543 550 544 559 759 544 560
+805 543 551 552 764 559 562 553 559 556 325 554 764 559 553 560 805 551 555 552 559 562 557 556 553 557 559 562 758 806 558 615
+759 615 758 764 804 759 755 402 620 761 715 762 767 620 762 761 758 616 558 209 764 759 560 755 764 559 560 759 764 559 615 562
+624 481 415 625 767 628 617 561 761 715 801 620 802 764 615 562 745 802 562 613 753 614 760 612 563 412 571 564 571 626 565 412
+571 569 171 816 571 565 322 566 571 565 566 576 571 626 563 416 626 571 413 567 571 564 568 569 574 565 206 360 571 625 572 570
+571 625 817 572 413 573 571 817 320 411 571 565 206 565 574 571 571 572 323 574 158 565 576 575 576 565 158 571 158 320 565 318
+581 577 578 579 581 579 190 198 581 607 197 315 315 186 746 580 746 315 607 581 581 746 186 584 581 607 406 197 745 581 607 753
+745 581 746 607 390 198 581 406 745 582 581 583 581 607 753 589 581 746 584 191 581 582 745 746 191 582 746 585 614 607 586 587
+622 614 587 588 760 614 753 607 589 753 614 607 587 589 614 607 614 587 586 70 803 663 607 276 587 463 614 592 614 592 463 464
+600 592 590 343 600 598 347 141 594 591 590 600 590 591 594 43 591 590 592 435 599 763 591 592 45 594 591 44 599 622 592 600
+600 394 199 347 622 600 199 593 622 593 592 600 45 345 594 44 599 595 600 591 601 394 622 600 595 45 600 591 622 614 588 596
+761 622 623 627 622 587 393 588 622 614 596 627 623 763 622 761 598 601 599 600 592 593 622 587 593 393 587 156 393 199 593 622
+599 600 592 591 599 600 597 598 599 622 600 601 599 763 421 604 599 622 601 602 623 421 763 761 599 603 597 595 599 591 21 595
+599 622 602 623 603 605 599 604 599 763 604 605 340 763 599 420 802 607 746 606 606 276 802 607 802 607 276 803 802 764 562 613
+802 746 208 608 802 746 608 606 745 802 746 562 758 558 616 615 802 615 616 609 610 350 208 124 802 562 349 746 802 615 609 350
+615 611 802 562 802 562 611 349 614 761 801 760 758 802 764 615 745 760 613 612 760 802 613 764 745 760 607 802 801 761 614 621
+760 802 803 607 758 532 209 811 758 764 760 620 758 802 616 803 758 802 615 616 764 612 613 760 612 617 764 352 614 612 617 760
+614 617 761 760 767 764 617 628 767 760 764 620 805 615 810 806 805 551 480 555 480 756 805 808 804 756 805 551 804 551 759 415
+805 559 759 543 805 615 549 810 805 559 618 615 804 758 806 811 805 552 555 549 805 559 543 548 805 559 548 618 761 762 367 715
+761 614 621 464 761 614 464 592 761 763 464 712 763 619 761 762 761 762 619 367 761 622 614 592 620 761 760 801 617 612 614 627
+803 801 614 621 767 761 763 421 761 622 627 614 763 421 623 599 622 763 623 599 620 762 715 714 620 625 714 624 804 415 624 477
+767 762 625 626 804 620 765 764 617 627 761 561 765 764 767 628 758 620 801 532 801 715 761 366 758 615 759 805 804 758 811 620
+414 629 415 402 761 617 767 760 634 720 635 630 636 695 635 634 630 409 699 631 634 700 227 632 634 718 632 633 634 695 694 633
+695 637 635 485 723 634 695 694 723 634 720 695 739 227 700 683 635 695 636 637 697 722 639 638 722 639 431 697 698 674 826 724
+826 697 696 722 728 335 697 431 697 431 335 639 735 697 674 826 696 719 693 698 723 826 717 698 713 696 721 723 697 698 693 640
+697 674 640 673 735 674 673 337 697 826 728 431 737 796 703 799 703 641 642 800 703 518 223 220 703 681 269 220 703 672 686 643
+708 703 686 681 703 671 643 644 703 671 260 642 644 260 671 531 786 645 647 785 653 645 785 646 653 645 646 662 656 653 778 662
+786 249 647 782 653 778 648 661 787 512 660 649 676 646 653 498 656 787 668 778 786 792 789 783 786 660 650 651 653 650 652 790
+786 651 789 788 653 650 654 652 676 653 654 652 787 668 659 655 656 787 778 653 661 657 787 778 787 778 657 668 787 653 661 778
+656 787 653 645 787 649 658 659 660 661 653 648 787 788 789 658 786 787 653 660 786 791 793 645 776 665 740 778 665 774 469 662
+656 665 662 778 664 656 740 665 665 774 803 663 665 740 616 803 705 803 740 665 665 774 663 469 665 507 276 606 664 665 740 616
+664 665 251 608 664 665 608 507 656 797 796 710 656 706 796 668 796 794 670 666 796 795 666 641 796 668 667 259 706 667 796 668
+796 671 669 710 796 641 232 261 796 668 670 798 737 703 671 672 703 796 641 800 799 737 711 703 703 796 737 671 737 708 703 672
+737 740 741 710 735 697 673 674 734 728 675 241 728 673 265 241 728 697 218 265 675 731 728 734 728 697 336 218 735 775 727 773
+734 775 473 727 728 431 727 368 735 775 773 826 697 335 728 336 676 475 774 469 676 665 778 662 676 665 774 778 778 775 774 676
+676 653 778 648 676 778 775 772 676 653 282 498 676 475 679 772 676 653 648 654 469 466 475 465 646 357 676 662 676 662 357 466
+676 475 277 677 676 653 652 282 676 475 678 679 676 475 677 264 676 680 678 654 708 699 684 685 708 703 681 269 708 703 682 702
+743 702 737 739 739 702 701 683 739 634 684 699 708 702 682 701 685 688 699 479 708 672 273 686 708 703 672 686 708 672 687 688
+708 687 741 738 430 696 358 85 696 695 270 689 696 430 691 85 696 695 689 272 696 697 690 638 723 696 720 713 721 696 713 722
+696 430 722 691 696 697 267 690 696 695 692 694 696 697 698 693 723 696 694 695 723 696 697 698 696 693 272 267 739 826 742 723
+739 634 699 720 739 708 699 684 739 720 738 723 718 700 723 634 739 708 701 702 737 708 702 703 739 634 227 226 739 708 704 701
+768 740 705 741 739 701 704 226 799 794 707 308 656 796 706 710 737 739 708 738 799 737 707 711 768 739 307 743 797 354 768 776
+737 708 672 741 768 739 738 826 737 739 702 708 737 703 709 711 737 703 702 709 737 796 710 671 799 800 703 711 723 713 738 721
+723 721 738 826 431 722 826 721 721 713 714 373 721 773 712 367 713 714 738 721 721 705 715 738 721 713 716 722 721 773 427 712
+721 705 367 715 721 722 716 427 723 696 695 720 723 826 742 717 723 634 694 718 723 739 634 700 723 696 719 694 723 696 698 719
+723 697 826 698 700 744 723 739 723 739 744 742 738 720 739 699 721 826 696 722 696 697 826 723 735 826 780 779 724 780 735 826
+735 775 779 725 728 826 697 735 826 773 728 431 735 728 773 727 735 826 674 724 735 728 673 697 735 775 725 726 734 735 736 474
+735 775 726 734 734 735 727 728 735 728 736 673 729 727 734 730 734 730 450 729 731 727 728 734 734 454 732 733 734 735 775 727
+734 735 728 736 734 263 241 234 797 768 737 740 776 768 826 781 797 768 769 737 768 737 738 739 738 826 705 768 768 737 741 738
+768 737 740 741 768 826 781 742 768 737 739 743 768 737 743 769 742 307 739 744 742 826 739 768 745 802 607 746 745 581 301 577
+745 581 577 583 753 301 581 747 745 748 749 583 745 562 749 752 745 746 750 751 745 562 752 613 349 348 745 562 745 760 753 607
+745 760 802 613 753 760 745 612 804 415 477 754 804 620 624 625 804 415 754 756 804 805 759 551 804 620 811 432 804 764 755 759
+804 620 432 624 804 551 415 756 804 805 757 806 804 764 765 755 767 414 620 765 804 415 414 625 758 805 806 615 804 758 759 805
+767 620 761 760 767 761 623 561 767 761 421 623 767 761 762 763 767 764 765 620 767 620 625 762 767 762 421 763 767 626 414 303
+767 626 766 422 767 626 303 766 617 764 767 760 767 625 414 626 656 797 740 778 656 797 710 740 776 740 768 797 797 737 796 710
+797 778 510 777 797 737 769 308 797 668 798 510 776 797 354 777 770 768 769 797 769 768 770 771 799 796 794 798 799 737 796 797
+776 778 772 775 776 778 777 772 776 775 513 779 776 768 781 354 776 775 773 774 776 665 778 774 774 775 778 776 776 775 826 773
+776 797 778 740 776 797 777 778 768 354 797 770 776 826 779 780 776 826 780 781 776 665 705 740 594 345 45 600 786 787 789 792
+782 783 250 784 783 789 250 784 786 653 249 790 786 645 787 791 786 653 645 785 786 653 785 249 786 645 793 497 786 787 788 789
+786 653 790 650 786 653 650 660 786 787 660 788 786 791 792 793 799 796 795 794 308 707 737 769 799 796 800 795 799 797 798 308
+796 797 798 799 798 797 796 668 796 800 703 799 641 796 703 671 408 581 607 390 758 760 764 802 758 558 806 811 804 758 620 764
+758 620 760 801 758 760 803 801 758 760 802 803 804 759 758 764 804 758 805 806 812 710 807 555 813 812 807 808 656 668 809 257
+656 520 668 257 656 787 791 809 813 812 741 710 810 710 740 664 810 807 710 549 757 813 811 806 740 813 811 741 813 812 710 807
+664 740 810 609 810 558 740 813 413 194 814 815 413 816 573 815 413 571 816 567 413 625 817 571 413 818 396 629 413 818 814 396
+413 415 819 817 413 415 397 819 402 759 415 804 415 759 402 822 821 396 397 820 415 413 396 629 821 415 822 759 821 415 823 397
+821 196 820 824 821 495 824 545 821 551 415 759 821 551 545 825 821 415 825 823 696 826 721 723
+ 3 6 9 12 15 18 21 24
+27 30 33 36 39 42 45 48
+51 54 57 60 63 66 69 72
+75 78 81 84 87 90 93 96
+99 102 105 108 111 114 117 120
+123 126 129 132 135 138 141 144
+147 150 153 156 159 162 165 168
+171 174 177 180 183 186 189 192
+195 198 201 204 207 210 213 216
+219 222 225 228 231 234 237 240
+243 246 249 252 255 258 261 264
+267 270 273 276 279 282 285 288
+291 294 297 300 303 306 309 312
+315 318 321 324 327 330 333 336
+339 342 345 348 351 354 357 360
+363 366 369 372 375 378 381 384
+387 390 393 396 399 402 405 408
+411 414 417 420 423 426 429 432
+435 438 441 444 447 450 453 456
+459 462 465 468 471 474 477 480
+483 486 489 492 495 498 501 504
+507 510 513 516 519 522 525 528
+531 534 537 540 543 546 549 552
+555 558 561 564 567 570 573 576
+579 582 585 588 591 594 597 600
+603 606 609 612 615 618 621 624
+627 630 633 636 639 642 645 648
+651 654 657 660 663 666 669 672
+675 678 681 684 687 690 693 696
+699 702 705 708 711 714 717 720
+723 726 729 732 735 738 741 744
+747 750 753 756 759 762 765 768
+771 774 777 780 783 786 789 792
+795 798 801 804 807 810 813 816
+819 822 825 828 831 834 837 840
+843 846 849 852 855 858 861 864
+867 870 873 876 879 882 885 888
+891 894 897 900 903 906 909 912
+915 918 921 924 927 930 933 936
+939 942 945 948 951 954 957 960
+963 966 969 972 975 978 981 984
+987 990 993 996 999 1002 1005 1008
+1011 1014 1017 1020 1023 1026 1029 1032
+1035 1038 1041 1044 1047 1050 1053 1056
+1059 1062 1065 1068 1071 1074 1077 1080
+1083 1086 1089 1092 1095 1098 1101 1104
+1107 1110 1113 1116 1119 1122 1125 1128
+1131 1134 1137 1140 1143 1146 1149 1152
+1155 1158 1161 1164 1167 1170 1173 1176
+1179 1182 1185 1188 1191 1194 1197 1200
+1203 1206 1209 1212 1215 1218 1221 1224
+1227 1230 1233 1236 1239 1242 1245 1248
+1251 1254 1257 1260 1263 1266 1269 1272
+1275 1278 1281 1284 1287 1290 1293 1296
+1299 1302 1305 1308 1311 1314 1317 1320
+1323 1326 1329 1332 1335 1338 1341 1344
+1347 1350 1353 1356 1359 1362 1365 1368
+1371 1374 1377 1380 1383 1386 1389 1392
+1395 1398 1401 1404 1407 1410 1413 1416
+1419 1422 1425 1428 1431 1434 1437 1440
+1443 1446 1449 1452 1455 1458 1461 1464
+1467 1470 1473 1476 1479 1482 1485 1488
+1491 1494 1497 1500 1503 1506 1509 1512
+1515 1518 1521 1524 1527 1530 1533 1536
+1539 1542 1545 1548 1551 1554 1557 1560
+1563 1566 1569 1572 1575 1578 1581 1584
+1587 1590 1593 1596 1599 1602 1605 1608
+1611 1614 1617 1620 1623 1626 1629 1632
+1635 1638 1641 1644 1647 1650 1653 1656
+1659 1662 1665 1668 1671 1674 1677 1680
+1683 1686 1689 1692 1695 1698 1701 1704
+1707 1710 1713 1716 1719 1722 1725 1728
+1731 1734 1737 1740 1743 1746 1749 1752
+1755 1758 1761 1764 1767 1770 1773 1776
+1779 1782 1785 1788 1791 1794 1797 1800
+1803 1806 1809 1812 1815 1818 1821 1824
+1827 1830 1833 1836 1839 1842 1845 1848
+1851 1854 1857 1860 1863 1866 1869 1872
+1875 1878 1881 1884 1887 1890 1893 1896
+1899 1902 1905 1908 1911 1914 1917 1920
+1923 1926 1929 1932 1935 1938 1941 1944
+1947 1950 1953 1956 1959 1962 1965 1968
+1971 1974 1977 1980 1983 1986 1989 1992
+1995 1998 2001 2004 2007 2010 2013 2016
+2019 2022 2025 2028 2031 2034 2037 2040
+2043 2046 2049 2052 2055 2058 2061 2064
+2067 2070 2073 2076 2079 2082 2085 2088
+2091 2094 2097 2100 2103 2106 2109 2112
+2115 2118 2121 2124 2127 2130 2133 2136
+2139 2142 2145 2148 2151 2154 2157 2160
+2163 2166 2169 2172 2175 2178 2181 2184
+2187 2190 2193 2196 2199 2202 2205 2208
+2211 2214 2217 2220 2223 2226 2229 2232
+2235 2238 2241 2244 2247 2250 2253 2256
+2259 2262 2265 2268 2271 2274 2277 2280
+2283 2286 2289 2292 2295 2298 2301 2304
+2307 2310 2313 2316 2319 2322 2325 2328
+2331 2334 2337 2340 2343 2346 2349 2352
+2355 2358 2361 2364 2367 2370 2373 2376
+2379 2382 2385 2388 2391 2394 2397 2400
+2403 2406 2409 2412 2415 2418 2421 2424
+2427 2430 2433 2436 2439 2442 2445 2448
+2451 2454 2457 2460 2463 2466 2469 2472
+2475 2478 2481 2484 2487 2490 2493 2496
+2499 2502 2505 2508 2511 2514 2517 2520
+2523 2526 2529 2532 2535 2538 2541 2544
+2547 2550 2553 2556 2559 2562 2565 2568
+2571 2574 2577 2580 2583 2586 2589 2592
+2595 2598 2601 2604 2607 2610 2613 2616
+2619 2622 2625 2628 2631 2634 2637 2640
+2643 2646 2649 2652 2655 2658 2661 2664
+2667 2670 2673 2676 2679 2682 2685 2688
+2691 2694 2697 2700 2703 2706 2709 2712
+2715 2718 2721 2724 2727 2730 2733 2736
+2739 2742 2745 2748 2751 2754 2757 2760
+2763 2766 2769 2772 2775 2778 2781 2784
+2787 2790 2793 2796 2799 2802 2805 2808
+2811 2814 2817 2820 2823 2826 2829 2832
+2835 2838 2841 2844 2847 2850 2853 2856
+2859 2862 2865 2868 2871 2874 2877 2880
+2883 2886 2889 2892 2895 2898 2901 2904
+2907 2910 2913 2916 2919 2922 2925 2928
+2931 2934 2937 2940 2943 2946 2949 2952
+2955 2958 2961 2964 2967 2970 2973 2976
+2979 2982 2985 2988 2991 2994 2997 3000
+3003 3006 3009 3012 3015 3018 3021 3024
+3027 3030 3033 3036 3039 3042 3045 3048
+3051 3054 3057 3060 3063 3066 3069 3072
+3075 3078 3081 3084 3087 3090 3093 3096
+3099 3102 3105 3108 3111 3114 3117 3120
+3123 3126 3129 3132 3135 3138 3141 3144
+3147 3150 3153 3156 3159 3162 3165 3168
+3171 3174 3177 3180 3183 3186 3189 3192
+3195 3198 3201 3204 3207 3210 3213 3216
+3219 3222 3225 3228 3231 3234 3237 3240
+3243 3246 3249 3252 3255 3258 3261 3264
+3267 3270 3273 3276 3279 3282 3285 3288
+3291 3294 3297 3300 3303 3306 3309 3312
+3315 3318 3321 3324 3327 3330 3333 3336
+3339 3342 3345 3348 3351 3354 3357 3360
+3363 3366 3369 3372 3375 3378 3381 3384
+3387 3390 3393 3396 3399 3402 3405 3408
+3411 3414 3417 3420 3423 3426 3429 3432
+3435 3438 3441 3444 3447 3450 3453 3456
+3459 3462 3465 3468 3471 3474 3477 3480
+3483 3486 3489 3492 3495 3498 3501 3504
+3507 3510 3513 3516 3519 3522 3525 3528
+3531 3534 3537 3540 3543 3546 3549 3552
+3555 3558 3561 3564 3567 3570 3573 3576
+3579 3582 3585 3588 3591 3594 3597 3600
+3603 3606 3609 3612 3615 3618 3621 3624
+3627 3630 3633 3636 3639 3642 3645 3648
+3651 3654 3657 3660 3663 3666 3669 3672
+3675 3678 3681 3684 3687 3690 3693 3696
+3699 3702 3705 3708 3711 3714 3717 3720
+3723 3726 3729 3732 3735 3738 3741 3744
+3747 3750 3753 3756 3759 3762 3765 3768
+3771 3774 3777 3780 3783 3786 3789 3792
+3795 3798 3801 3804 3807 3810 3813 3816
+3819 3822 3825 3828 3831 3834 3837 3840
+3843 3846 3849 3852 3855 3858 3861 3864
+3867 3870 3873 3876 3879 3882 3885 3888
+3891 3894 3897 3900 3903 3906 3909 3912
+3915 3918 3921 3924 3927 3930 3933 3936
+3939 3942 3945 3948 3951 3954 3957 3960
+3963 3966 3969 3972 3975 3978 3981 3984
+3987 3990 3993 3996 3999 4002 4005 4008
+4011 4014 4017 4020 4023 4026 4029 4032
+4035 4038 4041 4044 4047 4050 4053 4056
+4059 4062 4065 4068 4071 4074 4077 4080
+4083 4086 4089 4092 4095 4098 4101 4104
+4107 4110 4113 4116 4119 4122 4125 4128
+4131 4134 4137 4140 4143 4146 4149 4152
+4155 4158 4161 4164 4167 4170 4173 4176
+4179 4182 4185 4188 4191 4194 4197 4200
+4203 4206 4209 4212 4215 4218 4221 4224
+4227 4230 4233 4236 4239 4242 4245 4248
+4251 4254 4257 4260 4263 4266 4269 4272
+4275 4278 4281 4284 4287 4290 4293 4296
+4299 4302 4305 4308 4311 4314 4317 4320
+4323 4326 4329 4332 4335 4338 4341 4344
+4347 4350 4353 4356 4359 4362 4365 4368
+4371 4374 4377 4380 4383 4386 4389 4392
+4395 4398 4401 4404 4407 4410 4413 4416
+4419 4422 4425 4428 4431 4434 4437 4440
+4443 4446 4449 4452 4455 4458 4461 4464
+4467 4470 4473 4476 4479 4482 4485 4488
+4491 4494 4497 4500 4503 4506 4509 4512
+4515 4518 4521 4524 4527 4530 4533 4536
+4539 4542 4545 4548 4551 4554 4557 4560
+4563 4566 4569 4572 4575 4578 4581 4584
+4587 4590 4593 4596 4599 4602 4605 4608
+4611 4614 4617 4620 4623 4626 4629 4632
+4635 4638 4641 4644 4647 4650 4653 4656
+4659 4662 4665 4668 4671 4674 4677 4680
+4683 4686 4689 4692 4695 4698 4701 4704
+4707 4710 4713 4716 4719 4722 4725 4728
+4731 4734 4737 4740 4743 4746 4749 4752
+4755 4758 4761 4764 4767 4770 4773 4776
+4779 4782 4785 4788 4791 4794 4797 4800
+4803 4806 4809 4812 4815 4818 4821 4824
+4827 4830 4833 4836 4839 4842 4845 4848
+4851 4854 4857 4860 4863 4866 4869 4872
+4875 4878 4881 4884 4887 4890 4893 4896
+4899 4902 4905 4908 4911 4914 4917 4920
+4923 4926 4929 4932 4935 4938 4941 4944
+4947 4950 4953 4956 4959 4962 4965 4968
+4971 4974 4977 4980 4983 4986 4989 4992
+4995 4998 5001 5004 5007 5010 5013 5016
+5019 5022 5025 5028 5031 5034 5037 5040
+5043 5046 5049 5052 5055 5058 5061 5064
+5067 5070 5073 5076 5079 5082 5085 5088
+5091 5094 5097 5100 5103 5106 5109 5112
+5115 5118 5121 5124 5127 5130 5133 5136
+5140 5144 5148 5152 5156 5160 5164 5168
+5172 5176 5180 5184 5188 5192 5196 5200
+5204 5208 5212 5216 5220 5224 5228 5232
+5236 5240 5244 5248 5252 5256 5260 5264
+5268 5272 5276 5280 5284 5288 5292 5296
+5300 5304 5308 5312 5316 5320 5324 5328
+5332 5336 5340 5344 5348 5352 5356 5360
+5364 5368 5372 5376 5380 5384 5388 5392
+5396 5400 5404 5408 5412 5416 5420 5424
+5428 5432 5436 5440 5444 5448 5452 5456
+5460 5464 5468 5472 5476 5480 5484 5488
+5492 5496 5500 5504 5508 5512 5516 5520
+5524 5528 5532 5536 5540 5544 5548 5552
+5556 5560 5564 5568 5572 5576 5580 5584
+5588 5592 5596 5600 5604 5608 5612 5616
+5620 5624 5628 5632 5636 5640 5644 5648
+5652 5656 5660 5664 5668 5672 5676 5680
+5684 5688 5692 5696 5700 5704 5708 5712
+5716 5720 5724 5728 5732 5736 5740 5744
+5748 5752 5756 5760 5764 5768 5772 5776
+5780 5784 5788 5792 5796 5800 5804 5808
+5812 5816 5820 5824 5828 5832 5836 5840
+5844 5848 5852 5856 5860 5864 5868 5872
+5876 5880 5884 5888 5892 5896 5900 5904
+5908 5912 5916 5920 5924 5928 5932 5936
+5940 5944 5948 5952 5956 5960 5964 5968
+5972 5976 5980 5984 5988 5992 5996 6000
+6004 6008 6012 6016 6020 6024 6028 6032
+6036 6040 6044 6048 6052 6056 6060 6064
+6068 6072 6076 6080 6084 6088 6092 6096
+6100 6104 6108 6112 6116 6120 6124 6128
+6132 6136 6140 6144 6148 6152 6156 6160
+6164 6168 6172 6176 6180 6184 6188 6192
+6196 6200 6204 6208 6212 6216 6220 6224
+6228 6232 6236 6240 6244 6248 6252 6256
+6260 6264 6268 6272 6276 6280 6284 6288
+6292 6296 6300 6304 6308 6312 6316 6320
+6324 6328 6332 6336 6340 6344 6348 6352
+6356 6360 6364 6368 6372 6376 6380 6384
+6388 6392 6396 6400 6404 6408 6412 6416
+6420 6424 6428 6432 6436 6440 6444 6448
+6452 6456 6460 6464 6468 6472 6476 6480
+6484 6488 6492 6496 6500 6504 6508 6512
+6516 6520 6524 6528 6532 6536 6540 6544
+6548 6552 6556 6560 6564 6568 6572 6576
+6580 6584 6588 6592 6596 6600 6604 6608
+6612 6616 6620 6624 6628 6632 6636 6640
+6644 6648 6652 6656 6660 6664 6668 6672
+6676 6680 6684 6688 6692 6696 6700 6704
+6708 6712 6716 6720 6724 6728 6732 6736
+6740 6744 6748 6752 6756 6760 6764 6768
+6772 6776 6780 6784 6788 6792 6796 6800
+6804 6808 6812 6816 6820 6824 6828 6832
+6836 6840 6844 6848 6852 6856 6860 6864
+6868 6872 6876 6880 6884 6888 6892 6896
+6900 6904 6908 6912 6916 6920 6924 6928
+6932 6936 6940 6944 6948 6952 6956 6960
+6964 6968 6972 6976 6980 6984 6988 6992
+6996 7000 7004 7008 7012 7016 7020 7024
+7028 7032 7036 7040 7044 7048 7052 7056
+7060 7064 7068 7072 7076 7080 7084 7088
+7092 7096 7100 7104 7108 7112 7116 7120
+7124 7128 7132 7136 7140 7144 7148 7152
+7156 7160 7164 7168 7172 7176 7180 7184
+7188 7192 7196 7200 7204 7208 7212 7216
+7220 7224 7228 7232 7236 7240 7244 7248
+7252 7256 7260 7264 7268 7272 7276 7280
+7284 7288 7292 7296 7300 7304 7308 7312
+7316 7320 7324 7328 7332 7336 7340 7344
+7348 7352 7356 7360 7364 7368 7372 7376
+7380 7384 7388 7392 7396 7400 7404 7408
+7412 7416 7420 7424 7428 7432 7436 7440
+7444 7448 7452 7456 7460 7464 7468 7472
+7476 7480 7484 7488 7492 7496 7500 7504
+7508 7512 7516 7520 7524 7528 7532 7536
+7540 7544 7548 7552 7556 7560 7564 7568
+7572 7576 7580 7584 7588 7592 7596 7600
+7604 7608 7612 7616 7620 7624 7628 7632
+7636 7640 7644 7648 7652 7656 7660 7664
+7668 7672 7676 7680 7684 7688 7692 7696
+7700 7704 7708 7712 7716 7720 7724 7728
+7732 7736 7740 7744 7748 7752 7756 7760
+7764 7768 7772 7776 7780 7784 7788 7792
+7796 7800 7804 7808 7812 7816 7820 7824
+7828 7832 7836 7840 7844 7848 7852 7856
+7860 7864 7868 7872 7876 7880 7884 7888
+7892 7896 7900 7904 7908 7912 7916 7920
+7924 7928 7932 7936 7940 7944 7948 7952
+7956 7960 7964 7968 7972 7976 7980 7984
+7988 7992 7996 8000 8004 8008 8012 8016
+8020 8024 8028 8032 8036 8040 8044 8048
+8052 8056 8060 8064 8068 8072 8076 8080
+8084 8088 8092 8096 8100 8104 8108 8112
+8116 8120 8124 8128 8132 8136 8140 8144
+8148 8152 8156 8160 8164 8168 8172 8176
+8180 8184 8188 8192 8196 8200 8204 8208
+8212 8216 8220 8224 8228 8232 8236 8240
+8244 8248 8252 8256 8260 8264 8268 8272
+8276 8280 8284 8288 8292 8296 8300 8304
+8308 8312 8316 8320 8324 8328 8332 8336
+8340 8344 8348 8352 8356 8360 8364 8368
+8372 8376 8380 8384 8388 8392 8396 8400
+8404 8408 8412 8416 8420 8424 8428 8432
+8436 8440 8444 8448 8452 8456 8460 8464
+8468 8472 8476 8480 8484 8488 8492 8496
+8500 8504 8508 8512 8516 8520 8524 8528
+8532 8536 8540 8544 8548 8552 8556 8560
+8564 8568 8572 8576 8580 8584 8588 8592
+8596 8600 8604 8608 8612 8616 8620 8624
+8628 8632 8636 8640 8644 8648 8652 8656
+8660 8664 8668 8672 8676 8680 8684 8688
+8692 8696 8700 8704 8708 8712 8716 8720
+8724 8728 8732 8736 8740 8744 8748 8752
+8756 8760 8764 8768 8772 8776 8780 8784
+8788 8792 8796 8800 8804 8808 8812 8816
+8820 8824 8828 8832 8836 8840 8844 8848
+8852 8856 8860 8864 8868 8872 8876 8880
+8884 8888 8892 8896 8900 8904 8908 8912
+8916 8920 8924 8928 8932 8936 8940 8944
+8948 8952 8956 8960 8964 8968 8972 8976
+8980 8984 8988 8992 8996 9000 9004 9008
+9012 9016 9020 9024 9028 9032 9036 9040
+9044 9048 9052 9056 9060 9064 9068 9072
+9076 9080 9084 9088 9092 9096 9100 9104
+9108 9112 9116 9120 9124 9128 9132 9136
+9140 9144 9148 9152 9156 9160 9164 9168
+9172 9176 9180 9184 9188 9192 9196 9200
+9204 9208 9212 9216 9220 9224 9228 9232
+9236 9240 9244 9248 9252 9256 9260 9264
+9268 9272 9276 9280 9284 9288 9292 9296
+9300 9304 9308 9312 9316 9320 9324 9328
+9332 9336 9340 9344 9348 9352 9356 9360
+9364 9368 9372 9376 9380 9384 9388 9392
+9396 9400 9404 9408 9412 9416 9420 9424
+9428 9432 9436 9440 9444 9448 9452 9456
+9460 9464 9468 9472 9476 9480 9484 9488
+9492 9496 9500 9504 9508 9512 9516 9520
+9524 9528 9532 9536 9540 9544 9548 9552
+9556 9560 9564 9568 9572 9576 9580 9584
+9588 9592 9596 9600 9604 9608 9612 9616
+9620 9624 9628 9632 9636 9640 9644 9648
+9652 9656 9660 9664 9668 9672 9676 9680
+9684 9688 9692 9696 9700 9704 9708 9712
+9716 9720 9724 9728 9732 9736 9740 9744
+9748 9752 9756 9760 9764 9768 9772 9776
+9780 9784 9788 9792 9796 9800 9804 9808
+9812 9816 9820 9824 9828 9832 9836 9840
+9844 9848 9852 9856 9860 9864 9868 9872
+9876 9880 9884 9888 9892 9896 9900 9904
+9908 9912 9916 9920 9924 9928 9932 9936
+9940 9944 9948 9952 9956 9960 9964 9968
+9972 9976 9980 9984 9988 9992 9996 10000
+10004 10008 10012 10016 10020 10024 10028 10032
+10036 10040 10044 10048 10052 10056 10060 10064
+10068 10072 10076 10080 10084 10088 10092 10096
+10100 10104 10108 10112 10116 10120 10124 10128
+10132 10136 10140 10144 10148 10152 10156 10160
+10164 10168 10172 10176 10180 10184 10188 10192
+10196 10200 10204 10208 10212 10216 10220 10224
+10228 10232 10236 10240 10244 10248 10252 10256
+10260 10264 10268 10272 10276 10280 10284 10288
+10292 10296 10300 10304 10308 10312 10316 10320
+10324 10328 10332 10336 10340 10344 10348 10352
+10356 10360 10364 10368 10372 10376 10380 10384
+10388 10392 10396 10400 10404 10408 10412 10416
+10420 10424 10428 10432 10436 10440 10444 10448
+10452 10456 10460 10464 10468 10472 10476 10480
+10484 10488 10492 10496 10500 10504 10508 10512
+10516 10520 10524 10528 10532 10536 10540 10544
+10548 10552 10556 10560 10564 10568 10572 10576
+10580 10584 10588 10592 10596 10600 10604 10608
+10612 10616 10620 10624 10628 10632 10636 10640
+10644 10648 10652 10656 10660 10664 10668 10672
+10676 10680 10684 10688 10692 10696 10700 10704
+10708 10712 10716 10720 10724 10728 10732 10736
+10740 10744 10748 10752 10756 10760 10764 10768
+10772 10776 10780 10784 10788 10792 10796 10800
+10804 10808 10812 10816 10820 10824 10828 10832
+10836 10840 10844 10848 10852 10856 10860 10864
+10868 10872 10876 10880 10884 10888 10892 10896
+10900 10904 10908 10912 10916 10920 10924 10928
+10932 10936 10940 10944 10948 10952 10956 10960
+10964 10968 10972 10976 10980 10984 10988 10992
+10996 11000 11004 11008 11012 11016 11020 11024
+11028 11032 11036 11040 11044 11048 11052 11056
+11060 11064 11068 11072 11076 11080 11084 11088
+11092 11096 11100 11104 11108 11112 11116 11120
+11124 11128 11132 11136 11140 11144 11148 11152
+11156 11160 11164 11168 11172 11176 11180 11184
+11188 11192 11196 11200 11204 11208 11212 11216
+11220 11224 11228 11232 11236 11240 11244 11248
+11252 11256 11260 11264 11268 11272 11276 11280
+11284 11288 11292 11296 11300 11304 11308 11312
+11316 11320 11324 11328 11332 11336 11340 11344
+11348 11352 11356 11360 11364 11368 11372 11376
+11380 11384 11388 11392 11396 11400 11404 11408
+11412 11416 11420 11424 11428 11432 11436 11440
+11444 11448 11452 11456 11460 11464 11468 11472
+11476 11480 11484 11488 11492 11496 11500 11504
+11508 11512 11516 11520 11524 11528 11532 11536
+11540 11544 11548 11552 11556 11560 11564 11568
+11572 11576 11580 11584 11588 11592 11596 11600
+11604 11608 11612 11616 11620 11624 11628 11632
+11636 11640 11644 11648 11652 11656 11660 11664
+11668 11672 11676 11680 11684 11688 11692 11696
+11700 11704 11708 11712 11716 11720 11724 11728
+11732 11736 11740 11744 11748 11752 11756 11760
+11764 11768 11772 11776 11780 11784 11788 11792
+11796 11800 11804 11808 11812 11816 11820 11824
+11828 11832 11836 11840 11844 11848 11852 11856
+11860 11864 11868 11872 11876 11880 11884 11888
+11892 11896 11900 11904 11908 11912 11916 11920
+11924 11928 11932 11936 11940 11944 11948 11952
+11956 11960 11964 11968 11972 11976 11980 11984
+11988 11992 11996 12000 12004 12008 12012 12016
+12020 12024 12028 12032 12036 12040 12044 12048
+12052 12056 12060 12064 12068 12072 12076 12080
+12084 12088 12092 12096 12100 12104 12108 12112
+12116 12120 12124 12128 12132 12136 12140 12144
+12148 12152 12156 12160 12164 12168 12172 12176
+12180 12184 12188 12192 12196 12200 12204 12208
+12212 12216 12220 12224 12228 12232 12236 12240
+12244 12248 12252 12256 12260 12264 12268 12272
+12276 12280 12284 12288 12292 12296 12300 12304
+12308 12312 12316 12320 12324 12328 12332 12336
+12340 12344 12348 12352 12356 12360 12364 12368
+12372 12376 12380 12384 12388 12392 12396 12400
+12404 12408 12412 12416 12420 12424 12428 12432
+12436 12440 12444 12448 12452 12456 12460 12464
+12468 12472 12476 12480 12484 12488 12492 12496
+12500 12504 12508 12512 12516 12520 12524 12528
+12532 12536 12540 12544 12548 12552 12556 12560
+12564 12568 12572 12576 12580 12584 12588 12592
+12596 12600 12604 12608 12612 12616 12620 12624
+12628 12632 12636 12640 12644 12648 12652 12656
+12660 12664 12668 12672 12676 12680 12684 12688
+12692 12696 12700 12704 12708 12712 12716 12720
+12724 12728 12732 12736 12740 12744 12748 12752
+12756 12760 12764 12768 12772 12776 12780 12784
+12788 12792 12796 12800 12804 12808 12812 12816
+12820 12824 12828 12832 12836 12840 12844 12848
+12852 12856 12860 12864 12868 12872 12876 12880
+12884 12888 12892 12896 12900 12904 12908 12912
+12916 12920 12924 12928 12932 12936 12940 12944
+12948 12952 12956 12960 12964 12968 12972 12976
+12980 12984 12988 12992 12996 13000 13004 13008
+13012 13016 13020 13024 13028 13032 13036 13040
+13044 13048 13052 13056 13060 13064 13068 13072
+13076 13080 13084 13088 13092 13096 13100 13104
+13108 13112 13116 13120 13124 13128 13132 13136
+13140 13144 13148 13152 13156 13160 13164 13168
+13172 13176 13180 13184 13188 13192 13196 13200
+13204 13208 13212 13216 13220 13224 13228 13232
+13236 13240 13244 13248 13252 13256 13260 13264
+13268 13272 13276 13280 13284 13288 13292 13296
+13300 13304 13308 13312 13316 13320 13324 13328
+13332 13336 13340 13344 13348 13352 13356 13360
+13364 13368 13372 13376 13380 13384 13388 13392
+13396 13400 13404 13408 13412 13416 13420 13424
+13428 13432 13436 13440 13444 13448 13452 13456
+13460 13464 13468 13472 13476 13480 13484 13488
+13492 13496 13500 13504 13508 13512 13516 13520
+13524 13528 13532 13536 13540 13544 13548 13552
+13556 13560 13564 13568 13572 13576 13580 13584
+13588 13592 13596 13600 13604 13608 13612 13616
+13620 13624 13628 13632 13636 13640 13644 13648
+13652 13656 13660 13664 13668 13672 13676 13680
+13684 13688 13692 13696 13700 13704 13708 13712
+13716 13720 13724 13728 13732 13736 13740 13744
+13748 13752 13756 13760 13764 13768 13772 13776
+13780 13784 13788 13792 13796 13800 13804 13808
+13812 13816 13820 13824 13828 13832 13836 13840
+13844 13848 13852 13856 13860 13864 13868 13872
+13876 13880 13884 13888 13892 13896 13900 13904
+13908 13912 13916 13920 13924 13928 13932 13936
+13940 13944 13948 13952 13956 13960 13964 13968
+13972 13976 13980 13984 13988 13992 13996 14000
+14004 14008 14012 14016 14020 14024 14028 14032
+14036 14040 14044 14048 14052 14056 14060 14064
+14068 14072 14076 14080 14084 14088 14092 14096
+14100 14104 14108 14112 14116 14120 14124 14128
+14132 14136 14140 14144 14148 14152 14156 14160
+14164 14168 14172 14176 14180 14184 14188 14192
+14196 14200 14204 14208 14212 14216 14220 14224
+14228 14232 14236 14240 14244 14248 14252 14256
+14260 14264 14268 14272 14276 14280 14284 14288
+14292 14296 14300 14304 14308 14312 14316 14320
+14324 14328 14332 14336 14340 14344 14348 14352
+14356 14360 14364 14368 14372 14376 14380 14384
+14388 14392 14396 14400 14404 14408 14412 14416
+14420 14424 14428 14432 14436 14440 14444 14448
+14452 14456 14460 14464 14468 14472 14476 14480
+14484 14488 14492 14496 14500 14504 14508 14512
+14516 14520 14524 14528 14532 14536 14540 14544
+14548 14552 14556 14560 14564 14568 14572 14576
+14580 14584 14588 14592 14596 14600 14604 14608
+14612 14616 14620 14624 14628 14632 14636 14640
+14644 14648 14652 14656 14660 14664 14668 14672
+14676 14680 14684 14688 14692 14696 14700 14704
+14708 14712 14716 14720 14724 14728 14732 14736
+14740 14744 14748 14752 14756 14760 14764 14768
+14772 14776 14780 14784 14788 14792 14796 14800
+14804 14808 14812 14816 14820 14824 14828 14832
+14836 14840 14844 14848 14852 14856 14860 14864
+14868 14872 14876 14880 14884 14888 14892 14896
+14900 14904 14908 14912 14916 14920 14924 14928
+14932 14936 14940 14944 14948 14952 14956 14960
+14964 14968 14972 14976 14980 14984 14988 14992
+14996 15000 15004 15008 15012 15016 15020 15024
+15028 15032 15036 15040 15044 15048 15052 15056
+15060 15064 15068 15072 15076 15080 15084 15088
+15092 15096 15100 15104 15108 15112 15116 15120
+15124 15128 15132 15136 15140 15144 15148 15152
+15156 15160 15164 15168 15172 15176 15180 15184
+15188 15192 15196 15200 15204 15208 15212 15216
+15220 15224 15228 15232 15236 15240 15244 15248
+15252 15256 15260 15264 15268 15272 15276 15280
+15284 15288 15292 15296 15300 15304 15308 15312
+15316 15320 15324 15328 15332 15336 15340 15344
+15348 15352 15356 15360 15364 15368 15372 15376
+15380 15384 15388 15392 15396 15400 15404 15408
+15412 15416 15420 15424 15428 15432 15436 15440
+15444 15448 15452 15456 15460 15464 15468 15472
+15476 15480 15484 15488 15492 15496 15500 15504
+15508 15512 15516 15520 15524 15528 15532 15536
+15540 15544 15548 15552 15556 15560 15564 15568
+15572 15576 15580 15584 15588 15592 15596 15600
+15604 15608 15612 15616 15620 15624 15628 15632
+15636 15640 15644 15648 15652 15656 15660 15664
+15668 15672 15676 15680 15684 15688 15692 15696
+15700 15704 15708 15712 15716 15720 15724 15728
+15732 15736 15740 15744 15748 15752 15756 15760
+15764 15768 15772 15776 15780 15784 15788 15792
+15796 15800 15804 15808 15812 15816 15820 15824
+15828 15832 15836 15840 15844 15848 15852 15856
+15860 15864 15868 15872 15876 15880 15884 15888
+15892 15896 15900 15904 15908 15912 15916 15920
+15924 15928 15932 15936 15940 15944 15948 15952
+15956 15960 15964 15968 15972 15976 15980 15984
+15988 15992 15996 16000 16004 16008 16012 16016
+16020 16024 16028 16032 16036 16040 16044 16048
+16052 16056 16060 16064 16068 16072 16076 16080
+16084 16088 16092 16096 16100 16104 16108 16112
+16116 16120 16124 16128 16132 16136 16140 16144
+16148 16152 16156 16160 16164 16168
+ 5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+5 5 5 5 5 5 5 5
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10 10 10
+10 10 10 10 10 10
+
+
+
+
diff --git a/geos-processing/tests/test_TetQualityAnalysis.py b/geos-processing/tests/test_TetQualityAnalysis.py
new file mode 100644
index 00000000..6d79f64d
--- /dev/null
+++ b/geos-processing/tests/test_TetQualityAnalysis.py
@@ -0,0 +1,17 @@
+# SPDX-FileContributor: Paloma Martinez
+# SPDX-License-Identifier: Apache 2.0
+# ruff: noqa: E402 # disable Module level import not at top of file
+# mypy: disable-error-code="operator"
+from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid
+from geos.processing.pre_processing.TetQualityAnalysis import TetQualityAnalysis
+
+
+def test_TetQualityAnalysis( dataSetTest: vtkUnstructuredGrid ) -> None:
+ """Test applying TetQualityAnalysis filter."""
+ meshes: dict[ str, vtkUnstructuredGrid ] = {
+ 'mesh1': dataSetTest( "meshtet1" ),
+ 'mesh1b': dataSetTest( "meshtet1b" )
+ }
+ tetQualityFilter: TetQualityAnalysis = TetQualityAnalysis( meshes )
+
+ tetQualityFilter.applyFilter()
From 7a33895309534eca3ac435c0b9142d0e6a04cd91 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Mon, 2 Feb 2026 15:13:22 +0100
Subject: [PATCH 08/11] little commit to launch ci again
---
geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
index c3ea5660..0f0ab97c 100644
--- a/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
+++ b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
@@ -56,7 +56,7 @@
class PVTetQualityAnalysis( VTKPythonAlgorithmBase ):
def __init__( self: Self ) -> None:
- """QC analysis of the tetrahedras from 2 meshes."""
+ """QC analysis of the tetrahedras from two meshes."""
super().__init__( nInputPorts=2, inputType="vtkObject" )
self._filename: Optional[ str ] = None
From 069a5970b5383208b6a1bec97d830bf3b4959bf0 Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Wed, 4 Feb 2026 14:31:19 +0100
Subject: [PATCH 09/11] Modifs from review comments
---
.../mesh/stats/tetrahedraAnalysisHelpers.py | 22 ++++++++++++-------
.../pre_processing/TetQualityAnalysis.py | 18 ++++++---------
.../tests/test_TetQualityAnalysis.py | 14 ++++++++++++
.../pv/plugins/qc/PVTetQualityAnalysis.py | 2 +-
4 files changed, 36 insertions(+), 20 deletions(-)
diff --git a/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py b/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
index d040e6ad..400e06fe 100644
--- a/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
+++ b/geos-mesh/src/geos/mesh/stats/tetrahedraAnalysisHelpers.py
@@ -16,13 +16,12 @@ def getCoordinatesDoublePrecision( mesh: vtkDataSet ) -> npt.NDArray[ np.float64
Returns:
npt.NDArray[np.float64]: Coordinates
-
"""
points = mesh.GetPoints()
- npoints = points.GetNumberOfPoints()
+ nPoints = points.GetNumberOfPoints()
- coords = np.zeros( ( npoints, 3 ), dtype=np.float64 )
- for i in range( npoints ):
+ coords = np.zeros( ( nPoints, 3 ), dtype=np.float64 )
+ for i in range( nPoints ):
point = points.GetPoint( i )
coords[ i ] = [ point[ 0 ], point[ 1 ], point[ 2 ] ]
@@ -55,12 +54,14 @@ def extractTetConnectivity( mesh: vtkDataSet ) -> tuple[ npt.NDArray[ np.float64
return np.array( tetrahedraIds ), np.array( tetrahedraConnectivity )
-def analyzeAllTets( n: int, coords: npt.NDArray[ np.float64 ],
+def analyzeAllTets( coords: npt.NDArray[ np.float64 ],
connectivity: npt.NDArray[ np.float64 ] ) -> dict[ str, dict[ str, Any ] ]:
"""Vectorized analysis of all tetrahedra.
+ This analysis computes the following metrics: volumes, aspect ratio, radius ratio, flatness ratio,shape quality, min and max edge, min and max dihedral angles, dihedral range.
+
+
Args:
- n (int): Mesh id.
coords (npt.NDArray[np.float64]): Tetrahedra coordinates.
connectivity (npt.NDArray[np.float64]): Connectivity.
@@ -136,7 +137,6 @@ def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
Returns:
npt.NDArray[ np.float64 ]: Dihedral angle
-
"""
n1Norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
n2Norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
@@ -183,7 +183,13 @@ def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
def computeQualityScore( aspectRatio: npt.NDArray[ np.float64 ], shapeQuality: npt.NDArray[ np.float64 ],
edgeRatio: npt.NDArray[ np.float64 ],
minDihedralAngle: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
- """Compute combined quality score (0-100).
+ """Compute combined quality score (0-100) from aspect ratio, shape quality, minimal dihedral angle and edge ratio.
+
+ The quality score can be interpreted with the following scale:
+ Excellent (>80)
+ Good (60-80)
+ Fair (30-60)
+ Poor (≤30)
Args:
aspectRatio(npt.NDArray[np.float64]): Aspect ratio
diff --git a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
index 7203ff2c..baf33973 100644
--- a/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
+++ b/geos-processing/src/geos/processing/pre_processing/TetQualityAnalysis.py
@@ -17,6 +17,9 @@
__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.
@@ -38,17 +41,13 @@
tetQualityAnalysisFilter.setLoggerHandler( yourHandler )
# Change output filename [optional]
- tetQualityAnalysisFilter.SetFilename( filename )
-
+ tetQualityAnalysisFilter.setFilename( filename )
# Do calculations
try:
tetQualityAnalysisFilter.applyFilter()
- except ( ValueError, IndexError, TypeError, AttributeError ) as e:
- tetQualityAnalysisFilter.logger.error( f"The filter { tetQualityAnalysisFilter.logger.name } failed due to: { e }" )
except Exception as e:
- mess: str = f"The filter { meshQualityEnhancedFilter.logger.name } failed due to: { e }"
- tetQualityAnalysisFilter.logger.critical( mess, exc_info=True )
+ tetQualityAnalysisFilter.logger.error( f"The filter { tetQualityAnalysisFilter.logger.name } failed due to: { e }" )
"""
loggerName: str = "Tetrahedra Quality Analysis"
@@ -115,7 +114,7 @@ def applyFilter( self: Self ) -> None:
f" Points: {mesh.GetNumberOfPoints()}" + "\n" + "-" * 80 + "\n" )
# Analyze both meshes
- self.analyzedMesh[ n ] = analyzeAllTets( n, coords, tetrahedraConnectivity )
+ self.analyzedMesh[ n ] = analyzeAllTets( coords, tetrahedraConnectivity )
metrics = self.analyzedMesh[ n ]
self.tets[ n ] = ntets
@@ -501,8 +500,6 @@ def createComparisonDashboard( self: Self ) -> None:
# 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 ) ] )
- # for n, _ in enumerate( self.meshes, 1 ):
- # suptitle += f'Mesh {n}: {self.tets[n]:<15} tets '
fig.suptitle( suptitle, fontsize=16, fontweight='bold', y=0.99 )
# Color scheme
@@ -1141,11 +1138,10 @@ def createComparisonDashboard( self: Self ) -> None:
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}" )
- def setDashboardFilename( self: Self, filename: str ) -> None:
+ def setFilename( self: Self, filename: str ) -> None:
"""Set comparison dashboard output filename.
Args:
diff --git a/geos-processing/tests/test_TetQualityAnalysis.py b/geos-processing/tests/test_TetQualityAnalysis.py
index 6d79f64d..f4346974 100644
--- a/geos-processing/tests/test_TetQualityAnalysis.py
+++ b/geos-processing/tests/test_TetQualityAnalysis.py
@@ -15,3 +15,17 @@ def test_TetQualityAnalysis( dataSetTest: vtkUnstructuredGrid ) -> None:
tetQualityFilter: TetQualityAnalysis = TetQualityAnalysis( meshes )
tetQualityFilter.applyFilter()
+
+
+def test_TetQualityAnalysisRaisePathError( dataSetTest: vtkUnstructuredGrid ) -> None:
+ """Test applying TetQualityAnalysis filter."""
+ meshes: dict[ str, vtkUnstructuredGrid ] = {
+ 'mesh1': dataSetTest( "meshtet1" ),
+ 'mesh1b': dataSetTest( "meshtet1b" )
+ }
+ tetQualityFilter: TetQualityAnalysis = TetQualityAnalysis( meshes )
+
+ tetQualityFilter.setFilename( "/qliuf/moidh/meshComparison.png" )
+
+ with pytest.raises( FileNotFoundError ):
+ tetQualityFilter.applyFilter()
\ No newline at end of file
diff --git a/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
index 0f0ab97c..7955f4a2 100644
--- a/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
+++ b/geos-pv/src/geos/pv/plugins/qc/PVTetQualityAnalysis.py
@@ -80,7 +80,7 @@ def SetFileName( self: Self, fname: str ) -> None:
"""
if self._filename != fname:
self._filename = fname
- self.Modified()
+ self.Modified()
def RequestData(
self: Self,
From 3e4a3c6c20caeaee7d8fffb19e66343b9f6cfacf Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Wed, 4 Feb 2026 14:35:25 +0100
Subject: [PATCH 10/11] Fix mypy issue
---
geos-processing/tests/test_TetQualityAnalysis.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/geos-processing/tests/test_TetQualityAnalysis.py b/geos-processing/tests/test_TetQualityAnalysis.py
index f4346974..810b78b1 100644
--- a/geos-processing/tests/test_TetQualityAnalysis.py
+++ b/geos-processing/tests/test_TetQualityAnalysis.py
@@ -2,6 +2,8 @@
# SPDX-License-Identifier: Apache 2.0
# ruff: noqa: E402 # disable Module level import not at top of file
# mypy: disable-error-code="operator"
+import pytest
+
from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid
from geos.processing.pre_processing.TetQualityAnalysis import TetQualityAnalysis
From 3ea9b21b034c2fef27470e87fe1e399c16c81b7a Mon Sep 17 00:00:00 2001
From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com>
Date: Wed, 4 Feb 2026 14:39:50 +0100
Subject: [PATCH 11/11] ruff
---
geos-processing/tests/test_TetQualityAnalysis.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/geos-processing/tests/test_TetQualityAnalysis.py b/geos-processing/tests/test_TetQualityAnalysis.py
index 810b78b1..59ceeac7 100644
--- a/geos-processing/tests/test_TetQualityAnalysis.py
+++ b/geos-processing/tests/test_TetQualityAnalysis.py
@@ -30,4 +30,4 @@ def test_TetQualityAnalysisRaisePathError( dataSetTest: vtkUnstructuredGrid ) ->
tetQualityFilter.setFilename( "/qliuf/moidh/meshComparison.png" )
with pytest.raises( FileNotFoundError ):
- tetQualityFilter.applyFilter()
\ No newline at end of file
+ tetQualityFilter.applyFilter()