From 5c87d4ccde30fc385f9d51f7c2142f8840e1051f Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 28 May 2025 18:04:55 -0700 Subject: [PATCH 01/20] Change folder named "checks" for "actions" to be less ambiguous about mesh-doctor available features. --- .../doctor/{checks => actions}/__init__.py | 0 .../{checks => actions}/check_fractures.py | 6 +- .../{checks => actions}/collocated_nodes.py | 6 +- .../{checks => actions}/element_volumes.py | 6 +- .../fix_elements_orderings.py | 6 +- .../{checks => actions}/generate_cube.py | 6 +- .../{checks => actions}/generate_fractures.py | 6 +- .../generate_global_ids.py | 6 +- .../{checks => actions}/non_conformal.py | 6 +- .../{checks => actions}/reorient_mesh.py | 0 .../self_intersecting_elements.py | 6 +- .../{checks => actions}/supported_elements.py | 6 +- .../{checks => actions}/triangle_distance.py | 0 .../{checks => actions}/vtk_polyhedron.py | 0 geos-mesh/src/geos/mesh/doctor/mesh_doctor.py | 25 +++---- .../src/geos/mesh/doctor/parsing/__init__.py | 2 +- .../geos/mesh/doctor/parsing/cli_parsing.py | 2 +- geos-mesh/src/geos/mesh/doctor/register.py | 73 +++++++++---------- 18 files changed, 80 insertions(+), 82 deletions(-) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/__init__.py (100%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/check_fractures.py (97%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/collocated_nodes.py (94%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/element_volumes.py (93%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/fix_elements_orderings.py (93%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/generate_cube.py (97%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/generate_fractures.py (99%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/generate_global_ids.py (93%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/non_conformal.py (99%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/reorient_mesh.py (100%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/self_intersecting_elements.py (95%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/supported_elements.py (97%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/triangle_distance.py (100%) rename geos-mesh/src/geos/mesh/doctor/{checks => actions}/vtk_polyhedron.py (100%) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/__init__.py b/geos-mesh/src/geos/mesh/doctor/actions/__init__.py similarity index 100% rename from geos-mesh/src/geos/mesh/doctor/checks/__init__.py rename to geos-mesh/src/geos/mesh/doctor/actions/__init__.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/check_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py similarity index 97% rename from geos-mesh/src/geos/mesh/doctor/checks/check_fractures.py rename to geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py index 91375e47..c0ba40bb 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/check_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py @@ -117,7 +117,7 @@ def __check_neighbors( matrix: vtkUnstructuredGrid, fracture: vtkUnstructuredGri f" for collocated nodes {cns}." ) -def __check( vtk_input_file: str, options: Options ) -> Result: +def __action( vtk_input_file: str, options: Options ) -> Result: matrix, fracture = __read_multiblock( vtk_input_file, options.matrix_name, options.fracture_name ) matrix_points: vtkPoints = matrix.GetPoints() fracture_points: vtkPoints = fracture.GetPoints() @@ -148,9 +148,9 @@ def __check( vtk_input_file: str, options: Options ) -> Result: return Result( errors=errors ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: try: - return __check( vtk_input_file, options ) + return __action( vtk_input_file, options ) except BaseException as e: logging.error( e ) return Result( errors=() ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py similarity index 94% rename from geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py rename to geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py index 74cbbe8c..e8aea5d7 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py @@ -19,7 +19,7 @@ class Result: wrong_support_elements: Collection[ int ] # Element indices with support node indices appearing more than once. -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: points = mesh.GetPoints() locator = vtkIncrementalOctreePointLocator() @@ -63,6 +63,6 @@ def __check( mesh, options: Options ) -> Result: return Result( nodes_buckets=tmp, wrong_support_elements=wrong_support_elements ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/element_volumes.py b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py similarity index 93% rename from geos-mesh/src/geos/mesh/doctor/checks/element_volumes.py rename to geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py index 3a37375f..1c8bb4d2 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/element_volumes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py @@ -18,7 +18,7 @@ class Result: element_volumes: List[ Tuple[ int, float ] ] -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: cs = vtkCellSizeFilter() cs.ComputeAreaOff() @@ -66,6 +66,6 @@ def __check( mesh, options: Options ) -> Result: return Result( element_volumes=small_volumes ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/fix_elements_orderings.py b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py similarity index 93% rename from geos-mesh/src/geos/mesh/doctor/checks/fix_elements_orderings.py rename to geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py index 26c958dc..3e00cf52 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/fix_elements_orderings.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py @@ -17,7 +17,7 @@ class Result: unchanged_cell_types: FrozenSet[ int ] -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: # The vtk cell type is an int and will be the key of the following mapping, # that will point to the relevant permutation. cell_type_to_ordering: Dict[ int, List[ int ] ] = options.cell_type_to_ordering @@ -48,6 +48,6 @@ def __check( mesh, options: Options ) -> Result: unchanged_cell_types=frozenset( unchanged_cell_types ) ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py similarity index 97% rename from geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py rename to geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py index 5abd17f1..926adec7 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -132,15 +132,15 @@ def build_coordinates( positions, num_elements ): return cube -def __check( options: Options ) -> Result: +def __action( options: Options ) -> Result: output_mesh = __build( options ) write_mesh( output_mesh, options.vtk_output ) return Result( info=f"Mesh was written to {options.vtk_output.output}" ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: try: - return __check( options ) + return __action( options ) except BaseException as e: logging.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py similarity index 99% rename from geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py rename to geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py index 82e25b7b..69b400d3 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py @@ -546,7 +546,7 @@ def __split_mesh_on_fractures( mesh: vtkUnstructuredGrid, return ( output_mesh, fracture_meshes ) -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: output_mesh, fracture_meshes = __split_mesh_on_fractures( mesh, options ) write_mesh( output_mesh, options.mesh_VtkOutput ) for i, fracture_mesh in enumerate( fracture_meshes ): @@ -555,7 +555,7 @@ def __check( mesh, options: Options ) -> Result: return Result( info="OK" ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: try: mesh = read_mesh( vtk_input_file ) # Mesh cannot contain global ids before splitting. @@ -564,7 +564,7 @@ def check( vtk_input_file: str, options: Options ) -> Result: " is to split the mesh and then generate global ids for new split meshes." ) logging.error( err_msg ) raise ValueError( err_msg ) - return __check( mesh, options ) + return __action( mesh, options ) except BaseException as e: logging.error( e ) return Result( info="Something went wrong" ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py similarity index 93% rename from geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py rename to geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py index 2fdcfe27..c50ef3f3 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py @@ -45,16 +45,16 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g mesh.GetCellData().SetGlobalIds( cells_global_ids ) -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: __build_global_ids( mesh, options.generate_cells_global_ids, options.generate_points_global_ids ) write_mesh( mesh, options.vtk_output ) return Result( info=f"Mesh was written to {options.vtk_output.output}" ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: try: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) except BaseException as e: logging.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py similarity index 99% rename from geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py rename to geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py index e4037dac..69077172 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -345,7 +345,7 @@ def build_numpy_triangles( points_ids ): return are_points_conformal( point_tolerance, cp_i, cp_j ) -def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: +def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: """ Checks if the mesh is "conformal" (i.e. if some of its boundary faces may not be too close to each other without matching nodes). :param mesh: The vtk mesh @@ -404,6 +404,6 @@ def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: return Result( non_conformal_cells=tmp ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/reorient_mesh.py b/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py similarity index 100% rename from geos-mesh/src/geos/mesh/doctor/checks/reorient_mesh.py rename to geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/self_intersecting_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py similarity index 95% rename from geos-mesh/src/geos/mesh/doctor/checks/self_intersecting_elements.py rename to geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py index 0cad78b4..ee70ac15 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/self_intersecting_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py @@ -21,7 +21,7 @@ class Result: faces_are_oriented_incorrectly_elements: Collection[ int ] -def __check( mesh, options: Options ) -> Result: +def __action( mesh, options: Options ) -> Result: err_out = vtkFileOutputWindow() err_out.SetFileName( "/dev/null" ) # vtkCellValidator outputs loads for each cell... vtk_std_err_out = vtkOutputWindow() @@ -74,6 +74,6 @@ def __check( mesh, options: Options ) -> Result: faces_are_oriented_incorrectly_elements=faces_are_oriented_incorrectly_elements ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py similarity index 97% rename from geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py rename to geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py index 2a1c8061..52cd1183 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -105,7 +105,7 @@ def __call__( self, ic: int ) -> int: return ic -def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: +def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) else: @@ -132,6 +132,6 @@ def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) -def check( vtk_input_file: str, options: Options ) -> Result: +def action( vtk_input_file: str, options: Options ) -> Result: mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) - return __check( mesh, options ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/triangle_distance.py b/geos-mesh/src/geos/mesh/doctor/actions/triangle_distance.py similarity index 100% rename from geos-mesh/src/geos/mesh/doctor/checks/triangle_distance.py rename to geos-mesh/src/geos/mesh/doctor/actions/triangle_distance.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/vtk_polyhedron.py b/geos-mesh/src/geos/mesh/doctor/actions/vtk_polyhedron.py similarity index 100% rename from geos-mesh/src/geos/mesh/doctor/checks/vtk_polyhedron.py rename to geos-mesh/src/geos/mesh/doctor/actions/vtk_polyhedron.py diff --git a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py index ea1bfe8a..6f34f4e7 100644 --- a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py +++ b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py @@ -1,15 +1,14 @@ import sys +min_python_version = ( 3, 7 ) try: - min_python_version = ( 3, 7 ) assert sys.version_info >= min_python_version -except AssertionError as e: +except AssertionError: print( f"Please update python to at least version {'.'.join(map(str, min_python_version))}." ) sys.exit( 1 ) import logging - -from geos.mesh.doctor.parsing import CheckHelper +from geos.mesh.doctor.parsing import ActionHelper from geos.mesh.doctor.parsing.cli_parsing import parse_and_set_verbosity import geos.mesh.doctor.register as register @@ -17,18 +16,18 @@ def main(): logging.basicConfig( format='[%(asctime)s][%(levelname)s] %(message)s' ) parse_and_set_verbosity( sys.argv ) - main_parser, all_checks, all_checks_helpers = register.register() + main_parser, all_actions, all_actions_helpers = register.register() args = main_parser.parse_args( sys.argv[ 1: ] ) - logging.info( f"Checking mesh \"{args.vtk_input_file}\"." ) - check_options = all_checks_helpers[ args.subparsers ].convert( vars( args ) ) + logging.info( f"Working on mesh \"{args.vtk_input_file}\"." ) + action_options = all_actions_helpers[ args.subparsers ].convert( vars( args ) ) try: - check = all_checks[ args.subparsers ] - except KeyError as e: - logging.critical( f"Check {args.subparsers} is not a valid check." ) + action = all_actions[ args.subparsers ] + except KeyError: + logging.critical( f"Action {args.subparsers} is not a valid action." ) sys.exit( 1 ) - helper: CheckHelper = all_checks_helpers[ args.subparsers ] - result = check( args.vtk_input_file, check_options ) - helper.display_results( check_options, result ) + helper: ActionHelper = all_actions_helpers[ args.subparsers ] + result = action( args.vtk_input_file, action_options ) + helper.display_results( action_options, result ) if __name__ == '__main__': diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py index 679f880e..0aff52b9 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py @@ -14,7 +14,7 @@ @dataclass( frozen=True ) -class CheckHelper: +class ActionHelper: fill_subparser: Callable[ [ Any ], argparse.ArgumentParser ] convert: Callable[ [ Any ], Any ] display_results: Callable[ [ Any, Any ], None ] diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py index a34010ba..6129f532 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py @@ -48,7 +48,7 @@ def init_parser() -> argparse.ArgumentParser: Increase verbosity (-{__VERBOSITY_FLAG}, -{__VERBOSITY_FLAG * 2}) to get full information. """ formatter = lambda prog: argparse.RawTextHelpFormatter( prog, max_help_position=8 ) - parser = argparse.ArgumentParser( description='Inspects meshes for GEOSX.', + parser = argparse.ArgumentParser( description='Inspects meshes for GEOS.', epilog=textwrap.dedent( epilog_msg ), formatter_class=formatter ) # Nothing will be done with this verbosity/quiet input. diff --git a/geos-mesh/src/geos/mesh/doctor/register.py b/geos-mesh/src/geos/mesh/doctor/register.py index 75e8d486..84699156 100644 --- a/geos-mesh/src/geos/mesh/doctor/register.py +++ b/geos-mesh/src/geos/mesh/doctor/register.py @@ -2,66 +2,65 @@ import importlib import logging from typing import Dict, Callable, Any, Tuple - import geos.mesh.doctor.parsing as parsing -from geos.mesh.doctor.parsing import CheckHelper, cli_parsing +from geos.mesh.doctor.parsing import ActionHelper, cli_parsing -__HELPERS: Dict[ str, Callable[ [ None ], CheckHelper ] ] = dict() -__CHECKS: Dict[ str, Callable[ [ None ], Any ] ] = dict() +__HELPERS: Dict[ str, Callable[ [ None ], ActionHelper ] ] = dict() +__ACTIONS: Dict[ str, Callable[ [ None ], Any ] ] = dict() -def __load_module_check( module_name: str, check_fct="check" ): - module = importlib.import_module( "geos.mesh.doctor.checks." + module_name ) - return getattr( module, check_fct ) +def __load_module_action( module_name: str, action_fct="action" ): + module = importlib.import_module( "geos.mesh.doctor.actions." + module_name ) + return getattr( module, action_fct ) -def __load_module_check_helper( module_name: str, parsing_fct_suffix="_parsing" ): +def __load_module_action_helper( module_name: str, parsing_fct_suffix="_parsing" ): module = importlib.import_module( "geos.mesh.doctor.parsing." + module_name + parsing_fct_suffix ) - return CheckHelper( fill_subparser=module.fill_subparser, - convert=module.convert, - display_results=module.display_results ) + return ActionHelper( fill_subparser=module.fill_subparser, + convert=module.convert, + display_results=module.display_results ) -def __load_checks() -> Dict[ str, Callable[ [ str, Any ], Any ] ]: +def __load_actions() -> Dict[ str, Callable[ [ str, Any ], Any ] ]: """ - Loads all the checks. + Loads all the actions. This function acts like a protection layer if a module fails to load. - A check that fails to load won't stop the process. - :return: The checks. + A action that fails to load won't stop the process. + :return: The actions. """ - loaded_checks: Dict[ str, Callable[ [ str, Any ], Any ] ] = dict() - for check_name, check_provider in __CHECKS.items(): + loaded_actions: Dict[ str, Callable[ [ str, Any ], Any ] ] = dict() + for action_name, action_provider in __ACTIONS.items(): try: - loaded_checks[ check_name ] = check_provider() - logging.debug( f"Check \"{check_name}\" is loaded." ) + loaded_actions[ action_name ] = action_provider() + logging.debug( f"Action \"{action_name}\" is loaded." ) except Exception as e: - logging.warning( f"Could not load module \"{check_name}\": {e}" ) - return loaded_checks + logging.warning( f"Could not load module \"{action_name}\": {e}" ) + return loaded_actions def register( -) -> Tuple[ argparse.ArgumentParser, Dict[ str, Callable[ [ str, Any ], Any ] ], Dict[ str, CheckHelper ] ]: +) -> Tuple[ argparse.ArgumentParser, Dict[ str, Callable[ [ str, Any ], Any ] ], Dict[ str, ActionHelper ] ]: """ - Register all the parsing checks. Eventually initiate the registration of all the checks too. - :return: The checks and the checks helpers. + Register all the parsing actions. Eventually initiate the registration of all the actions too. + :return: The actions and the actions helpers. """ parser = cli_parsing.init_parser() subparsers = parser.add_subparsers( help="Modules", dest="subparsers" ) def closure_trick( cn: str ): - __HELPERS[ check_name ] = lambda: __load_module_check_helper( cn ) - __CHECKS[ check_name ] = lambda: __load_module_check( cn ) + __HELPERS[ action_name ] = lambda: __load_module_action_helper( cn ) + __ACTIONS[ action_name ] = lambda: __load_module_action( cn ) # Register the modules to load here. - for check_name in ( parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, parsing.FIX_ELEMENTS_ORDERINGS, - parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, parsing.GENERATE_GLOBAL_IDS, - parsing.NON_CONFORMAL, parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS ): - closure_trick( check_name ) - loaded_checks: Dict[ str, Callable[ [ str, Any ], Any ] ] = __load_checks() - loaded_checks_helpers: Dict[ str, CheckHelper ] = dict() - for check_name in loaded_checks.keys(): - h = __HELPERS[ check_name ]() + for action_name in ( parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, parsing.FIX_ELEMENTS_ORDERINGS, + parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, parsing.GENERATE_GLOBAL_IDS, + parsing.NON_CONFORMAL, parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS ): + closure_trick( action_name ) + loaded_actions: Dict[ str, Callable[ [ str, Any ], Any ] ] = __load_actions() + loaded_actions_helpers: Dict[ str, ActionHelper ] = dict() + for action_name in loaded_actions.keys(): + h = __HELPERS[ action_name ]() h.fill_subparser( subparsers ) - loaded_checks_helpers[ check_name ] = h - logging.debug( f"Parsing for check \"{check_name}\" is loaded." ) - return parser, loaded_checks, loaded_checks_helpers + loaded_actions_helpers[ action_name ] = h + logging.debug( f"Parsing for action \"{action_name}\" is loaded." ) + return parser, loaded_actions, loaded_actions_helpers From 3f240fc669b884d32cbc5be8f98ff4389cf8b024 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 2 Jun 2025 06:45:57 -0700 Subject: [PATCH 02/20] First version without results --- .../geos/mesh/doctor/actions/all_checks.py | 26 ++++ .../src/geos/mesh/doctor/parsing/__init__.py | 1 + .../mesh/doctor/parsing/all_checks_parsing.py | 114 ++++++++++++++++++ .../parsing/collocated_nodes_parsing.py | 8 +- .../doctor/parsing/element_volumes_parsing.py | 8 +- .../doctor/parsing/non_conformal_parsing.py | 39 +++--- .../self_intersecting_elements_parsing.py | 7 +- .../parsing/supported_elements_parsing.py | 4 +- geos-mesh/src/geos/mesh/doctor/register.py | 7 +- 9 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/actions/all_checks.py create mode 100644 geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py diff --git a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py new file mode 100644 index 00000000..d05f7a3d --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from geos.mesh.doctor.register import __load_module_action +from geos.utils.Logger import getLogger + +logger = getLogger() + + +@dataclass( frozen=True ) +class Options: + checks_to_perform: list[ str ] + checks_options: list + + +@dataclass( frozen=True ) +class Result: + check_results: dict + + +def action( vtk_input_file: str, options: Options ) -> list[ Result ]: + check_results = dict() + for check, option in zip( options.checks_to_perform, options.checks_options ): + check_action = __load_module_action( check ) + logger.info( f"Performing check '{check}'." ) + check_result = check_action( vtk_input_file, option ) + check_results[ check ] = check_result + return Result( check_results=check_results ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py index 0aff52b9..c37fa92b 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import Callable, Any +ALL_CHECKS = "all_checks" COLLOCATES_NODES = "collocated_nodes" ELEMENT_VOLUMES = "element_volumes" FIX_ELEMENTS_ORDERINGS = "fix_elements_orderings" diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py new file mode 100644 index 00000000..69416cee --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -0,0 +1,114 @@ +from copy import deepcopy +from geos.mesh.doctor.checks.all_checks import Options, Result +from geos.mesh.doctor.parsing import ( ALL_CHECKS, COLLOCATES_NODES, ELEMENT_VOLUMES, NON_CONFORMAL, + SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS ) +from geos.mesh.doctor.parsing.collocated_nodes_parsing import ( __COLLOCATED_NODES_DEFAULT, Options as OptionsCN, Result + as ResultCN ) +from geos.mesh.doctor.parsing.element_volumes_parsing import ( __ELEMENT_VOLUMES_DEFAULT, Options as OptionsEV, Result + as ResultEV ) +from geos.mesh.doctor.parsing.non_conformal_parsing import ( __NON_CONFORMAL_DEFAULT, Options as OptionsNC, Result as + ResultNC ) +from geos.mesh.doctor.parsing.self_intersecting_elements_parsing import ( __SELF_INTERSECTING_ELEMENTS_DEFAULT, Options + as OptionsSIE, Result as ResultSIE ) +from geos.mesh.doctor.parsing.supported_elements_parsing import ( __SUPPORTED_ELEMENTS_DEFAULT, Options as OptionsSE, + Result as ResultSE ) +from geos.utils.Logger import getLogger + +__CHECK_ONLY_FEATURES = [ + COLLOCATES_NODES, ELEMENT_VOLUMES, NON_CONFORMAL, SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS +] +__CHECK_ONLY_FEATURES_DEFAULT = { + COLLOCATES_NODES: __COLLOCATED_NODES_DEFAULT, + ELEMENT_VOLUMES: __ELEMENT_VOLUMES_DEFAULT, + NON_CONFORMAL: __NON_CONFORMAL_DEFAULT, + SELF_INTERSECTING_ELEMENTS: __SELF_INTERSECTING_ELEMENTS_DEFAULT, + SUPPORTED_ELEMENTS: __SUPPORTED_ELEMENTS_DEFAULT +} +__CHECK_ONLY_FEATURES_OPTIONS = { + COLLOCATES_NODES: OptionsCN, + ELEMENT_VOLUMES: OptionsEV, + NON_CONFORMAL: OptionsNC, + SELF_INTERSECTING_ELEMENTS: OptionsSIE, + SUPPORTED_ELEMENTS: OptionsSE +} +__CHECK_ONLY_FEATURES_RESULTS = { + COLLOCATES_NODES: ResultCN, + ELEMENT_VOLUMES: ResultEV, + NON_CONFORMAL: ResultNC, + SELF_INTERSECTING_ELEMENTS: ResultSIE, + SUPPORTED_ELEMENTS: ResultSE +} + +__CHECKS_TO_DO = "checks" +__CHECKS_TO_DO_DEFAULT = __CHECK_ONLY_FEATURES + +__CHECKS_SET_PARAMETERS = "set_parameters" +__CHECKS_SET_PARAMETERS_DEFAULT: list[ str ] = list() +__CHECKS_SET_PARAMETERS_DEFAULT_HELP: str = "" +for feature, default_map in __CHECK_ONLY_FEATURES_DEFAULT.items(): + __CHECKS_SET_PARAMETERS_DEFAULT_HELP += f"For {feature}," + for name, value in default_map.items(): + __CHECKS_SET_PARAMETERS_DEFAULT.append( name + ":" + str( value ) ) + __CHECKS_SET_PARAMETERS_DEFAULT_HELP += " " + name + ":" + str( value ) + __CHECKS_SET_PARAMETERS_DEFAULT_HELP += ". " + +logger = getLogger( "All_checks parsing" ) + + +def fill_subparser( subparsers ) -> None: + p = subparsers.add_parser( + ALL_CHECKS, help="Perform one or multiple mesh-doctor check operation in one command line on a same mesh." ) + p.add_argument( + '--' + __CHECKS_TO_DO, + type=float, + metavar=", ".join( __CHECKS_TO_DO_DEFAULT ), + default=", ".join( __CHECKS_TO_DO_DEFAULT ), + required=False, + help="[list of comma separated str]: Name of the mesh-doctor checks that you want to perform on your mesh." + f" By default, all the checks will be performed which correspond to this list: \"{','.join(__CHECKS_TO_DO_DEFAULT)}\"." + f" If only two of these checks are needed, you can only select them by specifying: --{__CHECKS_TO_DO} {__CHECKS_TO_DO_DEFAULT[0]}, {__CHECKS_TO_DO_DEFAULT[1]}" + ) + p.add_argument( + '--' + __CHECKS_SET_PARAMETERS, + type=float, + metavar=", ".join( __CHECKS_SET_PARAMETERS_DEFAULT ), + default=", ".join( __CHECKS_SET_PARAMETERS_DEFAULT ), + required=False, + help= + "[list of comma separated str]: Each of the checks that will be performed have some parameters to specify when using them." + " By default, every of these parameters have been set to default values that will be mostly used in the vast majority of cases." + f" If you want to change the values, just type the following command: --{__CHECKS_SET_PARAMETERS} parameter0:10, parameter1:25, ..." + f" The complete list of parameters to change with their default value is the following: {__CHECKS_SET_PARAMETERS_DEFAULT_HELP}" + ) + + +def convert( parsed_options ) -> Options: + # first, we need to gather every check that will be performed + checks_to_do: list[ str ] = parsed_options[ __CHECKS_TO_DO ].replace( " ", "" ).split( "," ) + checks_to_perform = set() + for check in checks_to_do: + if check not in __CHECKS_TO_DO_DEFAULT: + logger.critical( f"The given check '{check}' does not exist. Cannot perform this check. Choose between" + f" the available checks: {__CHECKS_TO_DO_DEFAULT}." ) + else: + checks_to_perform.add( check ) + checks_to_perform = list( checks_to_perform ) # only unique checks because of set + # then, we need to find the values to set in the Options object of every check + set_parameters: list[ str ] = parsed_options[ __CHECKS_SET_PARAMETERS ].replace( " ", "" ).split( "," ) + set_parameters_tuple: list[ tuple[ str ] ] = [ p.split( ":" ) for p in set_parameters ] + checks_parameters = deepcopy( __CHECK_ONLY_FEATURES_DEFAULT ) + for set_param in set_parameters_tuple: + for default_parameters in checks_parameters.values(): + if set_param[ 0 ] in default_parameters: + default_parameters[ set_param[ 0 ] ] = float( set_param[ 1 ] ) + # finally, we can create the Options object for every check with the right parameters + checks_options = list() + for check_to_perform in checks_to_perform: + option_to_use = __CHECK_ONLY_FEATURES_OPTIONS[ check_to_perform ] + options_parameters: dict[ str, float ] = checks_parameters[ check_to_perform ] + checks_options.append( option_to_use( **options_parameters ) ) + return Options( checks_to_perform=checks_to_perform, checks_options=checks_options ) + + +def display_results( options: Options, result: Result ): + pass diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py index 7fb84420..c96f7b83 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py @@ -6,10 +6,12 @@ ) from geos.mesh.doctor.checks.collocated_nodes import Options, Result - -from . import COLLOCATES_NODES +from geos.mesh.doctor.parsing import COLLOCATES_NODES __TOLERANCE = "tolerance" +__TOLERANCE_DEFAULT = 0. + +__COLLOCATED_NODES_DEFAULT = { __TOLERANCE: __TOLERANCE_DEFAULT } def convert( parsed_options ) -> Options: @@ -20,6 +22,8 @@ def fill_subparser( subparsers ) -> None: p = subparsers.add_parser( COLLOCATES_NODES, help="Checks if nodes are collocated." ) p.add_argument( '--' + __TOLERANCE, type=float, + metavar=__TOLERANCE_DEFAULT, + default=__TOLERANCE_DEFAULT, required=True, help="[float]: The absolute distance between two nodes for them to be considered collocated." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py index 162b9d3c..ec52151a 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py @@ -1,12 +1,12 @@ import logging - from geos.mesh.doctor.checks.element_volumes import Options, Result +from geos.mesh.doctor.parsing import ELEMENT_VOLUMES -from . import ELEMENT_VOLUMES - -__MIN_VOLUME = "min" +__MIN_VOLUME = "min_vol" __MIN_VOLUME_DEFAULT = 0. +__ELEMENT_VOLUMES_DEFAULT = { __MIN_VOLUME: __MIN_VOLUME_DEFAULT } + def fill_subparser( subparsers ) -> None: p = subparsers.add_parser( ELEMENT_VOLUMES, diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py index d4aeb46a..e48eb06e 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py @@ -1,21 +1,20 @@ import logging - -from typing import ( - FrozenSet, - List, -) - from geos.mesh.doctor.checks.non_conformal import Options, Result - -from . import NON_CONFORMAL +from geos.mesh.doctor.parsing import NON_CONFORMAL __ANGLE_TOLERANCE = "angle_tolerance" __POINT_TOLERANCE = "point_tolerance" __FACE_TOLERANCE = "face_tolerance" __ANGLE_TOLERANCE_DEFAULT = 10. +__POINT_TOLERANCE_DEFAULT = 0. +__FACE_TOLERANCE_DEFAULT = 0. -__ALL_KEYWORDS = { __ANGLE_TOLERANCE, __POINT_TOLERANCE, __FACE_TOLERANCE } +__NON_CONFORMAL_DEFAULT = { + __ANGLE_TOLERANCE: __ANGLE_TOLERANCE_DEFAULT, + __POINT_TOLERANCE: __POINT_TOLERANCE_DEFAULT, + __FACE_TOLERANCE: __FACE_TOLERANCE_DEFAULT +} def convert( parsed_options ) -> Options: @@ -31,19 +30,25 @@ def fill_subparser( subparsers ) -> None: metavar=__ANGLE_TOLERANCE_DEFAULT, default=__ANGLE_TOLERANCE_DEFAULT, help=f"[float]: angle tolerance in degrees. Defaults to {__ANGLE_TOLERANCE_DEFAULT}" ) - p.add_argument( '--' + __POINT_TOLERANCE, - type=float, - help=f"[float]: tolerance for two points to be considered collocated." ) - p.add_argument( '--' + __FACE_TOLERANCE, - type=float, - help=f"[float]: tolerance for two faces to be considered \"touching\"." ) + p.add_argument( + '--' + __POINT_TOLERANCE, + type=float, + metavar=__POINT_TOLERANCE_DEFAULT, + default=__POINT_TOLERANCE_DEFAULT, + help=f"[float]: tolerance for two points to be considered collocated. Defaults to {__POINT_TOLERANCE_DEFAULT}" ) + p.add_argument( + '--' + __FACE_TOLERANCE, + type=float, + metavar=__FACE_TOLERANCE_DEFAULT, + default=__FACE_TOLERANCE_DEFAULT, + help=f"[float]: tolerance for two faces to be considered \"touching\". Defaults to {__FACE_TOLERANCE_DEFAULT}" ) def display_results( options: Options, result: Result ): - non_conformal_cells: List[ int ] = [] + non_conformal_cells: list[ int ] = [] for i, j in result.non_conformal_cells: non_conformal_cells += i, j - non_conformal_cells: FrozenSet[ int ] = frozenset( non_conformal_cells ) + non_conformal_cells: frozenset[ int ] = frozenset( non_conformal_cells ) logging.error( f"You have {len(non_conformal_cells)} non conformal cells.\n{', '.join(map(str, sorted(non_conformal_cells)))}" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py index 3f440d93..f6f2936c 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py @@ -1,14 +1,13 @@ import logging - import numpy - from geos.mesh.doctor.checks.self_intersecting_elements import Options, Result - -from . import SELF_INTERSECTING_ELEMENTS +from geos.mesh.doctor.parsing import SELF_INTERSECTING_ELEMENTS __TOLERANCE = "min" __TOLERANCE_DEFAULT = numpy.finfo( float ).eps +__SELF_INTERSECTING_ELEMENTS_DEFAULT = { __TOLERANCE: __TOLERANCE_DEFAULT } + def convert( parsed_options ) -> Options: tolerance = parsed_options[ __TOLERANCE ] diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py index fea58f3c..326abc8c 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py @@ -8,11 +8,11 @@ __CHUNK_SIZE = "chunck_size" __NUM_PROC = "nproc" -__ALL_KEYWORDS = { __CHUNK_SIZE, __NUM_PROC } - __CHUNK_SIZE_DEFAULT = 1 __NUM_PROC_DEFAULT = multiprocessing.cpu_count() +__SUPPORTED_ELEMENTS_DEFAULT = { __CHUNK_SIZE: __CHUNK_SIZE_DEFAULT, __NUM_PROC: __NUM_PROC_DEFAULT } + def convert( parsed_options ) -> Options: return Options( chunk_size=parsed_options[ __CHUNK_SIZE ], num_proc=parsed_options[ __NUM_PROC ] ) diff --git a/geos-mesh/src/geos/mesh/doctor/register.py b/geos-mesh/src/geos/mesh/doctor/register.py index 84699156..921ac4c0 100644 --- a/geos-mesh/src/geos/mesh/doctor/register.py +++ b/geos-mesh/src/geos/mesh/doctor/register.py @@ -52,9 +52,10 @@ def closure_trick( cn: str ): __ACTIONS[ action_name ] = lambda: __load_module_action( cn ) # Register the modules to load here. - for action_name in ( parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, parsing.FIX_ELEMENTS_ORDERINGS, - parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, parsing.GENERATE_GLOBAL_IDS, - parsing.NON_CONFORMAL, parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS ): + for action_name in ( parsing.ALL_CHECKS, parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, + parsing.FIX_ELEMENTS_ORDERINGS, parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, + parsing.GENERATE_GLOBAL_IDS, parsing.NON_CONFORMAL, parsing.SELF_INTERSECTING_ELEMENTS, + parsing.SUPPORTED_ELEMENTS ): closure_trick( action_name ) loaded_actions: Dict[ str, Callable[ [ str, Any ], Any ] ] = __load_actions() loaded_actions_helpers: Dict[ str, ActionHelper ] = dict() From 892f9f111d06fe2c2b277f8befc5abafebce93f3 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 4 Jun 2025 17:56:29 -0700 Subject: [PATCH 03/20] Improve logging functionality across main mesh-doctor scripts --- geos-mesh/src/geos/mesh/doctor/mesh_doctor.py | 12 ++- .../geos/mesh/doctor/parsing/cli_parsing.py | 82 ++++++++++++++----- .../mesh/doctor/parsing/vtk_output_parsing.py | 6 +- geos-mesh/src/geos/mesh/doctor/register.py | 10 +-- geos-utils/src/geos/utils/Logger.py | 70 ++++++++++++---- 5 files changed, 126 insertions(+), 54 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py index 6f34f4e7..ec4218a9 100644 --- a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py +++ b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py @@ -7,23 +7,21 @@ print( f"Please update python to at least version {'.'.join(map(str, min_python_version))}." ) sys.exit( 1 ) -import logging from geos.mesh.doctor.parsing import ActionHelper -from geos.mesh.doctor.parsing.cli_parsing import parse_and_set_verbosity -import geos.mesh.doctor.register as register +from geos.mesh.doctor.parsing.cli_parsing import parse_and_set_verbosity, setup_logger +from geos.mesh.doctor.register import register_parsing_actions def main(): - logging.basicConfig( format='[%(asctime)s][%(levelname)s] %(message)s' ) parse_and_set_verbosity( sys.argv ) - main_parser, all_actions, all_actions_helpers = register.register() + main_parser, all_actions, all_actions_helpers = register_parsing_actions() args = main_parser.parse_args( sys.argv[ 1: ] ) - logging.info( f"Working on mesh \"{args.vtk_input_file}\"." ) + setup_logger.info( f"Working on mesh \"{args.vtk_input_file}\"." ) action_options = all_actions_helpers[ args.subparsers ].convert( vars( args ) ) try: action = all_actions[ args.subparsers ] except KeyError: - logging.critical( f"Action {args.subparsers} is not a valid action." ) + setup_logger.critical( f"Action {args.subparsers} is not a valid action." ) sys.exit( 1 ) helper: ActionHelper = all_actions_helpers[ args.subparsers ] result = action( args.vtk_input_file, action_options ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py index 6129f532..ce722c22 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py @@ -2,6 +2,7 @@ import logging import textwrap from typing import List +from geos.utils.Logger import getLogger as get_custom_logger # Alias for clarity __VERBOSE_KEY = "verbose" __QUIET_KEY = "quiet" @@ -9,34 +10,71 @@ __VERBOSITY_FLAG = "v" __QUIET_FLAG = "q" +# Get a logger for this setup module itself, using your custom logger +# This ensures its messages (like the "Logger level set to...") use your custom format. +setup_logger = get_custom_logger( "mesh-doctor" ) + + +# --- Conversion Logic --- +def parse_comma_separated_string( value: str ) -> list[ str ]: + """Helper to parse comma-separated strings, stripping whitespace and removing empty items.""" + if not value or not value.strip(): + return list() + return [ item.strip() for item in value.split( ',' ) if item.strip() ] + def parse_and_set_verbosity( cli_args: List[ str ] ) -> None: """ - Parse the verbosity flag only. And sets the logger's level accordingly. - :param cli_args: The list of arguments (as strings) + Parse the verbosity flag only and set the root logger's level accordingly. + Messages from loggers created with `get_custom_logger` will inherit this level + if their own level is set to NOTSET. + :param cli_args: The list of command-line arguments (e.g., sys.argv) :return: None """ dummy_verbosity_parser = argparse.ArgumentParser( add_help=False ) - dummy_verbosity_parser.add_argument( '-' + __VERBOSITY_FLAG, - '--' + __VERBOSE_KEY, - action='count', - default=2, - dest=__VERBOSE_KEY ) - dummy_verbosity_parser.add_argument( '-' + __QUIET_FLAG, - '--' + __QUIET_KEY, - action='count', - default=0, - dest=__QUIET_KEY ) - args = dummy_verbosity_parser.parse_known_args( cli_args[ 1: ] )[ 0 ] - d = vars( args ) - v = d[ __VERBOSE_KEY ] - d[ __QUIET_KEY ] - verbosity = logging.CRITICAL - ( 10 * v ) - if verbosity < logging.DEBUG: - verbosity = logging.DEBUG - elif verbosity > logging.CRITICAL: - verbosity = logging.CRITICAL - logging.getLogger().setLevel( verbosity ) - logging.info( f"Logger level set to \"{logging.getLevelName(verbosity)}\"" ) + # Add verbosity arguments to this dummy parser + dummy_verbosity_parser.add_argument( + '-' + __VERBOSITY_FLAG, + '--' + __VERBOSE_KEY, + action='count', + default=0, # Base default, actual interpretation depends on help text mapping + dest=__VERBOSE_KEY + ) + dummy_verbosity_parser.add_argument( + '-' + __QUIET_FLAG, + '--' + __QUIET_KEY, + action='count', + default=0, + dest=__QUIET_KEY + ) + + # Parse only known args to extract verbosity/quiet flags + # cli_args[1:] is used assuming cli_args[0] is the script name (like sys.argv) + args, _ = dummy_verbosity_parser.parse_known_args( cli_args[ 1: ] ) + + verbose_count = args.verbose + quiet_count = args.quiet + + if verbose_count == 0 and quiet_count == 0: + # Default level (no -v or -q flags) + effective_level = logging.WARNING + elif verbose_count == 1: + effective_level = logging.INFO + elif verbose_count >= 2: + effective_level = logging.DEBUG + elif quiet_count == 1: + effective_level = logging.ERROR + elif quiet_count >= 2: + effective_level = logging.CRITICAL + else: # Should not happen with count logic but good to have a fallback + effective_level = logging.WARNING + + # Set the level on the ROOT logger. + # Loggers from get_custom_logger (with level NOTSET) will inherit this. + setup_logger.setLevel( effective_level ) + + # Use the setup_logger (which uses your custom formatter) for this message + setup_logger.info( f"Logger level set to \"{logging.getLevelName( effective_level )}\"" ) def init_parser() -> argparse.ArgumentParser: diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py index d98d8bcf..5d5a9416 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py @@ -1,7 +1,9 @@ import os.path -import logging import textwrap from geos.mesh.io.vtkIO import VtkOutput +from geos.utils.Logger import getLogger + +logger = getLogger( "vtk_output_parsing" ) __OUTPUT_FILE = "output" __OUTPUT_BINARY_MODE = "data-mode" @@ -40,6 +42,6 @@ def convert( parsed_options, prefix="" ) -> VtkOutput: binary_mode_key = __build_arg( prefix, __OUTPUT_BINARY_MODE ).replace( "-", "_" ) output = parsed_options[ output_key ] if parsed_options[ binary_mode_key ] and os.path.splitext( output )[ -1 ] == ".vtk": - logging.info( "VTK data mode will be ignored for legacy file format \"vtk\"." ) + logger.info( "VTK data mode will be ignored for legacy file format \"vtk\"." ) is_data_mode_binary: bool = parsed_options[ binary_mode_key ] == __OUTPUT_BINARY_MODE_DEFAULT return VtkOutput( output=output, is_data_mode_binary=is_data_mode_binary ) diff --git a/geos-mesh/src/geos/mesh/doctor/register.py b/geos-mesh/src/geos/mesh/doctor/register.py index 921ac4c0..31ac712f 100644 --- a/geos-mesh/src/geos/mesh/doctor/register.py +++ b/geos-mesh/src/geos/mesh/doctor/register.py @@ -1,9 +1,9 @@ import argparse import importlib -import logging from typing import Dict, Callable, Any, Tuple import geos.mesh.doctor.parsing as parsing from geos.mesh.doctor.parsing import ActionHelper, cli_parsing +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __HELPERS: Dict[ str, Callable[ [ None ], ActionHelper ] ] = dict() __ACTIONS: Dict[ str, Callable[ [ None ], Any ] ] = dict() @@ -32,13 +32,13 @@ def __load_actions() -> Dict[ str, Callable[ [ str, Any ], Any ] ]: for action_name, action_provider in __ACTIONS.items(): try: loaded_actions[ action_name ] = action_provider() - logging.debug( f"Action \"{action_name}\" is loaded." ) + setup_logger.debug( f"Action \"{action_name}\" is loaded." ) except Exception as e: - logging.warning( f"Could not load module \"{action_name}\": {e}" ) + setup_logger.warning( f"Could not load module \"{action_name}\": {e}" ) return loaded_actions -def register( +def register_parsing_actions( ) -> Tuple[ argparse.ArgumentParser, Dict[ str, Callable[ [ str, Any ], Any ] ], Dict[ str, ActionHelper ] ]: """ Register all the parsing actions. Eventually initiate the registration of all the actions too. @@ -63,5 +63,5 @@ def closure_trick( cn: str ): h = __HELPERS[ action_name ]() h.fill_subparser( subparsers ) loaded_actions_helpers[ action_name ] = h - logging.debug( f"Parsing for action \"{action_name}\" is loaded." ) + setup_logger.debug( f"Parsing for action \"{action_name}\" is loaded." ) return parser, loaded_actions, loaded_actions_helpers diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 78945525..1dfbadea 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -3,7 +3,6 @@ # SPDX-FileContributor: Martin Lemay import logging from typing import Union - from typing_extensions import Self __doc__ = """ @@ -15,6 +14,7 @@ # types redefinition to import logging.* from this module Logger = logging.Logger #: logger type +# Define logging levels at the module level so they are available for the Formatter class DEBUG: int = logging.DEBUG INFO: int = logging.INFO WARNING: int = logging.WARNING @@ -31,24 +31,26 @@ class CustomLoggerFormatter( logging.Formatter ): .. code-block:: python - logger = logging.getLogger("Logger name") - ch = logging.StreamHandler() - ch.setFormatter(CustomLoggerFormatter()) - logger.addHandler(ch) + logger = logging.getLogger( "Logger name", use_color=False ) + # Ensure handler is added only once, e.g., by checking logger.handlers + if not logger.handlers: + ch = logging.StreamHandler() + ch.setFormatter(CustomLoggerFormatter()) + logger.addHandler(ch) """ - # define color codes grey: str = "\x1b[38;20m" yellow: str = "\x1b[33;20m" red: str = "\x1b[31;20m" bold_red: str = "\x1b[31;1m" reset: str = "\x1b[0m" + # define prefix of log messages format1: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" format2: str = ( "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" ) - #: format for each logger output type - FORMATS: dict[ int, str ] = { + #: format for each logger output type with colors + FORMATS_COLOR: dict[ int, str ] = { DEBUG: grey + format2 + reset, INFO: grey + format1 + reset, WARNING: yellow + format1 + reset, @@ -56,8 +58,8 @@ class CustomLoggerFormatter( logging.Formatter ): CRITICAL: bold_red + format2 + reset, } - #: same without colors - FORMATS_PV: dict[ int, str ] = { + #: format for each logger output type without colors (e.g., for Paraview) + FORMATS_PLAIN: dict[ int, str ] = { DEBUG: format2, INFO: format1, WARNING: format1, @@ -65,6 +67,21 @@ class CustomLoggerFormatter( logging.Formatter ): CRITICAL: format2, } + # Pre-compiled formatters for efficiency + _compiled_formatters: dict[ int, logging.Formatter ] = { + level: logging.Formatter( fmt ) for level, fmt in FORMATS_PLAIN.items() + } + + _compiled_color_formatters: dict[ int, logging.Formatter ] = { + level: logging.Formatter( fmt ) for level, fmt in FORMATS_COLOR.items() + } + + def __init__( self: Self, use_color=False ): + if use_color: + self.active_formatters = self._compiled_color_formatters + else: + self.active_formatters = self._compiled_formatters + def format( self: Self, record: logging.LogRecord ) -> str: """Return the format according to input record. @@ -74,14 +91,22 @@ def format( self: Self, record: logging.LogRecord ) -> str: Returns: str: format as a string """ - log_fmt: Union[ str, None ] = self.FORMATS_PV.get( record.levelno ) - formatter = logging.Formatter( log_fmt ) - return formatter.format( record ) + # Defaulting to plain formatters as per original logic + log_fmt_obj: Union[ logging.Formatter, None ] = self.active_formatters.get( record.levelno ) + if log_fmt_obj: + return log_fmt_obj.format( record ) + else: + # Fallback for unknown levels or if a level is missing in the map + return logging.Formatter().format( record ) -def getLogger( title: str ) -> Logger: +def getLogger( title: str, use_color: bool = False ) -> Logger: """Return the Logger with pre-defined configuration. + This function is now idempotent regarding handler addition. + Calling it multiple times with the same title will return the same + logger instance without adding more handlers if one is already present. + Example: .. code-block:: python @@ -106,8 +131,17 @@ def getLogger( title: str ) -> Logger: Logger: logger """ logger: Logger = logging.getLogger( title ) - logger.setLevel( logging.INFO ) - ch = logging.StreamHandler() - ch.setFormatter( CustomLoggerFormatter() ) - logger.addHandler( ch ) + # Only configure the logger (add handlers, set level) if it hasn't been configured before. + if not logger.hasHandlers(): # More Pythonic way to check if logger.handlers is empty + logger.setLevel( INFO ) # Set the desired default level for this logger + # Create and add the stream handler + ch = logging.StreamHandler() + ch.setFormatter( CustomLoggerFormatter( use_color ) ) # Use your custom formatter + logger.addHandler( ch ) + # Optional: Prevent messages from propagating to the root logger's handlers + logger.propagate = False + # If you need to ensure a certain level is set every time getLogger is called, + # even if handlers were already present, you can set the level outside the 'if' block. + # However, typically, setLevel is part of the initial handler configuration. + # logger.setLevel(INFO) # Uncomment if you need to enforce level on every call return logger From 4e4a9bbfbe2176f5373e662654eeb5c98d69da8b Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 4 Jun 2025 17:58:53 -0700 Subject: [PATCH 04/20] Improve logging across other scripts + better argparse variable name (make them unique across all actions) --- .../mesh/doctor/actions/check_fractures.py | 10 +++-- .../mesh/doctor/actions/collocated_nodes.py | 6 ++- .../mesh/doctor/actions/element_volumes.py | 6 ++- .../geos/mesh/doctor/actions/generate_cube.py | 8 ++-- .../mesh/doctor/actions/generate_fractures.py | 27 ++++++------- .../doctor/actions/generate_global_ids.py | 10 +++-- .../geos/mesh/doctor/actions/non_conformal.py | 3 +- .../geos/mesh/doctor/actions/reorient_mesh.py | 8 ++-- .../actions/self_intersecting_elements.py | 4 +- .../parsing/collocated_nodes_parsing.py | 31 +++++++-------- .../doctor/parsing/element_volumes_parsing.py | 13 ++++--- .../parsing/fix_elements_orderings_parsing.py | 19 +++++----- .../doctor/parsing/generate_cube_parsing.py | 12 +++--- .../parsing/generate_fractures_parsing.py | 2 +- .../parsing/generate_global_ids_parsing.py | 10 ++--- .../doctor/parsing/non_conformal_parsing.py | 8 ++-- .../self_intersecting_elements_parsing.py | 38 ++++++++++--------- 17 files changed, 113 insertions(+), 102 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py index c0ba40bb..636c418d 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py @@ -1,4 +1,3 @@ -import logging import numpy from dataclasses import dataclass from tqdm import tqdm @@ -7,8 +6,11 @@ from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.util.numpy_support import vtk_to_numpy -from geos.mesh.doctor.checks.generate_fractures import Coordinates3D +from geos.mesh.doctor.actions.generate_fractures import Coordinates3D from geos.mesh.utils.genericHelpers import vtk_iter +from geos.utils.Logger import getLogger + +logger = getLogger( "check_fractures" ) @dataclass( frozen=True ) @@ -113,7 +115,7 @@ def __check_neighbors( matrix: vtkUnstructuredGrid, fracture: vtkUnstructuredGri if f in fracture_faces: found += 1 if found != 2: - logging.warning( f"Something went wrong since we should have found 2 fractures faces (we found {found})" + + logger.warning( f"Something went wrong since we should have found 2 fractures faces (we found {found})" + f" for collocated nodes {cns}." ) @@ -152,5 +154,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: try: return __action( vtk_input_file, options ) except BaseException as e: - logging.error( e ) + logger.error( e ) return Result( errors=() ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py index e8aea5d7..5f63dbbf 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py @@ -1,11 +1,13 @@ from collections import defaultdict from dataclasses import dataclass -import logging import numpy from typing import Collection, Iterable from vtkmodules.vtkCommonCore import reference, vtkPoints from vtkmodules.vtkCommonDataModel import vtkIncrementalOctreePointLocator from geos.mesh.io.vtkIO import read_mesh +from geos.utils.Logger import getLogger + +logger = getLogger( "collocated_nodes" ) @dataclass( frozen=True ) @@ -38,7 +40,7 @@ def __action( mesh, options: Options ) -> Result: # If it's not inserted, `point_id` contains the node that was already at that location. # But in that case, `point_id` is the new numbering in the destination points array. # It's more useful for the user to get the old index in the original mesh, so he can look for it in his data. - logging.debug( + logger.debug( f"Point {i} at {points.GetPoint(i)} has been rejected, point {filtered_to_original[point_id.get()]} is already inserted." ) rejected_points[ point_id.get() ].append( i ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py index 1c8bb4d2..20a16f27 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py @@ -1,11 +1,13 @@ from dataclasses import dataclass -import logging from typing import List, Tuple import uuid from vtkmodules.vtkCommonDataModel import VTK_HEXAHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_WEDGE from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter, vtkMeshQuality from vtkmodules.util.numpy_support import vtk_to_numpy from geos.mesh.io.vtkIO import read_mesh +from geos.utils.Logger import getLogger + +logger = getLogger( "element_volumes" ) @dataclass( frozen=True ) @@ -43,7 +45,7 @@ def __action( mesh, options: Options ) -> Result: mq.SetWedgeQualityMeasureToVolume() SUPPORTED_TYPES.append( VTK_WEDGE ) else: - logging.warning( + logger.warning( "Your \"pyvtk\" version does not bring pyramid nor wedge support with vtkMeshQuality. Using the fallback solution." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py index 926adec7..2edca5fe 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -1,13 +1,15 @@ from dataclasses import dataclass -import logging import numpy from typing import Iterable, Sequence from vtkmodules.util.numpy_support import numpy_to_vtk from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkHexahedron, vtkRectilinearGrid, vtkUnstructuredGrid, VTK_HEXAHEDRON ) -from geos.mesh.doctor.checks.generate_global_ids import __build_global_ids +from geos.mesh.doctor.actions.generate_global_ids import __build_global_ids from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.utils.Logger import getLogger + +logger = getLogger( "generate_cube" ) @dataclass( frozen=True ) @@ -142,5 +144,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: try: return __action( options ) except BaseException as e: - logging.error( e ) + logger.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py index 69b400d3..e54e407e 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py @@ -1,7 +1,6 @@ from collections import defaultdict from dataclasses import dataclass from enum import Enum -import logging import networkx from numpy import empty, ones, zeros from tqdm import tqdm @@ -12,15 +11,17 @@ VTK_POLYHEDRON ) from vtkmodules.util.numpy_support import numpy_to_vtk, vtk_to_numpy from vtkmodules.util.vtkConstants import VTK_ID_TYPE -from geos.mesh.doctor.checks.vtk_polyhedron import FaceStream +from geos.mesh.doctor.actions.vtk_polyhedron import FaceStream from geos.mesh.utils.arrayHelpers import has_array - from geos.mesh.utils.genericHelpers import to_vtk_id_list, vtk_iter from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh +from geos.utils.Logger import getLogger """ TypeAliases cannot be used with Python 3.9. A simple assignment like described there will be used: https://docs.python.org/3/library/typing.html#typing.TypeAlias:~:text=through%20simple%20assignment%3A-,Vector%20%3D%20list%5Bfloat%5D,-Or%20marked%20with """ +logger = getLogger( "generate_fractures" ) + IDMapping = Mapping[ int, int ] CellsPointsCoords = dict[ int, list[ tuple[ float ] ] ] Coordinates3D = tuple[ float ] @@ -254,7 +255,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v input_cell_data = old_mesh.GetCellData() for i in range( input_cell_data.GetNumberOfArrays() ): input_array: vtkDataArray = input_cell_data.GetArray( i ) - logging.info( f"Copying cell field \"{input_array.GetName()}\"." ) + logger.info( f"Copying cell field \"{input_array.GetName()}\"." ) tmp = input_array.NewInstance() tmp.DeepCopy( input_array ) splitted_mesh.GetCellData().AddArray( input_array ) @@ -263,7 +264,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v input_field_data = old_mesh.GetFieldData() for i in range( input_field_data.GetNumberOfArrays() ): input_array = input_field_data.GetArray( i ) - logging.info( f"Copying field data \"{input_array.GetName()}\"." ) + logger.info( f"Copying field data \"{input_array.GetName()}\"." ) tmp = input_array.NewInstance() tmp.DeepCopy( input_array ) splitted_mesh.GetFieldData().AddArray( input_array ) @@ -274,7 +275,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v for i in range( input_point_data.GetNumberOfArrays() ): old_points_array = vtk_to_numpy( input_point_data.GetArray( i ) ) name: str = input_point_data.GetArrayName( i ) - logging.info( f"Copying point data \"{name}\"." ) + logger.info( f"Copying point data \"{name}\"." ) old_nrows: int = old_points_array.shape[ 0 ] old_ncols: int = 1 if len( old_points_array.shape ) == 1 else old_points_array.shape[ 1 ] # Reshape old_points_array if it is 1-dimensional @@ -313,7 +314,7 @@ def __copy_fields_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_mesh: v if len( old_cells_array.shape ) == 1: old_cells_array = old_cells_array.reshape( ( old_nrows, 1 ) ) name: str = input_cell_data.GetArrayName( i ) - logging.info( f"Copying cell data \"{name}\"." ) + logger.info( f"Copying cell data \"{name}\"." ) new_array = old_cells_array[ face_cell_id, : ] # Reshape the VTK array to match the original dimensions old_ncols: int = 1 if len( old_cells_array.shape ) == 1 else old_cells_array.shape[ 1 ] @@ -334,7 +335,7 @@ def __copy_fields_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_mesh: v if len( old_points_array.shape ) == 1: old_points_array = old_points_array.reshape( ( old_nrows, 1 ) ) name = input_point_data.GetArrayName( i ) - logging.info( f"Copying point data \"{name}\"." ) + logger.info( f"Copying point data \"{name}\"." ) new_array = old_points_array[ list( node_3d_to_node_2d.keys() ), : ] old_ncols = 1 if len( old_points_array.shape ) == 1 else old_points_array.shape[ 1 ] if old_ncols > 1: @@ -433,7 +434,7 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac :param cell_to_node_mapping: For each cell, gives the nodes that must be duplicated and their new index. :return: The fracture mesh. """ - logging.info( "Generating the meshes" ) + logger.info( "Generating the meshes" ) mesh_points: vtkPoints = old_mesh.GetPoints() is_node_duplicated = zeros( mesh_points.GetNumberOfPoints(), dtype=bool ) # defaults to False @@ -466,11 +467,11 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac # for dfns in discarded_face_nodes: # tmp.append(", ".join(map(str, dfns))) msg: str = "(" + '), ('.join( map( lambda dfns: ", ".join( map( str, dfns ) ), discarded_face_nodes ) ) + ")" - # logging.info(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" + # logger.info(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" # + "from the fracture mesh because none of their/its nodes were duplicated.") # print(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" # + "from the fracture mesh because none of their/its nodes were duplicated.") - logging.info( f"The faces made of nodes [{msg}] were/was discarded" + + logger.info( f"The faces made of nodes [{msg}] were/was discarded" + "from the fracture mesh because none of their/its nodes were duplicated." ) fracture_nodes_tmp = ones( mesh_points.GetNumberOfPoints(), dtype=int ) * -1 @@ -562,9 +563,9 @@ def action( vtk_input_file: str, options: Options ) -> Result: if has_array( mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): err_msg: str = ( "The mesh cannot contain global ids for neither cells nor points. The correct procedure " + " is to split the mesh and then generate global ids for new split meshes." ) - logging.error( err_msg ) + logger.error( err_msg ) raise ValueError( err_msg ) return __action( mesh, options ) except BaseException as e: - logging.error( e ) + logger.error( e ) return Result( info="Something went wrong" ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py index c50ef3f3..97b88339 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py @@ -1,7 +1,9 @@ from dataclasses import dataclass -import logging from vtkmodules.vtkCommonCore import vtkIdTypeArray from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh +from geos.utils.Logger import getLogger + +logger = getLogger( "generate_global_ids" ) @dataclass( frozen=True ) @@ -25,7 +27,7 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g # Building GLOBAL_IDS for points and cells.g GLOBAL_IDS for points and cells. # First for points... if mesh.GetPointData().GetGlobalIds(): - logging.error( "Mesh already has globals ids for points; nothing done." ) + logger.error( "Mesh already has globals ids for points; nothing done." ) elif generate_points_global_ids: point_global_ids = vtkIdTypeArray() point_global_ids.SetName( "GLOBAL_IDS_POINTS" ) @@ -35,7 +37,7 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g mesh.GetPointData().SetGlobalIds( point_global_ids ) # ... then for cells. if mesh.GetCellData().GetGlobalIds(): - logging.error( "Mesh already has globals ids for cells; nothing done." ) + logger.error( "Mesh already has globals ids for cells; nothing done." ) elif generate_cells_global_ids: cells_global_ids = vtkIdTypeArray() cells_global_ids.SetName( "GLOBAL_IDS_CELLS" ) @@ -56,5 +58,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) return __action( mesh, options ) except BaseException as e: - logging.error( e ) + logger.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py index 69077172..d1c83a37 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -12,8 +12,7 @@ from vtkmodules.vtkFiltersCore import vtkPolyDataNormals from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter from vtkmodules.vtkFiltersModeling import vtkCollisionDetectionFilter, vtkLinearExtrusionFilter -from geos.mesh.doctor.checks import reorient_mesh -from geos.mesh.doctor.checks import triangle_distance +from geos.mesh.doctor.actions import reorient_mesh, triangle_distance from geos.mesh.utils.genericHelpers import vtk_iter from geos.mesh.io.vtkIO import read_mesh diff --git a/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py b/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py index aca4c7ee..5f32c94c 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py @@ -1,4 +1,3 @@ -import logging import networkx import numpy from tqdm import tqdm @@ -7,8 +6,11 @@ from vtkmodules.vtkCommonDataModel import ( VTK_POLYHEDRON, VTK_TRIANGLE, vtkCellArray, vtkPolyData, vtkPolygon, vtkUnstructuredGrid, vtkTetra ) from vtkmodules.vtkFiltersCore import vtkTriangleFilter -from geos.mesh.doctor.checks.vtk_polyhedron import FaceStream, build_face_to_face_connectivity_through_edges +from geos.mesh.doctor.actions.vtk_polyhedron import FaceStream, build_face_to_face_connectivity_through_edges from geos.mesh.utils.genericHelpers import to_vtk_id_list +from geos.utils.Logger import getLogger + +logger = getLogger( "reorient_mesh" ) def __compute_volume( mesh_points: vtkPoints, face_stream: FaceStream ) -> float: @@ -129,7 +131,7 @@ def reorient_mesh( mesh, cell_indices: Iterator[ int ] ) -> vtkUnstructuredGrid: # I did not manage to call `output_mesh.CopyStructure(mesh)` because I could not modify the polyhedron in place. # Therefore, I insert the cells one by one... output_mesh.SetPoints( mesh.GetPoints() ) - logging.info( "Reorienting the polyhedron cells to enforce normals directed outward." ) + logger.info( "Reorienting the polyhedron cells to enforce normals directed outward." ) with tqdm( total=needs_to_be_reoriented.sum(), desc="Reorienting polyhedra" ) as progress_bar: # For smoother progress, we only update on reoriented elements. for ic in range( num_cells ): diff --git a/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py index ee70ac15..3b7d313a 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/self_intersecting_elements.py @@ -8,7 +8,7 @@ @dataclass( frozen=True ) class Options: - tolerance: float + min_distance: float @dataclass( frozen=True ) @@ -43,7 +43,7 @@ def __action( mesh, options: Options ) -> Result: faces_are_oriented_incorrectly_elements: List[ int ] = [] f = vtkCellValidator() - f.SetTolerance( options.tolerance ) + f.SetTolerance( options.min_distance ) f.SetInputData( mesh ) f.Update() diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py index c96f7b83..17ddc6a2 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py @@ -1,12 +1,8 @@ -import logging - -from typing import ( - FrozenSet, - List, -) - -from geos.mesh.doctor.checks.collocated_nodes import Options, Result +from geos.mesh.doctor.actions.collocated_nodes import Options, Result from geos.mesh.doctor.parsing import COLLOCATES_NODES +from geos.utils.Logger import getLogger + +logger = getLogger( "Collocated_nodes parsing" ) __TOLERANCE = "tolerance" __TOLERANCE_DEFAULT = 0. @@ -29,25 +25,24 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - all_collocated_nodes: List[ int ] = [] + all_collocated_nodes: list[ int ] = [] for bucket in result.nodes_buckets: for node in bucket: all_collocated_nodes.append( node ) - all_collocated_nodes: FrozenSet[ int ] = frozenset( all_collocated_nodes ) # Surely useless + all_collocated_nodes: frozenset[ int ] = frozenset( all_collocated_nodes ) # Surely useless if all_collocated_nodes: - logging.error( f"You have {len(all_collocated_nodes)} collocated nodes (tolerance = {options.tolerance})." ) + logger.error( f"You have {len(all_collocated_nodes)} collocated nodes (tolerance = {options.tolerance})." ) - logging.info( "Here are all the buckets of collocated nodes." ) - tmp: List[ str ] = [] + logger.info( "Here are all the buckets of collocated nodes." ) + tmp: list[ str ] = [] for bucket in result.nodes_buckets: tmp.append( f"({', '.join(map(str, bucket))})" ) - logging.info( f"({', '.join(tmp)})" ) + logger.info( f"({', '.join(tmp)})" ) else: - logging.error( f"You have no collocated node (tolerance = {options.tolerance})." ) + logger.error( f"You have no collocated node (tolerance = {options.tolerance})." ) if result.wrong_support_elements: tmp: str = ", ".join( map( str, result.wrong_support_elements ) ) - logging.error( f"You have {len(result.wrong_support_elements)} elements with duplicated support nodes.\n" + - tmp ) + logger.error( f"You have {len(result.wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) else: - logging.error( "You have no element with duplicated support nodes." ) + logger.error( "You have no element with duplicated support nodes." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py index ec52151a..2292226b 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py @@ -1,8 +1,10 @@ -import logging -from geos.mesh.doctor.checks.element_volumes import Options, Result +from geos.mesh.doctor.actions.element_volumes import Options, Result from geos.mesh.doctor.parsing import ELEMENT_VOLUMES +from geos.utils.Logger import getLogger -__MIN_VOLUME = "min_vol" +logger = getLogger( "element_volumes parsing" ) + +__MIN_VOLUME = "min_volume" __MIN_VOLUME_DEFAULT = 0. __ELEMENT_VOLUMES_DEFAULT = { __MIN_VOLUME: __MIN_VOLUME_DEFAULT } @@ -29,7 +31,6 @@ def convert( parsed_options ) -> Options: def display_results( options: Options, result: Result ): - logging.error( f"You have {len(result.element_volumes)} elements with volumes smaller than {options.min_volume}." ) + logger.error( f"You have {len(result.element_volumes)} elements with volumes smaller than {options.min_volume}." ) if result.element_volumes: - logging.error( "The elements indices and their volumes are:\n" + - "\n".join( map( str, result.element_volumes ) ) ) + logger.error( "The elements indices and their volumes are:\n\n".join( map( str, result.element_volumes ) ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py index 71fb3a51..8bfa5fed 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py @@ -1,6 +1,4 @@ -import logging import random - from vtkmodules.vtkCommonDataModel import ( VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, @@ -10,10 +8,11 @@ VTK_VOXEL, VTK_WEDGE, ) +from geos.mesh.doctor.actions.fix_elements_orderings import Options, Result +from geos.mesh.doctor.parsing import vtk_output_parsing, FIX_ELEMENTS_ORDERINGS +from geos.utils.Logger import getLogger -from geos.mesh.doctor.checks.fix_elements_orderings import Options, Result - -from . import vtk_output_parsing, FIX_ELEMENTS_ORDERINGS +logger = getLogger( "fix_elements_orderings parsing" ) __CELL_TYPE_MAPPING = { "Hexahedron": VTK_HEXAHEDRON, @@ -63,7 +62,7 @@ def convert( parsed_options ) -> Options: tmp = tuple( map( int, raw_mapping.split( "," ) ) ) if not set( tmp ) == set( range( __CELL_TYPE_SUPPORT_SIZE[ vtk_key ] ) ): err_msg = f"Permutation {raw_mapping} for type {key} is not valid." - logging.error( err_msg ) + logger.error( err_msg ) raise ValueError( err_msg ) cell_type_to_ordering[ vtk_key ] = tmp vtk_output = vtk_output_parsing.convert( parsed_options ) @@ -72,10 +71,10 @@ def convert( parsed_options ) -> Options: def display_results( options: Options, result: Result ): if result.output: - logging.info( f"New mesh was written to file '{result.output}'" ) + logger.info( f"New mesh was written to file '{result.output}'" ) if result.unchanged_cell_types: - logging.info( f"Those vtk types were not reordered: [{', '.join(map(str, result.unchanged_cell_types))}]." ) + logger.info( f"Those vtk types were not reordered: [{', '.join(map(str, result.unchanged_cell_types))}]." ) else: - logging.info( "All the cells of the mesh were reordered." ) + logger.info( "All the cells of the mesh were reordered." ) else: - logging.info( "No output file was written." ) + logger.info( "No output file was written." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py index 650adf04..b83b1f39 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py @@ -1,9 +1,9 @@ -import logging +from geos.mesh.doctor.actions.generate_cube import Options, Result, FieldInfo +from geos.mesh.doctor.parsing import vtk_output_parsing, generate_global_ids_parsing, GENERATE_CUBE +from geos.mesh.doctor.parsing.generate_global_ids_parsing import GlobalIdsInfo +from geos.utils.Logger import getLogger -from geos.mesh.doctor.checks.generate_cube import Options, Result, FieldInfo - -from . import vtk_output_parsing, generate_global_ids_parsing, GENERATE_CUBE -from .generate_global_ids_parsing import GlobalIdsInfo +logger = getLogger( "generate_cube parsing" ) __X, __Y, __Z, __NX, __NY, __NZ = "x", "y", "z", "nx", "ny", "nz" __FIELDS = "fields" @@ -84,4 +84,4 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - logging.info( result.info ) + logger.info( result.info ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/generate_fractures_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/generate_fractures_parsing.py index 18206a4e..85dcb5d4 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/generate_fractures_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/generate_fractures_parsing.py @@ -1,5 +1,5 @@ import os -from geos.mesh.doctor.checks.generate_fractures import Options, Result, FracturePolicy +from geos.mesh.doctor.actions.generate_fractures import Options, Result, FracturePolicy from geos.mesh.doctor.parsing import vtk_output_parsing, GENERATE_FRACTURES from geos.mesh.io.vtkIO import VtkOutput diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py index 43997c67..5902a403 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -import logging +from geos.mesh.doctor.actions.generate_global_ids import Options, Result +from geos.mesh.doctor.parsing import vtk_output_parsing, GENERATE_GLOBAL_IDS +from geos.utils.Logger import getLogger -from geos.mesh.doctor.checks.generate_global_ids import Options, Result - -from . import vtk_output_parsing, GENERATE_GLOBAL_IDS +logger = getLogger( "generate_global_ids parsing" ) __CELLS, __POINTS = "cells", "points" @@ -51,4 +51,4 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - logging.info( result.info ) + logger.info( result.info ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py index e48eb06e..bdc327a4 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py @@ -1,6 +1,8 @@ -import logging -from geos.mesh.doctor.checks.non_conformal import Options, Result +from geos.mesh.doctor.actions.non_conformal import Options, Result from geos.mesh.doctor.parsing import NON_CONFORMAL +from geos.utils.Logger import getLogger + +logger = getLogger( "non_conformal parsing" ) __ANGLE_TOLERANCE = "angle_tolerance" __POINT_TOLERANCE = "point_tolerance" @@ -49,6 +51,6 @@ def display_results( options: Options, result: Result ): for i, j in result.non_conformal_cells: non_conformal_cells += i, j non_conformal_cells: frozenset[ int ] = frozenset( non_conformal_cells ) - logging.error( + logger.error( f"You have {len(non_conformal_cells)} non conformal cells.\n{', '.join(map(str, sorted(non_conformal_cells)))}" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py index f6f2936c..e569dd69 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py @@ -1,39 +1,41 @@ -import logging import numpy -from geos.mesh.doctor.checks.self_intersecting_elements import Options, Result +from geos.mesh.doctor.actions.self_intersecting_elements import Options, Result from geos.mesh.doctor.parsing import SELF_INTERSECTING_ELEMENTS +from geos.utils.Logger import getLogger -__TOLERANCE = "min" -__TOLERANCE_DEFAULT = numpy.finfo( float ).eps +logger = getLogger( "self_intersecting_elements" ) -__SELF_INTERSECTING_ELEMENTS_DEFAULT = { __TOLERANCE: __TOLERANCE_DEFAULT } +__MIN_DISTANCE = "min_distance" +__MIN_DISTANCE_DEFAULT = numpy.finfo( float ).eps + +__SELF_INTERSECTING_ELEMENTS_DEFAULT = { __MIN_DISTANCE: __MIN_DISTANCE_DEFAULT } def convert( parsed_options ) -> Options: - tolerance = parsed_options[ __TOLERANCE ] - if tolerance == 0: - logging.warning( - "Having tolerance set to 0 can induce lots of false positive results (adjacent faces may be considered intersecting)." + min_distance = parsed_options[ __MIN_DISTANCE ] + if min_distance == 0: + logger.warning( + "Having minimum distance set to 0 can induce lots of false positive results (adjacent faces may be considered intersecting)." ) - elif tolerance < 0: + elif min_distance < 0: raise ValueError( - f"Negative tolerance ({tolerance}) in the {SELF_INTERSECTING_ELEMENTS} check is not allowed." ) - return Options( tolerance=tolerance ) + f"Negative minimum distance ({min_distance}) in the {SELF_INTERSECTING_ELEMENTS} check is not allowed." ) + return Options( min_distance=min_distance ) def fill_subparser( subparsers ) -> None: p = subparsers.add_parser( SELF_INTERSECTING_ELEMENTS, help="Checks if the faces of the elements are self intersecting." ) p.add_argument( - '--' + __TOLERANCE, + '--' + __MIN_DISTANCE, type=float, required=False, - metavar=__TOLERANCE_DEFAULT, - default=__TOLERANCE_DEFAULT, - help=f"[float]: The tolerance in the computation. Defaults to your machine precision {__TOLERANCE_DEFAULT}." ) + metavar=__MIN_DISTANCE_DEFAULT, + default=__MIN_DISTANCE_DEFAULT, + help=f"[float]: The minimum distance in the computation. Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." ) def display_results( options: Options, result: Result ): - logging.error( f"You have {len(result.intersecting_faces_elements)} elements with self intersecting faces." ) + logger.error( f"You have {len(result.intersecting_faces_elements)} elements with self intersecting faces." ) if result.intersecting_faces_elements: - logging.error( "The elements indices are:\n" + ", ".join( map( str, result.intersecting_faces_elements ) ) ) + logger.error( "The elements indices are:\n" + ", ".join( map( str, result.intersecting_faces_elements ) ) ) From 54c3cdf68e3c0adff27d4849039d1d6d07727589 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 4 Jun 2025 17:59:48 -0700 Subject: [PATCH 05/20] First version of all_checks without "supported_elements" check --- .../geos/mesh/doctor/actions/all_checks.py | 16 +- .../mesh/doctor/parsing/all_checks_parsing.py | 309 ++++++++++++------ 2 files changed, 214 insertions(+), 111 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py index d05f7a3d..0a504963 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -2,25 +2,27 @@ from geos.mesh.doctor.register import __load_module_action from geos.utils.Logger import getLogger -logger = getLogger() +logger = getLogger( "All_checks" ) @dataclass( frozen=True ) class Options: checks_to_perform: list[ str ] - checks_options: list + checks_options: dict[ str, any ] + check_displays: dict[ str, any ] @dataclass( frozen=True ) class Result: - check_results: dict + check_results: dict[ str, any ] def action( vtk_input_file: str, options: Options ) -> list[ Result ]: check_results = dict() - for check, option in zip( options.checks_to_perform, options.checks_options ): - check_action = __load_module_action( check ) - logger.info( f"Performing check '{check}'." ) + for check_name in options.checks_to_perform: + check_action = __load_module_action( check_name ) + logger.info( f"Performing check '{check_name}'." ) + option = options.checks_options[ check_name ] check_result = check_action( vtk_input_file, option ) - check_results[ check ] = check_result + check_results[ check_name ] = check_result return Result( check_results=check_results ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 69416cee..188d73ba 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -1,114 +1,215 @@ +import argparse # Assuming argparse is used for fill_subparser from copy import deepcopy -from geos.mesh.doctor.checks.all_checks import Options, Result -from geos.mesh.doctor.parsing import ( ALL_CHECKS, COLLOCATES_NODES, ELEMENT_VOLUMES, NON_CONFORMAL, - SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS ) -from geos.mesh.doctor.parsing.collocated_nodes_parsing import ( __COLLOCATED_NODES_DEFAULT, Options as OptionsCN, Result - as ResultCN ) -from geos.mesh.doctor.parsing.element_volumes_parsing import ( __ELEMENT_VOLUMES_DEFAULT, Options as OptionsEV, Result - as ResultEV ) -from geos.mesh.doctor.parsing.non_conformal_parsing import ( __NON_CONFORMAL_DEFAULT, Options as OptionsNC, Result as - ResultNC ) -from geos.mesh.doctor.parsing.self_intersecting_elements_parsing import ( __SELF_INTERSECTING_ELEMENTS_DEFAULT, Options - as OptionsSIE, Result as ResultSIE ) -from geos.mesh.doctor.parsing.supported_elements_parsing import ( __SUPPORTED_ELEMENTS_DEFAULT, Options as OptionsSE, - Result as ResultSE ) +from dataclasses import dataclass +from typing import Type + +# Assuming these are the container classes for all checks +from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult + +# Import constants for check names +from geos.mesh.doctor.parsing import ( + ALL_CHECKS, # Name for the subparser + COLLOCATES_NODES, + ELEMENT_VOLUMES, + NON_CONFORMAL, + SELF_INTERSECTING_ELEMENTS, + # SUPPORTED_ELEMENTS, +) + +# Import module-specific Options, Result, and defaults +# Using module aliases for clarity +from geos.mesh.doctor.parsing import collocated_nodes_parsing as cn_parser +from geos.mesh.doctor.parsing import element_volumes_parsing as ev_parser +from geos.mesh.doctor.parsing import non_conformal_parsing as nc_parser +from geos.mesh.doctor.parsing import self_intersecting_elements_parsing as sie_parser +# from geos.mesh.doctor.parsing import supported_elements_parsing as se_parser +from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string from geos.utils.Logger import getLogger -__CHECK_ONLY_FEATURES = [ - COLLOCATES_NODES, ELEMENT_VOLUMES, NON_CONFORMAL, SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS -] -__CHECK_ONLY_FEATURES_DEFAULT = { - COLLOCATES_NODES: __COLLOCATED_NODES_DEFAULT, - ELEMENT_VOLUMES: __ELEMENT_VOLUMES_DEFAULT, - NON_CONFORMAL: __NON_CONFORMAL_DEFAULT, - SELF_INTERSECTING_ELEMENTS: __SELF_INTERSECTING_ELEMENTS_DEFAULT, - SUPPORTED_ELEMENTS: __SUPPORTED_ELEMENTS_DEFAULT -} -__CHECK_ONLY_FEATURES_OPTIONS = { - COLLOCATES_NODES: OptionsCN, - ELEMENT_VOLUMES: OptionsEV, - NON_CONFORMAL: OptionsNC, - SELF_INTERSECTING_ELEMENTS: OptionsSIE, - SUPPORTED_ELEMENTS: OptionsSE +logger = getLogger( "All_checks_parsing" ) + +# --- Centralized Configuration for Check Features --- +# This structure makes it easier to manage checks and their properties. + + +@dataclass( frozen=True ) # Consider using dataclass if appropriate, or a simple dict +class CheckFeature: + name: str + options_cls: Type[ any ] # Specific Options class (e.g., cn_parser.Options) + result_cls: Type[ any ] # Specific Result class (e.g., cn_parser.Result) + default_params: dict[ str, any ] # Parser keywords with default values + display: Type[ any ] # Specific display function for results + + +# Deepcopy to prevent accidental modification of originals default parameters +CHECK_FEATURES_CONFIG = { + COLLOCATES_NODES: + CheckFeature( name=COLLOCATES_NODES, + options_cls=cn_parser.Options, + result_cls=cn_parser.Result, + default_params=deepcopy( cn_parser.__COLLOCATED_NODES_DEFAULT ), + display=cn_parser.display_results ), + ELEMENT_VOLUMES: + CheckFeature( name=ELEMENT_VOLUMES, + options_cls=ev_parser.Options, + result_cls=ev_parser.Result, + default_params=deepcopy( ev_parser.__ELEMENT_VOLUMES_DEFAULT ), + display=ev_parser.display_results ), + NON_CONFORMAL: + CheckFeature( name=NON_CONFORMAL, + options_cls=nc_parser.Options, + result_cls=nc_parser.Result, + default_params=deepcopy( nc_parser.__NON_CONFORMAL_DEFAULT ), + display=nc_parser.display_results ), + SELF_INTERSECTING_ELEMENTS: + CheckFeature( name=SELF_INTERSECTING_ELEMENTS, + options_cls=sie_parser.Options, + result_cls=sie_parser.Result, + default_params=deepcopy( sie_parser.__SELF_INTERSECTING_ELEMENTS_DEFAULT ), + display=sie_parser.display_results ), + # SUPPORTED_ELEMENTS: + # CheckFeature( name=SUPPORTED_ELEMENTS, + # options_cls=se_parser.Options, + # result_cls=se_parser.Result, + # default_params=deepcopy( se_parser.__SUPPORTED_ELEMENTS_DEFAULT ), + # display=se_parser.display_results ), } -__CHECK_ONLY_FEATURES_RESULTS = { - COLLOCATES_NODES: ResultCN, - ELEMENT_VOLUMES: ResultEV, - NON_CONFORMAL: ResultNC, - SELF_INTERSECTING_ELEMENTS: ResultSIE, - SUPPORTED_ELEMENTS: ResultSE + +# Ordered list of check names, defining the default order and for consistent help messages +ORDERED_CHECK_NAMES: list[ str ] = [ + COLLOCATES_NODES, + ELEMENT_VOLUMES, + NON_CONFORMAL, + SELF_INTERSECTING_ELEMENTS, + # SUPPORTED_ELEMENTS, +] +DEFAULT_PARAMS: dict[ str, dict[ str, float ] ] = { + name: feature.default_params.copy() for name, feature in CHECK_FEATURES_CONFIG.items() } -__CHECKS_TO_DO = "checks" -__CHECKS_TO_DO_DEFAULT = __CHECK_ONLY_FEATURES - -__CHECKS_SET_PARAMETERS = "set_parameters" -__CHECKS_SET_PARAMETERS_DEFAULT: list[ str ] = list() -__CHECKS_SET_PARAMETERS_DEFAULT_HELP: str = "" -for feature, default_map in __CHECK_ONLY_FEATURES_DEFAULT.items(): - __CHECKS_SET_PARAMETERS_DEFAULT_HELP += f"For {feature}," - for name, value in default_map.items(): - __CHECKS_SET_PARAMETERS_DEFAULT.append( name + ":" + str( value ) ) - __CHECKS_SET_PARAMETERS_DEFAULT_HELP += " " + name + ":" + str( value ) - __CHECKS_SET_PARAMETERS_DEFAULT_HELP += ". " - -logger = getLogger( "All_checks parsing" ) - - -def fill_subparser( subparsers ) -> None: - p = subparsers.add_parser( - ALL_CHECKS, help="Perform one or multiple mesh-doctor check operation in one command line on a same mesh." ) - p.add_argument( - '--' + __CHECKS_TO_DO, - type=float, - metavar=", ".join( __CHECKS_TO_DO_DEFAULT ), - default=", ".join( __CHECKS_TO_DO_DEFAULT ), - required=False, - help="[list of comma separated str]: Name of the mesh-doctor checks that you want to perform on your mesh." - f" By default, all the checks will be performed which correspond to this list: \"{','.join(__CHECKS_TO_DO_DEFAULT)}\"." - f" If only two of these checks are needed, you can only select them by specifying: --{__CHECKS_TO_DO} {__CHECKS_TO_DO_DEFAULT[0]}, {__CHECKS_TO_DO_DEFAULT[1]}" +# --- Argument Parser Constants --- +CHECKS_TO_DO_ARG = "checks_to_perform" +PARAMETERS_ARG = "set_parameters" + +# Generate help text for set_parameters dynamically +PARAMETERS_ARG_HELP: str = "" +for check_name in ORDERED_CHECK_NAMES: + config = CHECK_FEATURES_CONFIG[ check_name ] + if config.default_params: + config_params: list[ str ] = list() + for name, value in config.default_params.items(): + config_params.append( f"{name}:{value}" ) + PARAMETERS_ARG_HELP += f"For {check_name}: {', '.join( config_params )}. " + + +# --- Argument Parser Setup --- +def fill_subparser( subparsers: argparse._SubParsersAction ) -> None: + """Fills the subparser for 'ALL_CHECKS' with its arguments.""" + parser = subparsers.add_parser( + ALL_CHECKS, + help="Perform one or multiple mesh-doctor check operations in one command line on the same mesh.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter # Shows defaults in help ) - p.add_argument( - '--' + __CHECKS_SET_PARAMETERS, - type=float, - metavar=", ".join( __CHECKS_SET_PARAMETERS_DEFAULT ), - default=", ".join( __CHECKS_SET_PARAMETERS_DEFAULT ), + parser.add_argument( f"--{CHECKS_TO_DO_ARG}", + type=str, + default="", + required=False, + help=( "Comma-separated list of mesh-doctor checks to perform. If no input was given, all of" + f" the following checks will be executed by default: {ORDERED_CHECK_NAMES}. If you want" + " to choose only certain of them, you can name them individually." + f" Example: --{CHECKS_TO_DO_ARG} {ORDERED_CHECK_NAMES[0]},{ORDERED_CHECK_NAMES[1]}" ) ) + parser.add_argument( + f"--{PARAMETERS_ARG}", + type=str, + default="", required=False, - help= - "[list of comma separated str]: Each of the checks that will be performed have some parameters to specify when using them." - " By default, every of these parameters have been set to default values that will be mostly used in the vast majority of cases." - f" If you want to change the values, just type the following command: --{__CHECKS_SET_PARAMETERS} parameter0:10, parameter1:25, ..." - f" The complete list of parameters to change with their default value is the following: {__CHECKS_SET_PARAMETERS_DEFAULT_HELP}" - ) + help=( "Comma-separated list of parameters to set for the checks (e.g., 'param_name:value'). " + "These parameters override the defaults. Default parameters are:" + f" {PARAMETERS_ARG_HELP} Example: --{PARAMETERS_ARG} parameter_name:10.5,other_param:25" ) ) + + +def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: + """ + Converts parsed command-line arguments into an AllChecksOptions object. + """ + # 1. Determine which checks to perform + final_selected_check_names: list[ str ] = deepcopy( ORDERED_CHECK_NAMES ) + if not parsed_args[ CHECKS_TO_DO_ARG ]: # handles default and if user explicitly provides --checks_to_perform "" + logger.info( "All current available checks in mesh-doctor will be performed." ) + else: # the user specifically entered check names to perform + checks_to_do: list[ str ] = parse_comma_separated_string( parsed_args[ CHECKS_TO_DO_ARG ] ) + final_selected_check_names = list() + for name in checks_to_do: + if name not in CHECK_FEATURES_CONFIG: + logger.warning( f"The given check '{name}' does not exist. Cannot perform this check." + f" Choose from: {ORDERED_CHECK_NAMES}." ) + elif name not in final_selected_check_names: # Add if valid and not already added + final_selected_check_names.append( name ) + + # If after parsing, no valid checks are selected (e.g., all inputs were invalid) + if not final_selected_check_names: + logger.error( "No valid checks selected based on input. No operations will be configured." ) + raise ValueError( "No valid checks selected based on input. No operations will be configured." ) + + # 2. Prepare parameters of Options for every check feature that will be used + final_selected_check_params: dict[ str, dict[ str, float ] ] = deepcopy( DEFAULT_PARAMS ) + for name in list( final_selected_check_params.keys() ): + if name not in final_selected_check_names: + del final_selected_check_params[name] # Remove non-used check features + + if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" + logger.info( "Default configuation of parameters adopted for every check to perform." ) + else: + set_parameters = parse_comma_separated_string( parsed_args[ PARAMETERS_ARG ] ) + for param in set_parameters: + if ':' not in param: + logger.warning( f"Parameter '{param}' in --{PARAMETERS_ARG} is not in 'name:value' format. Skipping." ) + continue + name, *value = param.split( ':', 1 ) + name = name.strip() + if value: # Check if there is anything after the first colon + value_str = value[ 0 ].strip() + else: + # Handle cases where there's nothing after the colon, if necessary + logger.warning( f"Parameter '{name}' has no value after the colon. Skipping or using default." ) + continue + try: + value_float = float( value_str ) + except ValueError: + logger.warning( + f"Invalid value for parameter '{name}': '{value_str}'. Must be a number. Skipping this override." ) + continue + + for check_name_key in final_selected_check_params.keys(): # Iterate through all possible checks + if param in final_selected_check_params[ check_name_key ]: + final_selected_check_params[ check_name_key ][ param ] = value_float + break + + # 3. Instantiate the Options objects for the selected checks using their effective parameters + individual_check_options: dict[ str, any ] = dict() + individual_check_display: dict[ str, any ] = dict() + for check_name in list( final_selected_check_params.keys() ): + options_constructor_params = final_selected_check_params[ check_name ] + feature_config = CHECK_FEATURES_CONFIG[ check_name ] + try: + individual_check_options[ check_name ] = feature_config.options_cls( **options_constructor_params ) + individual_check_display[ check_name ] = feature_config.display + except Exception as e: # Catch potential errors during options instantiation + logger.error( + f"Failed to create options for check '{check_name}' with params {options_constructor_params}: {e}." + f" Therefore the check '{check_name}' will not be performed." ) + final_selected_check_names.remove( check_name ) + + return AllChecksOptions( checks_to_perform=final_selected_check_names, + checks_options=individual_check_options, + check_displays=individual_check_display ) -def convert( parsed_options ) -> Options: - # first, we need to gather every check that will be performed - checks_to_do: list[ str ] = parsed_options[ __CHECKS_TO_DO ].replace( " ", "" ).split( "," ) - checks_to_perform = set() - for check in checks_to_do: - if check not in __CHECKS_TO_DO_DEFAULT: - logger.critical( f"The given check '{check}' does not exist. Cannot perform this check. Choose between" - f" the available checks: {__CHECKS_TO_DO_DEFAULT}." ) - else: - checks_to_perform.add( check ) - checks_to_perform = list( checks_to_perform ) # only unique checks because of set - # then, we need to find the values to set in the Options object of every check - set_parameters: list[ str ] = parsed_options[ __CHECKS_SET_PARAMETERS ].replace( " ", "" ).split( "," ) - set_parameters_tuple: list[ tuple[ str ] ] = [ p.split( ":" ) for p in set_parameters ] - checks_parameters = deepcopy( __CHECK_ONLY_FEATURES_DEFAULT ) - for set_param in set_parameters_tuple: - for default_parameters in checks_parameters.values(): - if set_param[ 0 ] in default_parameters: - default_parameters[ set_param[ 0 ] ] = float( set_param[ 1 ] ) - # finally, we can create the Options object for every check with the right parameters - checks_options = list() - for check_to_perform in checks_to_perform: - option_to_use = __CHECK_ONLY_FEATURES_OPTIONS[ check_to_perform ] - options_parameters: dict[ str, float ] = checks_parameters[ check_to_perform ] - checks_options.append( option_to_use( **options_parameters ) ) - return Options( checks_to_perform=checks_to_perform, checks_options=checks_options ) - - -def display_results( options: Options, result: Result ): - pass +# --- Display Results --- +def display_results( options: AllChecksOptions, result: AllChecksResult ) -> None: + """Displays the results of the checks.""" + # Implementation for displaying results based on the structured options and results. + logger.info( f"Displaying results for checks: {options.checks_to_perform}" ) + for name, res in result.check_results.items(): + options.check_displays[ name ]( options.checks_options[ name ], res ) From 3d0552e13a466d3806e8b2afaec626c60af42115 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 09:53:04 -0700 Subject: [PATCH 06/20] supported_elements action corrected and now included in all_checks --- .../geos/mesh/doctor/actions/all_checks.py | 2 +- .../mesh/doctor/actions/supported_elements.py | 118 ++++++++++++------ .../mesh/doctor/parsing/all_checks_parsing.py | 26 ++-- .../parsing/supported_elements_parsing.py | 22 ++-- 4 files changed, 100 insertions(+), 68 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py index 0a504963..091e4395 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -18,7 +18,7 @@ class Result: def action( vtk_input_file: str, options: Options ) -> list[ Result ]: - check_results = dict() + check_results: dict[ str, any ] = dict() for check_name in options.checks_to_perform: check_action = __load_module_action( check_name ) logger.info( f"Performing check '{check_name}'." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py index 52cd1183..abf7b1a2 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -1,8 +1,6 @@ from dataclasses import dataclass -import logging import multiprocessing import networkx -import numpy from tqdm import tqdm from typing import FrozenSet, Iterable, Mapping, Optional from vtkmodules.util.numpy_support import vtk_to_numpy @@ -10,40 +8,56 @@ from vtkmodules.vtkCommonDataModel import ( vtkCellTypes, vtkUnstructuredGrid, VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, VTK_WEDGE ) -from geos.mesh.doctor.checks.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream +from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream from geos.mesh.utils.genericHelpers import vtk_iter from geos.mesh.io.vtkIO import read_mesh +from geos.utils.Logger import getLogger + +logger = getLogger( "supported_elements" ) @dataclass( frozen=True ) class Options: - num_proc: int + nproc: int chunk_size: int @dataclass( frozen=True ) class Result: unsupported_std_elements_types: FrozenSet[ int ] # list of unsupported types - unsupported_polyhedron_elements: FrozenSet[ - int ] # list of polyhedron elements that could not be converted to supported std elements + unsupported_polyhedron_elements: FrozenSet[ int ] # list of polyhedron elements that could not be converted to supported std elements # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. MESH: Optional[ vtkUnstructuredGrid ] = None -class IsPolyhedronConvertible: +def init_worker_mesh( input_file_for_worker: str ): + """Initializer for multiprocessing.Pool to set the global MESH variable in each worker process. - def __init__( self, mesh: vtkUnstructuredGrid ): - global MESH # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. - MESH = mesh + Args: + input_file_for_worker (str): Filepath to vtk grid + """ + global MESH + logger.debug(f"Worker process (PID: {multiprocessing.current_process().pid}) initializing MESH from file: {input_file_for_worker}") + MESH = read_mesh( input_file_for_worker ) + if MESH is None: + logger.error(f"Worker process (PID: {multiprocessing.current_process().pid}) failed to load mesh from {input_file_for_worker}") + # You might want to raise an error here or ensure MESH being None is handled downstream + # For now, the assert MESH is not None in __call__ will catch this. + +class IsPolyhedronConvertible: + def __init__( self ): def build_prism_graph( n: int, name: str ) -> networkx.Graph: - """ - Builds the face to face connectivities (through edges) for prism graphs. - :param n: The number of nodes of the basis (i.e. the pentagonal prims gets n = 5) - :param name: A human-readable name for logging purpose. - :return: A graph instance. + """Builds the face to face connectivities (through edges) for prism graphs. + + Args: + n (int): The number of nodes of the basis (i.e. the pentagonal prims gets n = 5) + name (str): A human-readable name for logging purpose. + + Returns: + networkx.Graph: A graph instance. """ tmp = networkx.cycle_graph( n ) for node in range( n ): @@ -71,26 +85,34 @@ def build_prism_graph( n: int, name: str ) -> networkx.Graph: } def __is_polyhedron_supported( self, face_stream ) -> str: - """ - Checks if a polyhedron can be converted into a supported cell. + """Checks if a polyhedron can be converted into a supported cell. If so, returns the name of the type. If not, the returned name will be empty. - :param face_stream: The polyhedron. - :return: The name of the supported type or an empty string. + + Args: + face_stream (_type_): The polyhedron. + + Returns: + str: The name of the supported type or an empty string. """ cell_graph = build_face_to_face_connectivity_through_edges( face_stream, add_compatibility=True ) + if cell_graph.order() not in self.__reference_graphs: + return "" for reference_graph in self.__reference_graphs[ cell_graph.order() ]: if networkx.is_isomorphic( reference_graph, cell_graph ): return str( reference_graph.name ) return "" def __call__( self, ic: int ) -> int: - """ - Checks if a vtk polyhedron cell can be converted into a supported GEOSX element. - :param ic: The index element. - :return: -1 if the polyhedron vtk element can be converted into a supported element type. The index otherwise. + """Checks if a vtk polyhedron cell can be converted into a supported GEOSX element. + + Args: + ic (int): The index element. + + Returns: + int: -1 if the polyhedron vtk element can be converted into a supported element type. The index otherwise. """ global MESH - assert MESH is not None + assert MESH is not None, f"MESH global variable not initialized in worker process (PID: {multiprocessing.current_process().pid}). This should have been set by init_worker_mesh." if MESH.GetCellType( ic ) != VTK_POLYHEDRON: return -1 pt_ids = vtkIdList() @@ -98,20 +120,29 @@ def __call__( self, ic: int ) -> int: face_stream = FaceStream.build_from_vtk_id_list( pt_ids ) converted_type_name = self.__is_polyhedron_supported( face_stream ) if converted_type_name: - logging.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) + logger.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) return -1 else: - logging.debug( f"Polyhedron cell {ic} cannot be converted into any supported element." ) + logger.debug( f"Polyhedron cell {ic} (in PID {multiprocessing.current_process().pid}) cannot be converted into any supported element." ) return ic -def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: - if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. - cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) +def __action( vtk_input_file: str, options: Options ) -> Result: + # Main process loads the mesh for its own use + mesh = read_mesh( vtk_input_file ) + if mesh is None: + logger.error(f"Main process failed to load mesh from {vtk_input_file}. Aborting.") + # Return an empty/error result or raise an exception + return Result(unsupported_std_elements_types=frozenset(), unsupported_polyhedron_elements=frozenset()) + + if hasattr( mesh, "GetDistinctCellTypesArray" ): + cell_types_numpy = vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) + cell_types = set(cell_types_numpy.tolist()) else: - cell_types = vtkCellTypes() - mesh.GetCellTypes( cell_types ) - cell_types = set( vtk_iter( cell_types ) ) + vtk_cell_types_obj = vtkCellTypes() + mesh.GetCellTypes( vtk_cell_types_obj ) + cell_types = set( vtk_iter( vtk_cell_types_obj ) ) + supported_cell_types = { VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, VTK_WEDGE @@ -120,18 +151,23 @@ def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: # Dealing with polyhedron elements. num_cells = mesh.GetNumberOfCells() - result = numpy.ones( num_cells, dtype=int ) * -1 - with multiprocessing.Pool( processes=options.num_proc ) as pool: - generator = pool.imap_unordered( IsPolyhedronConvertible( mesh ), + polyhedron_converter = IsPolyhedronConvertible() + + unsupported_polyhedron_indices = [] + # Pass the vtk_input_file to the initializer + with multiprocessing.Pool( processes=options.nproc, + initializer=init_worker_mesh, + initargs=(vtk_input_file,) ) as pool: # Comma makes it a tuple + generator = pool.imap_unordered( polyhedron_converter, range( num_cells ), chunksize=options.chunk_size ) - for i, val in enumerate( tqdm( generator, total=num_cells, desc="Testing support for elements" ) ): - result[ i ] = val - unsupported_polyhedron_elements = [ i for i in result if i > -1 ] + for cell_index_or_neg_one in tqdm( generator, total=num_cells, desc="Testing support for elements" ): + if cell_index_or_neg_one != -1: + unsupported_polyhedron_indices.append(cell_index_or_neg_one) + return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), - unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) + unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_indices ) ) def action( vtk_input_file: str, options: Options ) -> Result: - mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return __action( vtk_input_file, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 188d73ba..d61d89e0 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -1,12 +1,9 @@ -import argparse # Assuming argparse is used for fill_subparser +import argparse from copy import deepcopy from dataclasses import dataclass from typing import Type - -# Assuming these are the container classes for all checks from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult - # Import constants for check names from geos.mesh.doctor.parsing import ( ALL_CHECKS, # Name for the subparser @@ -14,16 +11,15 @@ ELEMENT_VOLUMES, NON_CONFORMAL, SELF_INTERSECTING_ELEMENTS, - # SUPPORTED_ELEMENTS, + SUPPORTED_ELEMENTS, ) - # Import module-specific Options, Result, and defaults # Using module aliases for clarity from geos.mesh.doctor.parsing import collocated_nodes_parsing as cn_parser from geos.mesh.doctor.parsing import element_volumes_parsing as ev_parser from geos.mesh.doctor.parsing import non_conformal_parsing as nc_parser from geos.mesh.doctor.parsing import self_intersecting_elements_parsing as sie_parser -# from geos.mesh.doctor.parsing import supported_elements_parsing as se_parser +from geos.mesh.doctor.parsing import supported_elements_parsing as se_parser from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string from geos.utils.Logger import getLogger @@ -68,12 +64,12 @@ class CheckFeature: result_cls=sie_parser.Result, default_params=deepcopy( sie_parser.__SELF_INTERSECTING_ELEMENTS_DEFAULT ), display=sie_parser.display_results ), - # SUPPORTED_ELEMENTS: - # CheckFeature( name=SUPPORTED_ELEMENTS, - # options_cls=se_parser.Options, - # result_cls=se_parser.Result, - # default_params=deepcopy( se_parser.__SUPPORTED_ELEMENTS_DEFAULT ), - # display=se_parser.display_results ), + SUPPORTED_ELEMENTS: + CheckFeature( name=SUPPORTED_ELEMENTS, + options_cls=se_parser.Options, + result_cls=se_parser.Result, + default_params=deepcopy( se_parser.__SUPPORTED_ELEMENTS_DEFAULT ), + display=se_parser.display_results ), } # Ordered list of check names, defining the default order and for consistent help messages @@ -82,7 +78,7 @@ class CheckFeature: ELEMENT_VOLUMES, NON_CONFORMAL, SELF_INTERSECTING_ELEMENTS, - # SUPPORTED_ELEMENTS, + SUPPORTED_ELEMENTS, ] DEFAULT_PARAMS: dict[ str, dict[ str, float ] ] = { name: feature.default_params.copy() for name, feature in CHECK_FEATURES_CONFIG.items() @@ -156,7 +152,7 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: final_selected_check_params: dict[ str, dict[ str, float ] ] = deepcopy( DEFAULT_PARAMS ) for name in list( final_selected_check_params.keys() ): if name not in final_selected_check_names: - del final_selected_check_params[name] # Remove non-used check features + del final_selected_check_params[ name ] # Remove non-used check features if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" logger.info( "Default configuation of parameters adopted for every check to perform." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py index 326abc8c..e39032a7 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py @@ -1,11 +1,11 @@ -import logging import multiprocessing +from geos.mesh.doctor.actions.supported_elements import Options, Result +from geos.mesh.doctor.parsing import SUPPORTED_ELEMENTS +from geos.utils.Logger import getLogger -from geos.mesh.doctor.checks.supported_elements import Options, Result +logger = getLogger( "supported_elements" ) -from . import SUPPORTED_ELEMENTS - -__CHUNK_SIZE = "chunck_size" +__CHUNK_SIZE = "chunk_size" __NUM_PROC = "nproc" __CHUNK_SIZE_DEFAULT = 1 @@ -15,7 +15,7 @@ def convert( parsed_options ) -> Options: - return Options( chunk_size=parsed_options[ __CHUNK_SIZE ], num_proc=parsed_options[ __NUM_PROC ] ) + return Options( chunk_size=parsed_options[ __CHUNK_SIZE ], nproc=parsed_options[ __NUM_PROC ] ) def fill_subparser( subparsers ) -> None: @@ -39,16 +39,16 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): if result.unsupported_polyhedron_elements: - logging.error( + logger.error( f"There is/are {len(result.unsupported_polyhedron_elements)} polyhedra that may not be converted to supported elements." ) - logging.error( + logger.error( f"The list of the unsupported polyhedra is\n{tuple(sorted(result.unsupported_polyhedron_elements))}." ) else: - logging.info( "All the polyhedra (if any) can be converted to supported elements." ) + logger.info( "All the polyhedra (if any) can be converted to supported elements." ) if result.unsupported_std_elements_types: - logging.error( + logger.error( f"There are unsupported vtk standard element types. The list of those vtk types is {tuple(sorted(result.unsupported_std_elements_types))}." ) else: - logging.info( "All the standard vtk element types (if any) are supported." ) + logger.info( "All the standard vtk element types (if any) are supported." ) From 964a094eaed7d815aed8767d13fe9fadfbdfe359 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 10:08:55 -0700 Subject: [PATCH 07/20] Update tests --- geos-mesh/tests/test_cli_parsing.py | 2 +- geos-mesh/tests/test_collocated_nodes.py | 6 +++--- geos-mesh/tests/test_element_volumes.py | 6 +++--- geos-mesh/tests/test_generate_cube.py | 2 +- geos-mesh/tests/test_generate_fractures.py | 10 +++++----- geos-mesh/tests/test_generate_global_ids.py | 2 +- geos-mesh/tests/test_non_conformal.py | 14 +++++++------- geos-mesh/tests/test_reorient_mesh.py | 4 ++-- geos-mesh/tests/test_self_intersecting_elements.py | 4 ++-- geos-mesh/tests/test_supported_elements.py | 4 ++-- geos-mesh/tests/test_triangle_distance.py | 2 +- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/geos-mesh/tests/test_cli_parsing.py b/geos-mesh/tests/test_cli_parsing.py index a73fe3f3..5187d2e5 100644 --- a/geos-mesh/tests/test_cli_parsing.py +++ b/geos-mesh/tests/test_cli_parsing.py @@ -2,7 +2,7 @@ from dataclasses import dataclass import pytest from typing import Iterator, Sequence -from geos.mesh.doctor.checks.generate_fractures import FracturePolicy, Options +from geos.mesh.doctor.actions.generate_fractures import FracturePolicy, Options from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, display_results, fill_subparser from geos.mesh.io.vtkIO import VtkOutput diff --git a/geos-mesh/tests/test_collocated_nodes.py b/geos-mesh/tests/test_collocated_nodes.py index ecc36dbb..86f798f7 100644 --- a/geos-mesh/tests/test_collocated_nodes.py +++ b/geos-mesh/tests/test_collocated_nodes.py @@ -2,7 +2,7 @@ from typing import Iterator, Tuple from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkTetra, vtkUnstructuredGrid, VTK_TETRA -from geos.mesh.doctor.checks.collocated_nodes import Options, __check +from geos.mesh.doctor.actions.collocated_nodes import Options, __action def get_points() -> Iterator[ Tuple[ vtkPoints, int ] ]: @@ -27,7 +27,7 @@ def test_simple_collocated_points( data: Tuple[ vtkPoints, int ] ): mesh = vtkUnstructuredGrid() mesh.SetPoints( points ) - result = __check( mesh, Options( tolerance=1.e-12 ) ) + result = __action( mesh, Options( tolerance=1.e-12 ) ) assert len( result.wrong_support_elements ) == 0 assert len( result.nodes_buckets ) == num_nodes_bucket @@ -58,7 +58,7 @@ def test_wrong_support_elements(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __check( mesh, Options( tolerance=1.e-12 ) ) + result = __action( mesh, Options( tolerance=1.e-12 ) ) assert len( result.nodes_buckets ) == 0 assert len( result.wrong_support_elements ) == 1 diff --git a/geos-mesh/tests/test_element_volumes.py b/geos-mesh/tests/test_element_volumes.py index 50635eb0..dccbda93 100644 --- a/geos-mesh/tests/test_element_volumes.py +++ b/geos-mesh/tests/test_element_volumes.py @@ -1,7 +1,7 @@ import numpy from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import VTK_TETRA, vtkCellArray, vtkTetra, vtkUnstructuredGrid -from geos.mesh.doctor.checks.element_volumes import Options, __check +from geos.mesh.doctor.actions.element_volumes import Options, __action def test_simple_tet(): @@ -28,12 +28,12 @@ def test_simple_tet(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __check( mesh, Options( min_volume=1. ) ) + result = __action( mesh, Options( min_volume=1. ) ) assert len( result.element_volumes ) == 1 assert result.element_volumes[ 0 ][ 0 ] == 0 assert abs( result.element_volumes[ 0 ][ 1 ] - 1. / 6. ) < 10 * numpy.finfo( float ).eps - result = __check( mesh, Options( min_volume=0. ) ) + result = __action( mesh, Options( min_volume=0. ) ) assert len( result.element_volumes ) == 0 diff --git a/geos-mesh/tests/test_generate_cube.py b/geos-mesh/tests/test_generate_cube.py index effa8aa8..d02ef68b 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -1,4 +1,4 @@ -from geos.mesh.doctor.checks.generate_cube import __build, Options, FieldInfo +from geos.mesh.doctor.actions.generate_cube import __build, Options, FieldInfo def test_generate_cube(): diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index 49f9bd82..66c9496f 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -4,10 +4,10 @@ from typing import Iterable, Iterator, Sequence from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkQuad, VTK_HEXAHEDRON, VTK_POLYHEDRON, VTK_QUAD ) from vtkmodules.util.numpy_support import numpy_to_vtk, vtk_to_numpy -from geos.mesh.doctor.checks.check_fractures import format_collocated_nodes -from geos.mesh.doctor.checks.generate_cube import build_rectilinear_blocks_mesh, XYZ -from geos.mesh.doctor.checks.generate_fractures import ( __split_mesh_on_fractures, Options, FracturePolicy, - Coordinates3D, IDMapping ) +from geos.mesh.doctor.actions.check_fractures import format_collocated_nodes +from geos.mesh.doctor.actions.generate_cube import build_rectilinear_blocks_mesh, XYZ +from geos.mesh.doctor.actions.generate_fractures import ( __split_mesh_on_fractures, Options, FracturePolicy, + Coordinates3D, IDMapping ) from geos.mesh.utils.genericHelpers import to_vtk_id_list FaceNodesCoords = tuple[ tuple[ float ] ] @@ -215,7 +215,7 @@ def test_generate_fracture( test_case: TestCase ): def add_simplified_field_for_cells( mesh: vtkUnstructuredGrid, field_name: str, field_dimension: int ): - """Reduce functionality obtained from src.geos.mesh.doctor.checks.generate_fracture.__add_fields + """Reduce functionality obtained from src.geos.mesh.doctor.actions.generate_fracture.__add_fields where the goal is to add a cell data array with incrementing values. Args: diff --git a/geos-mesh/tests/test_generate_global_ids.py b/geos-mesh/tests/test_generate_global_ids.py index 40c21179..614f771c 100644 --- a/geos-mesh/tests/test_generate_global_ids.py +++ b/geos-mesh/tests/test_generate_global_ids.py @@ -1,6 +1,6 @@ from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkUnstructuredGrid, vtkVertex, VTK_VERTEX -from geos.mesh.doctor.checks.generate_global_ids import __build_global_ids +from geos.mesh.doctor.actions.generate_global_ids import __build_global_ids def test_generate_global_ids(): diff --git a/geos-mesh/tests/test_non_conformal.py b/geos-mesh/tests/test_non_conformal.py index 438f0948..9f6da41a 100644 --- a/geos-mesh/tests/test_non_conformal.py +++ b/geos-mesh/tests/test_non_conformal.py @@ -1,6 +1,6 @@ import numpy -from geos.mesh.doctor.checks.generate_cube import build_rectilinear_blocks_mesh, XYZ -from geos.mesh.doctor.checks.non_conformal import Options, __check +from geos.mesh.doctor.actions.generate_cube import build_rectilinear_blocks_mesh, XYZ +from geos.mesh.doctor.actions.non_conformal import Options, __action def test_two_close_hexs(): @@ -12,13 +12,13 @@ def test_two_close_hexs(): # Close enough, but points tolerance is too strict to consider the faces matching. options = Options( angle_tolerance=1., point_tolerance=delta / 2, face_tolerance=delta * 2 ) - results = __check( mesh, options ) + results = __action( mesh, options ) assert len( results.non_conformal_cells ) == 1 assert set( results.non_conformal_cells[ 0 ] ) == { 0, 1 } # Close enough, and points tolerance is loose enough to consider the faces matching. options = Options( angle_tolerance=1., point_tolerance=delta * 2, face_tolerance=delta * 2 ) - results = __check( mesh, options ) + results = __action( mesh, options ) assert len( results.non_conformal_cells ) == 0 @@ -31,7 +31,7 @@ def test_two_distant_hexs(): options = Options( angle_tolerance=1., point_tolerance=delta / 2., face_tolerance=delta / 2. ) - results = __check( mesh, options ) + results = __action( mesh, options ) assert len( results.non_conformal_cells ) == 0 @@ -44,7 +44,7 @@ def test_two_close_shifted_hexs(): options = Options( angle_tolerance=1., point_tolerance=delta_x * 2, face_tolerance=delta_x * 2 ) - results = __check( mesh, options ) + results = __action( mesh, options ) assert len( results.non_conformal_cells ) == 1 assert set( results.non_conformal_cells[ 0 ] ) == { 0, 1 } @@ -58,6 +58,6 @@ def test_big_elem_next_to_small_elem(): options = Options( angle_tolerance=1., point_tolerance=delta * 2, face_tolerance=delta * 2 ) - results = __check( mesh, options ) + results = __action( mesh, options ) assert len( results.non_conformal_cells ) == 1 assert set( results.non_conformal_cells[ 0 ] ) == { 0, 1 } diff --git a/geos-mesh/tests/test_reorient_mesh.py b/geos-mesh/tests/test_reorient_mesh.py index 5884d5f7..dea8abdc 100644 --- a/geos-mesh/tests/test_reorient_mesh.py +++ b/geos-mesh/tests/test_reorient_mesh.py @@ -4,8 +4,8 @@ from typing import Generator from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_POLYHEDRON -from geos.mesh.doctor.checks.reorient_mesh import reorient_mesh -from geos.mesh.doctor.checks.vtk_polyhedron import FaceStream +from geos.mesh.doctor.actions.reorient_mesh import reorient_mesh +from geos.mesh.doctor.actions.vtk_polyhedron import FaceStream from geos.mesh.utils.genericHelpers import to_vtk_id_list, vtk_iter diff --git a/geos-mesh/tests/test_self_intersecting_elements.py b/geos-mesh/tests/test_self_intersecting_elements.py index d890b1e1..45216f01 100644 --- a/geos-mesh/tests/test_self_intersecting_elements.py +++ b/geos-mesh/tests/test_self_intersecting_elements.py @@ -1,6 +1,6 @@ from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkUnstructuredGrid, VTK_HEXAHEDRON -from geos.mesh.doctor.checks.self_intersecting_elements import Options, __check +from geos.mesh.doctor.actions.self_intersecting_elements import Options, __action def test_jumbled_hex(): @@ -35,7 +35,7 @@ def test_jumbled_hex(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __check( mesh, Options( tolerance=0. ) ) + result = __action( mesh, Options( min_distance=0. ) ) assert len( result.intersecting_faces_elements ) == 1 assert result.intersecting_faces_elements[ 0 ] == 0 diff --git a/geos-mesh/tests/test_supported_elements.py b/geos-mesh/tests/test_supported_elements.py index 5e87fb38..07321abc 100644 --- a/geos-mesh/tests/test_supported_elements.py +++ b/geos-mesh/tests/test_supported_elements.py @@ -3,8 +3,8 @@ from typing import Tuple from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_POLYHEDRON -# from geos.mesh.doctor.checks.supported_elements import Options, check, __check -from geos.mesh.doctor.checks.vtk_polyhedron import parse_face_stream, FaceStream +# from geos.mesh.doctor.actions.supported_elements import Options, action, __action +from geos.mesh.doctor.actions.vtk_polyhedron import parse_face_stream, FaceStream from geos.mesh.utils.genericHelpers import to_vtk_id_list diff --git a/geos-mesh/tests/test_triangle_distance.py b/geos-mesh/tests/test_triangle_distance.py index b90f881b..96274f14 100644 --- a/geos-mesh/tests/test_triangle_distance.py +++ b/geos-mesh/tests/test_triangle_distance.py @@ -2,7 +2,7 @@ import numpy from numpy.linalg import norm import pytest -from geos.mesh.doctor.checks.triangle_distance import distance_between_two_segments, distance_between_two_triangles +from geos.mesh.doctor.actions.triangle_distance import distance_between_two_segments, distance_between_two_triangles @dataclass( frozen=True ) From 1ebccdf10ae2ce2a5e5c76d9cc796f56d73e7024 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 14:14:52 -0700 Subject: [PATCH 08/20] Add test for all_checks + bug fix --- .../mesh/doctor/parsing/all_checks_parsing.py | 4 +- geos-mesh/tests/test_all_checks.py | 165 ++++++++++++++++++ 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 geos-mesh/tests/test_all_checks.py diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index d61d89e0..6e77a4bd 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -178,8 +178,8 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: continue for check_name_key in final_selected_check_params.keys(): # Iterate through all possible checks - if param in final_selected_check_params[ check_name_key ]: - final_selected_check_params[ check_name_key ][ param ] = value_float + if name in final_selected_check_params[ check_name_key ]: + final_selected_check_params[ check_name_key ][ name ] = value_float break # 3. Instantiate the Options objects for the selected checks using their effective parameters diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py new file mode 100644 index 00000000..ffdc6d74 --- /dev/null +++ b/geos-mesh/tests/test_all_checks.py @@ -0,0 +1,165 @@ +import pytest +import argparse +from unittest.mock import patch, MagicMock, call +from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult +from geos.mesh.doctor.actions.all_checks import action +from geos.mesh.doctor.parsing.all_checks_parsing import convert, fill_subparser, display_results +from geos.mesh.doctor.parsing.all_checks_parsing import ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG + + +# Mock data and fixtures +@pytest.fixture +def mock_parser(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers( dest="action" ) + return parser, subparsers + + +@pytest.fixture +def mock_check_action(): + return MagicMock( return_value={ "status": "success" } ) + + +@pytest.fixture +def mock_args(): + return { + "checks_to_perform": "collocated_nodes, element_volumes", + "set_parameters": "tolerance:1.0, min_volume:0.5" + } + + +# Tests for all_checks_parsing.py +class TestAllChecksParsing: + + def test_fill_subparser( self, mock_parser ): + parser, subparsers = mock_parser + fill_subparser( subparsers ) + + # Verify subparser was created + subparsers_actions = [ + action for action in parser._subparsers._actions if isinstance( action, argparse._SubParsersAction ) + ] + assert len( subparsers_actions ) == 1 + + # Check if our subparser is in the choices + subparser_choices = subparsers_actions[ 0 ].choices + assert "all_checks" in subparser_choices # assuming ALL_CHECKS is "all_checks" + + def test_convert_with_default_checks( self ): + # Test with empty string for checks_to_perform (should use all checks) + args = { "checks_to_perform": "", "set_parameters": "" } + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ) as mock_logger: + options = convert( args ) + + # Should log that all checks will be performed + mock_logger.info.assert_any_call( "All current available checks in mesh-doctor will be performed." ) + + # Should include all checks + assert options.checks_to_perform == ORDERED_CHECK_NAMES + + # Should use default parameters + for check_name in ORDERED_CHECK_NAMES: + assert check_name in options.checks_options + + def test_convert_with_specific_checks( self, mock_args ): + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + options = convert( mock_args ) + + # Should only include the specified checks + expected_checks = [ "collocated_nodes", "element_volumes" ] + assert options.checks_to_perform == expected_checks + + # Should only have options for specified checks + assert set( options.checks_options.keys() ) == set( expected_checks ) + + def test_convert_with_invalid_check( self ): + args = { "checks_to_perform": "invalid_check_name", "set_parameters": "" } + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ) as mock_logger: + with pytest.raises( ValueError, match="No valid checks selected" ): + convert( args ) + + # Should log warning about invalid check + mock_logger.warning.assert_called() + + def test_convert_with_parameter_override( self ): + # Choose a check and parameter that exists in DEFAULT_PARAMS + check_name = "collocated_nodes" + param_name = next( iter( CHECK_FEATURES_CONFIG[ check_name ].default_params.keys() ) ) + args = { "checks_to_perform": check_name, "set_parameters": f"{param_name}:99.9" } + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + options = convert( args ) + + # Get the options object for the check + check_options = options.checks_options[ check_name ] + + # Verify the parameter was overridden + # This assumes the parameter is accessible as an attribute of the options object + # May need adjustment based on your actual implementation + assert getattr( check_options, param_name, None ) == 99.9 + + def test_display_results( self ): + # Create mock options and results + mock_display_func = MagicMock() + check_name = ORDERED_CHECK_NAMES[ 0 ] + options = AllChecksOptions( checks_to_perform=[ check_name ], + checks_options={ check_name: "mock_options" }, + check_displays={ check_name: mock_display_func } ) + result = AllChecksResult( check_results={ check_name: "mock_result" } ) + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + display_results( options, result ) + + # Verify display function was called with correct arguments + mock_display_func.assert_called_once_with( "mock_options", "mock_result" ) + + +# Tests for all_checks.py +class TestAllChecks: + + def test_action_calls_check_modules( self, mock_check_action ): + # Setup mock options + check_name = ORDERED_CHECK_NAMES[ 0 ] + mock_options = AllChecksOptions( checks_to_perform=[ check_name ], + checks_options={ check_name: "mock_options" }, + check_displays={ check_name: MagicMock() } ) + # Mock the module loading function + with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', + return_value=mock_check_action ) as mock_load: + with patch( 'geos.mesh.doctor.actions.all_checks.logger' ): + result = action( "test_file.vtk", mock_options ) + + # Verify the module was loaded + mock_load.assert_called_once_with( check_name ) + + # Verify the check action was called with correct args + mock_check_action.assert_called_once_with( "test_file.vtk", "mock_options" ) + + # Verify result contains the check result + assert check_name in result.check_results + assert result.check_results[ check_name ] == { "status": "success" } + + def test_action_with_multiple_checks( self, mock_check_action ): + # Setup mock options with multiple checks + check_names = [ ORDERED_CHECK_NAMES[ 0 ], ORDERED_CHECK_NAMES[ 1 ] ] + mock_options = AllChecksOptions( + checks_to_perform=check_names, + checks_options={ name: f"mock_options_{i}" for i, name in enumerate( check_names ) }, + check_displays={ name: MagicMock() for name in check_names } + ) + # Mock the module loading function + with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', + return_value=mock_check_action ) as mock_load: + with patch( 'geos.mesh.doctor.actions.all_checks.logger' ): + result = action( "test_file.vtk", mock_options ) + + # Verify the modules were loaded + assert mock_load.call_count == 2 + mock_load.assert_has_calls( [ call( check_names[ 0 ] ), call( check_names[ 1 ] ) ] ) + + # Verify all checks were called + assert mock_check_action.call_count == 2 + + # Verify result contains all check results + for name in check_names: + assert name in result.check_results + assert result.check_results[ name ] == { "status": "success" } From cb63ad84dc0da3fa8e8db50fc96888c3f4721fc2 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 16:03:31 -0700 Subject: [PATCH 09/20] Update documentation --- docs/geos-mesh.rst | 18 ++- docs/geos_mesh_docs/doctor.rst | 228 +++++++------------------------- docs/geos_mesh_docs/home.rst | 4 - docs/geos_mesh_docs/modules.rst | 20 --- 4 files changed, 61 insertions(+), 209 deletions(-) delete mode 100644 docs/geos_mesh_docs/home.rst delete mode 100644 docs/geos_mesh_docs/modules.rst diff --git a/docs/geos-mesh.rst b/docs/geos-mesh.rst index 81d82205..061f596d 100644 --- a/docs/geos-mesh.rst +++ b/docs/geos-mesh.rst @@ -1,10 +1,22 @@ GEOS Mesh tools ==================== +**geos-mesh** is a Python package that contains several tools and utilities to handle processing and quality checks of meshes. + .. toctree:: - :maxdepth: 5 + :maxdepth: 1 :caption: Contents: - ./geos_mesh_docs/home.rst + ./geos_mesh_docs/doctor + + ./geos_mesh_docs/converter + + ./geos_mesh_docs/io + + ./geos_mesh_docs/model + + ./geos_mesh_docs/processing + + ./geos_mesh_docs/stats - ./geos_mesh_docs/modules.rst \ No newline at end of file + ./geos_mesh_docs/utils diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index 0da26c1e..762f20b9 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -5,64 +5,30 @@ Mesh Doctor ``mesh-doctor`` is organized as a collection of modules with their dedicated sets of options. The current page will introduce those modules, but the details and all the arguments can be retrieved by using the ``--help`` option for each module. +Prerequisites +^^^^^^^^^^^^^ + +To use mesh-doctor, you first need to have installed the ``geos-mesh`` package using the following command: + +.. code-block:: bash + + python -m pip install --upgrade ./geos-mesh + +Once done, you can call ``mesh-doctor`` in your command line as presented in the rest of this documentation. + Modules ^^^^^^^ To list all the modules available through ``mesh-doctor``, you can simply use the ``--help`` option, which will list all available modules as well as a quick summary. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py --help - usage: mesh_doctor.py [-h] [-v] [-q] -i VTK_MESH_FILE - {collocated_nodes,element_volumes,fix_elements_orderings,generate_cube,generate_fractures,generate_global_ids,non_conformal,self_intersecting_elements,supported_elements} - ... - - Inspects meshes for GEOSX. - - positional arguments: - {collocated_nodes,element_volumes,fix_elements_orderings,generate_cube,generate_fractures,generate_global_ids,non_conformal,self_intersecting_elements,supported_elements} - Modules - collocated_nodes - Checks if nodes are collocated. - element_volumes - Checks if the volumes of the elements are greater than "min". - fix_elements_orderings - Reorders the support nodes for the given cell types. - generate_cube - Generate a cube and its fields. - generate_fractures - Splits the mesh to generate the faults and fractures. [EXPERIMENTAL] - generate_global_ids - Adds globals ids for points and cells. - non_conformal - Detects non conformal elements. [EXPERIMENTAL] - self_intersecting_elements - Checks if the faces of the elements are self intersecting. - supported_elements - Check that all the elements of the mesh are supported by GEOSX. - - options: - -h, --help - show this help message and exit - -v Use -v 'INFO', -vv for 'DEBUG'. Defaults to 'WARNING'. - -q Use -q to reduce the verbosity of the output. - -i VTK_MESH_FILE, --vtk-input-file VTK_MESH_FILE - - Note that checks are dynamically loaded. - An option may be missing because of an unloaded module. - Increase verbosity (-v, -vv) to get full information. +.. command-output:: mesh-doctor --help + :shell: Then, if you are interested in a specific module, you can ask for its documentation using the ``mesh-doctor module_name --help`` pattern. For example -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py collocated_nodes --help - usage: mesh_doctor.py collocated_nodes [-h] --tolerance TOLERANCE - - options: - -h, --help show this help message and exit - --tolerance TOLERANCE [float]: The absolute distance between two nodes for them to be considered collocated. +.. command-output:: mesh-doctor collocated_nodes --help + :shell: ``mesh-doctor`` loads its module dynamically. If a module can't be loaded, ``mesh-doctor`` will proceed and try to load other modules. @@ -78,20 +44,27 @@ You can solve this issue by installing the dependencies of ``mesh-doctor`` defin Here is a list and brief description of all the modules available. +``all_checks`` +"""""""""""""""""""" + +``mesh-doctor`` modules are called ``actions`` and they can be splitted into 2 different categories: +``check actions`` that will give you a feedback on a .vtu mesh that you would like to use in GEOS. +``operate actions`` that will either create a new mesh or modify a mesh. + +``all_checks`` aims at applying every single ``check`` action in one single command. The list is the following: +``collocated_nodes``, ``element_volumes``, ``non_conformal``, ``self_intersecting_elements``, ``supported_elements``. + +.. command-output:: mesh-doctor all_checks --help + :shell: + ``collocated_nodes`` """""""""""""""""""" Displays the neighboring nodes that are closer to each other than a prescribed threshold. It is not uncommon to define multiple nodes for the exact same position, which will typically be an issue for ``geos`` and should be fixed. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py collocated_nodes --help - usage: mesh_doctor.py collocated_nodes [-h] --tolerance TOLERANCE - - options: - -h, --help show this help message and exit - --tolerance TOLERANCE [float]: The absolute distance between two nodes for them to be considered collocated. +.. command-output:: mesh-doctor collocated_nodes --help + :shell: ``element_volumes`` """"""""""""""""""" @@ -99,14 +72,8 @@ It is not uncommon to define multiple nodes for the exact same position, which w Computes the volumes of all the cells and displays the ones that are below a prescribed threshold. Cells with negative volumes will typically be an issue for ``geos`` and should be fixed. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py element_volumes --help - usage: mesh_doctor.py element_volumes [-h] --min 0.0 - - options: - -h, --help show this help message and exit - --min 0.0 [float]: The minimum acceptable volume. Defaults to 0.0. +.. command-output:: mesh-doctor element_volumes --help + :shell: ``fix_elements_orderings`` """""""""""""""""""""""""" @@ -115,29 +82,8 @@ It sometimes happens that an exported mesh does not abide by the ``vtk`` orderin The ``fix_elements_orderings`` module can rearrange the nodes of given types of elements. This can be convenient if you cannot regenerate the mesh. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py fix_elements_orderings --help - usage: mesh_doctor.py fix_elements_orderings [-h] [--Hexahedron 1,6,5,4,7,0,2,3] [--Prism5 8,2,0,7,6,9,5,1,4,3] - [--Prism6 11,2,8,10,5,0,9,7,6,1,4,3] [--Pyramid 3,4,0,2,1] - [--Tetrahedron 2,0,3,1] [--Voxel 1,6,5,4,7,0,2,3] - [--Wedge 3,5,4,0,2,1] --output OUTPUT [--data-mode binary, ascii] - - options: - -h, --help show this help message and exit - --Hexahedron 1,6,5,4,7,0,2,3 - [list of integers]: node permutation for "Hexahedron". - --Prism5 8,2,0,7,6,9,5,1,4,3 - [list of integers]: node permutation for "Prism5". - --Prism6 11,2,8,10,5,0,9,7,6,1,4,3 - [list of integers]: node permutation for "Prism6". - --Pyramid 3,4,0,2,1 [list of integers]: node permutation for "Pyramid". - --Tetrahedron 2,0,3,1 [list of integers]: node permutation for "Tetrahedron". - --Voxel 1,6,5,4,7,0,2,3 [list of integers]: node permutation for "Voxel". - --Wedge 3,5,4,0,2,1 [list of integers]: node permutation for "Wedge". - --output OUTPUT [string]: The vtk output file destination. - --data-mode binary, ascii - [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. +.. command-output:: mesh-doctor fix_elements_orderings --help + :shell: ``generate_cube`` """"""""""""""""" @@ -146,30 +92,8 @@ This module conveniently generates cubic meshes in ``vtk``. It can also generate fields with simple values. This tool can also be useful to generate a trial mesh that will later be refined or customized. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py generate_cube --help - usage: mesh_doctor.py generate_cube [-h] [--x 0:1.5:3] [--y 0:5:10] [--z 0:1] [--nx 2:2] [--ny 1:1] [--nz 4] - [--fields name:support:dim [name:support:dim ...]] [--cells] [--no-cells] - [--points] [--no-points] --output OUTPUT [--data-mode binary, ascii] - - options: - -h, --help show this help message and exit - --x 0:1.5:3 [list of floats]: X coordinates of the points. - --y 0:5:10 [list of floats]: Y coordinates of the points. - --z 0:1 [list of floats]: Z coordinates of the points. - --nx 2:2 [list of integers]: Number of elements in the X direction. - --ny 1:1 [list of integers]: Number of elements in the Y direction. - --nz 4 [list of integers]: Number of elements in the Z direction. - --fields name:support:dim - [name:support:dim ...]: Create fields on CELLS or POINTS, with given dimension (typically 1 or 3). - --cells [bool]: Generate global ids for cells. Defaults to true. - --no-cells [bool]: Don't generate global ids for cells. - --points [bool]: Generate global ids for points. Defaults to true. - --no-points [bool]: Don't generate global ids for points. - --output OUTPUT [string]: The vtk output file destination. - --data-mode binary, ascii - [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. +.. command-output:: mesh-doctor generate_cube --help + :shell: ``generate_fractures`` """""""""""""""""""""" @@ -177,30 +101,8 @@ This tool can also be useful to generate a trial mesh that will later be refined For a conformal fracture to be defined in a mesh, ``geos`` requires the mesh to be split at the faces where the fracture gets across the mesh. The ``generate_fractures`` module will split the mesh and generate the multi-block ``vtk`` files. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py generate_fractures --help - usage: mesh_doctor.py generate_fractures [-h] --policy field, internal_surfaces [--name NAME] [--values VALUES] --output OUTPUT - [--data-mode binary, ascii] [--fractures_output_dir FRACTURES_OUTPUT_DIR] - - options: - -h, --help show this help message and exit - --policy field, internal_surfaces - [string]: The criterion to define the surfaces that will be changed into fracture zones. Possible values are "field, internal_surfaces" - --name NAME [string]: If the "field" policy is selected, defines which field will be considered to define the fractures. - If the "internal_surfaces" policy is selected, defines the name of the attribute will be considered to identify the fractures. - --values VALUES [list of comma separated integers]: If the "field" policy is selected, which changes of the field will be considered as a fracture. - If the "internal_surfaces" policy is selected, list of the fracture attributes. - You can create multiple fractures by separating the values with ':' like shown in this example. - --values 10,12:13,14,16,18:22 will create 3 fractures identified respectively with the values (10,12), (13,14,16,18) and (22). - If no ':' is found, all values specified will be assumed to create only 1 single fracture. - --output OUTPUT [string]: The vtk output file destination. - --data-mode binary, ascii - [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. - --fractures_output_dir FRACTURES_OUTPUT_DIR - [string]: The output directory for the fractures meshes that will be generated from the mesh. - --fractures_data_mode FRACTURES_DATA_MODE - [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. +.. command-output:: mesh-doctor generate_fractures --help + :shell: ``generate_global_ids`` """"""""""""""""""""""" @@ -208,21 +110,8 @@ The ``generate_fractures`` module will split the mesh and generate the multi-blo When running ``geos`` in parallel, `global ids` can be used to refer to data across multiple ranks. The ``generate_global_ids`` can generate `global ids` for the imported ``vtk`` mesh. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py generate_global_ids --help - usage: mesh_doctor.py generate_global_ids [-h] [--cells] [--no-cells] [--points] [--no-points] --output OUTPUT - [--data-mode binary, ascii] - - options: - -h, --help show this help message and exit - --cells [bool]: Generate global ids for cells. Defaults to true. - --no-cells [bool]: Don't generate global ids for cells. - --points [bool]: Generate global ids for points. Defaults to true. - --no-points [bool]: Don't generate global ids for points. - --output OUTPUT [string]: The vtk output file destination. - --data-mode binary, ascii - [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. +.. command-output:: mesh-doctor generate_global_ids --help + :shell: ``non_conformal`` """"""""""""""""" @@ -232,19 +121,8 @@ This module will detect elements which are close enough (there's a user defined The angle between two faces can also be precribed. This module can be a bit time consuming. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py non_conformal --help - usage: mesh_doctor.py non_conformal [-h] [--angle_tolerance 10.0] [--point_tolerance POINT_TOLERANCE] - [--face_tolerance FACE_TOLERANCE] - - options: - -h, --help show this help message and exit - --angle_tolerance 10.0 [float]: angle tolerance in degrees. Defaults to 10.0 - --point_tolerance POINT_TOLERANCE - [float]: tolerance for two points to be considered collocated. - --face_tolerance FACE_TOLERANCE - [float]: tolerance for two faces to be considered "touching". +.. command-output:: mesh-doctor non_conformal --help + :shell: ``self_intersecting_elements`` """""""""""""""""""""""""""""" @@ -252,15 +130,8 @@ This module can be a bit time consuming. Some meshes can have cells that auto-intersect. This module will display the elements that have faces intersecting. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py self_intersecting_elements --help - usage: mesh_doctor.py self_intersecting_elements [-h] [--min 2.220446049250313e-16] - - options: - -h, --help show this help message and exit - --min 2.220446049250313e-16 - [float]: The tolerance in the computation. Defaults to your machine precision 2.220446049250313e-16. +.. command-output:: mesh-doctor self_intersecting_elements --help + :shell: ``supported_elements`` """""""""""""""""""""" @@ -273,12 +144,5 @@ But also prismes up to 11 faces. The ``supported_elements`` check will validate that no unsupported element is included in the input mesh. It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get converted into a supported type of element. -.. code-block:: - - $ python src/geos/mesh/doctor/mesh_doctor.py supported_elements --help - usage: mesh_doctor.py supported_elements [-h] [--chunck_size 1] [--nproc 8] - - options: - -h, --help show this help message and exit - --chunck_size 1 [int]: Defaults chunk size for parallel processing to 1 - --nproc 8 [int]: Number of threads used for parallel processing. Defaults to your CPU count 8. \ No newline at end of file +.. command-output:: mesh-doctor supported_elements --help + :shell: \ No newline at end of file diff --git a/docs/geos_mesh_docs/home.rst b/docs/geos_mesh_docs/home.rst deleted file mode 100644 index 78cffacb..00000000 --- a/docs/geos_mesh_docs/home.rst +++ /dev/null @@ -1,4 +0,0 @@ -Home -======== - -**geos-mesh** is a Python package that contains several tools and utilities to handle processing and quality checks of meshes. \ No newline at end of file diff --git a/docs/geos_mesh_docs/modules.rst b/docs/geos_mesh_docs/modules.rst deleted file mode 100644 index 4e13c711..00000000 --- a/docs/geos_mesh_docs/modules.rst +++ /dev/null @@ -1,20 +0,0 @@ -GEOS Mesh tools -=================== - - -.. toctree:: - :maxdepth: 5 - - doctor - - converter - - io - - model - - processing - - stats - - utils \ No newline at end of file From a4fe9d638fb7e95ecbea4a18375c7a207d31faba Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 16:05:32 -0700 Subject: [PATCH 10/20] yapf format --- .../mesh/doctor/actions/check_fractures.py | 2 +- .../mesh/doctor/actions/generate_fractures.py | 2 +- .../mesh/doctor/actions/supported_elements.py | 34 +++++++++++-------- .../mesh/doctor/parsing/all_checks_parsing.py | 3 +- .../geos/mesh/doctor/parsing/cli_parsing.py | 15 ++++---- .../self_intersecting_elements_parsing.py | 4 ++- geos-mesh/tests/test_all_checks.py | 12 ++++--- 7 files changed, 40 insertions(+), 32 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py index 636c418d..ce6fcd6a 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py @@ -116,7 +116,7 @@ def __check_neighbors( matrix: vtkUnstructuredGrid, fracture: vtkUnstructuredGri found += 1 if found != 2: logger.warning( f"Something went wrong since we should have found 2 fractures faces (we found {found})" + - f" for collocated nodes {cns}." ) + f" for collocated nodes {cns}." ) def __action( vtk_input_file: str, options: Options ) -> Result: diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py index e54e407e..a755b7e8 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py @@ -472,7 +472,7 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac # print(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" # + "from the fracture mesh because none of their/its nodes were duplicated.") logger.info( f"The faces made of nodes [{msg}] were/was discarded" + - "from the fracture mesh because none of their/its nodes were duplicated." ) + "from the fracture mesh because none of their/its nodes were duplicated." ) fracture_nodes_tmp = ones( mesh_points.GetNumberOfPoints(), dtype=int ) * -1 for ns in face_nodes: diff --git a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py index abf7b1a2..0fa8fc63 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -25,7 +25,8 @@ class Options: @dataclass( frozen=True ) class Result: unsupported_std_elements_types: FrozenSet[ int ] # list of unsupported types - unsupported_polyhedron_elements: FrozenSet[ int ] # list of polyhedron elements that could not be converted to supported std elements + unsupported_polyhedron_elements: FrozenSet[ + int ] # list of polyhedron elements that could not be converted to supported std elements # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. @@ -39,16 +40,22 @@ def init_worker_mesh( input_file_for_worker: str ): input_file_for_worker (str): Filepath to vtk grid """ global MESH - logger.debug(f"Worker process (PID: {multiprocessing.current_process().pid}) initializing MESH from file: {input_file_for_worker}") + logger.debug( + f"Worker process (PID: {multiprocessing.current_process().pid}) initializing MESH from file: {input_file_for_worker}" + ) MESH = read_mesh( input_file_for_worker ) if MESH is None: - logger.error(f"Worker process (PID: {multiprocessing.current_process().pid}) failed to load mesh from {input_file_for_worker}") + logger.error( + f"Worker process (PID: {multiprocessing.current_process().pid}) failed to load mesh from {input_file_for_worker}" + ) # You might want to raise an error here or ensure MESH being None is handled downstream # For now, the assert MESH is not None in __call__ will catch this. class IsPolyhedronConvertible: + def __init__( self ): + def build_prism_graph( n: int, name: str ) -> networkx.Graph: """Builds the face to face connectivities (through edges) for prism graphs. @@ -123,7 +130,9 @@ def __call__( self, ic: int ) -> int: logger.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) return -1 else: - logger.debug( f"Polyhedron cell {ic} (in PID {multiprocessing.current_process().pid}) cannot be converted into any supported element." ) + logger.debug( + f"Polyhedron cell {ic} (in PID {multiprocessing.current_process().pid}) cannot be converted into any supported element." + ) return ic @@ -131,13 +140,13 @@ def __action( vtk_input_file: str, options: Options ) -> Result: # Main process loads the mesh for its own use mesh = read_mesh( vtk_input_file ) if mesh is None: - logger.error(f"Main process failed to load mesh from {vtk_input_file}. Aborting.") + logger.error( f"Main process failed to load mesh from {vtk_input_file}. Aborting." ) # Return an empty/error result or raise an exception - return Result(unsupported_std_elements_types=frozenset(), unsupported_polyhedron_elements=frozenset()) + return Result( unsupported_std_elements_types=frozenset(), unsupported_polyhedron_elements=frozenset() ) if hasattr( mesh, "GetDistinctCellTypesArray" ): cell_types_numpy = vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) - cell_types = set(cell_types_numpy.tolist()) + cell_types = set( cell_types_numpy.tolist() ) else: vtk_cell_types_obj = vtkCellTypes() mesh.GetCellTypes( vtk_cell_types_obj ) @@ -155,15 +164,12 @@ def __action( vtk_input_file: str, options: Options ) -> Result: unsupported_polyhedron_indices = [] # Pass the vtk_input_file to the initializer - with multiprocessing.Pool( processes=options.nproc, - initializer=init_worker_mesh, - initargs=(vtk_input_file,) ) as pool: # Comma makes it a tuple - generator = pool.imap_unordered( polyhedron_converter, - range( num_cells ), - chunksize=options.chunk_size ) + with multiprocessing.Pool( processes=options.nproc, initializer=init_worker_mesh, + initargs=( vtk_input_file, ) ) as pool: # Comma makes it a tuple + generator = pool.imap_unordered( polyhedron_converter, range( num_cells ), chunksize=options.chunk_size ) for cell_index_or_neg_one in tqdm( generator, total=num_cells, desc="Testing support for elements" ): if cell_index_or_neg_one != -1: - unsupported_polyhedron_indices.append(cell_index_or_neg_one) + unsupported_polyhedron_indices.append( cell_index_or_neg_one ) return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_indices ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 6e77a4bd..57927080 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -81,7 +81,8 @@ class CheckFeature: SUPPORTED_ELEMENTS, ] DEFAULT_PARAMS: dict[ str, dict[ str, float ] ] = { - name: feature.default_params.copy() for name, feature in CHECK_FEATURES_CONFIG.items() + name: feature.default_params.copy() + for name, feature in CHECK_FEATURES_CONFIG.items() } # --- Argument Parser Constants --- diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py index ce722c22..0f3d697a 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py @@ -38,15 +38,12 @@ def parse_and_set_verbosity( cli_args: List[ str ] ) -> None: '--' + __VERBOSE_KEY, action='count', default=0, # Base default, actual interpretation depends on help text mapping - dest=__VERBOSE_KEY - ) - dummy_verbosity_parser.add_argument( - '-' + __QUIET_FLAG, - '--' + __QUIET_KEY, - action='count', - default=0, - dest=__QUIET_KEY - ) + dest=__VERBOSE_KEY ) + dummy_verbosity_parser.add_argument( '-' + __QUIET_FLAG, + '--' + __QUIET_KEY, + action='count', + default=0, + dest=__QUIET_KEY ) # Parse only known args to extract verbosity/quiet flags # cli_args[1:] is used assuming cli_args[0] is the script name (like sys.argv) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py index e569dd69..40ff7a56 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py @@ -32,7 +32,9 @@ def fill_subparser( subparsers ) -> None: required=False, metavar=__MIN_DISTANCE_DEFAULT, default=__MIN_DISTANCE_DEFAULT, - help=f"[float]: The minimum distance in the computation. Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." ) + help= + f"[float]: The minimum distance in the computation. Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." + ) def display_results( options: Options, result: Result ): diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py index ffdc6d74..cf1c2efa 100644 --- a/geos-mesh/tests/test_all_checks.py +++ b/geos-mesh/tests/test_all_checks.py @@ -141,11 +141,13 @@ def test_action_calls_check_modules( self, mock_check_action ): def test_action_with_multiple_checks( self, mock_check_action ): # Setup mock options with multiple checks check_names = [ ORDERED_CHECK_NAMES[ 0 ], ORDERED_CHECK_NAMES[ 1 ] ] - mock_options = AllChecksOptions( - checks_to_perform=check_names, - checks_options={ name: f"mock_options_{i}" for i, name in enumerate( check_names ) }, - check_displays={ name: MagicMock() for name in check_names } - ) + mock_options = AllChecksOptions( checks_to_perform=check_names, + checks_options={ + name: f"mock_options_{i}" + for i, name in enumerate( check_names ) + }, + check_displays={ name: MagicMock() + for name in check_names } ) # Mock the module loading function with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', return_value=mock_check_action ) as mock_load: From 44d0ba7532244edc8d607c031806ab38cf2dfe4c Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 18:03:12 -0700 Subject: [PATCH 11/20] Remove automatic documentation + yapf --- docs/geos_mesh_docs/doctor.rst | 211 ++++++++++++++++++++++++---- geos-utils/src/geos/utils/Logger.py | 6 +- 2 files changed, 190 insertions(+), 27 deletions(-) diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index 762f20b9..d1a1499e 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -21,14 +21,54 @@ Modules To list all the modules available through ``mesh-doctor``, you can simply use the ``--help`` option, which will list all available modules as well as a quick summary. -.. command-output:: mesh-doctor --help - :shell: +.. code-block:: + + $ mesh-doctor --help + usage: mesh_doctor.py [-h] [-v] [-q] -i VTK_MESH_FILE + {collocated_nodes,element_volumes,fix_elements_orderings,generate_cube,generate_fractures,generate_global_ids,non_conformal,self_intersecting_elements,supported_elements} + ... + Inspects meshes for GEOSX. + positional arguments: + {collocated_nodes,element_volumes,fix_elements_orderings,generate_cube,generate_fractures,generate_global_ids,non_conformal,self_intersecting_elements,supported_elements} + Modules + collocated_nodes + Checks if nodes are collocated. + element_volumes + Checks if the volumes of the elements are greater than "min". + fix_elements_orderings + Reorders the support nodes for the given cell types. + generate_cube + Generate a cube and its fields. + generate_fractures + Splits the mesh to generate the faults and fractures. [EXPERIMENTAL] + generate_global_ids + Adds globals ids for points and cells. + non_conformal + Detects non conformal elements. [EXPERIMENTAL] + self_intersecting_elements + Checks if the faces of the elements are self intersecting. + supported_elements + Check that all the elements of the mesh are supported by GEOSX. + options: + -h, --help + show this help message and exit + -v Use -v 'INFO', -vv for 'DEBUG'. Defaults to 'WARNING'. + -q Use -q to reduce the verbosity of the output. + -i VTK_MESH_FILE, --vtk-input-file VTK_MESH_FILE + Note that checks are dynamically loaded. + An option may be missing because of an unloaded module. + Increase verbosity (-v, -vv) to get full information. Then, if you are interested in a specific module, you can ask for its documentation using the ``mesh-doctor module_name --help`` pattern. For example -.. command-output:: mesh-doctor collocated_nodes --help - :shell: +.. code-block:: + + $ mesh-doctor collocated_nodes --help + usage: mesh_doctor.py collocated_nodes [-h] --tolerance TOLERANCE + options: + -h, --help show this help message and exit + --tolerance TOLERANCE [float]: The absolute distance between two nodes for them to be considered collocated. ``mesh-doctor`` loads its module dynamically. If a module can't be loaded, ``mesh-doctor`` will proceed and try to load other modules. @@ -45,7 +85,7 @@ You can solve this issue by installing the dependencies of ``mesh-doctor`` defin Here is a list and brief description of all the modules available. ``all_checks`` -"""""""""""""""""""" +"""""""""""""" ``mesh-doctor`` modules are called ``actions`` and they can be splitted into 2 different categories: ``check actions`` that will give you a feedback on a .vtu mesh that you would like to use in GEOS. @@ -54,8 +94,23 @@ Here is a list and brief description of all the modules available. ``all_checks`` aims at applying every single ``check`` action in one single command. The list is the following: ``collocated_nodes``, ``element_volumes``, ``non_conformal``, ``self_intersecting_elements``, ``supported_elements``. -.. command-output:: mesh-doctor all_checks --help - :shell: +.. code-block:: + + $ mesh-doctor all_checks --help + usage: mesh-doctor all_checks [-h] [--checks_to_perform CHECKS_TO_PERFORM] [--set_parameters SET_PARAMETERS] + + options: + -h, --help show this help message and exit + --checks_to_perform CHECKS_TO_PERFORM + Comma-separated list of mesh-doctor checks to perform. If no input was given, all of the following checks will be executed by default: + ['collocated_nodes', 'element_volumes', 'non_conformal', 'self_intersecting_elements', 'supported_elements']. + If you want to choose only certain of them, you can name them individually. + Example: --checks_to_perform collocated_nodes,element_volumes (default: ) + --set_parameters SET_PARAMETERS + Comma-separated list of parameters to set for the checks (e.g., 'param_name:value'). These parameters override the defaults. + Default parameters are: For collocated_nodes: tolerance:0.0. For element_volumes: min_volume:0.0. For non_conformal: angle_tolerance:10.0, point_tolerance:0.0, face_tolerance:0.0. + For self_intersecting_elements: min_distance:2.220446049250313e-16. For supported_elements: chunk_size:1, nproc:8. + Example: --set_parameters parameter_name:10.5,other_param:25 (default: ) ``collocated_nodes`` """""""""""""""""""" @@ -63,8 +118,13 @@ Here is a list and brief description of all the modules available. Displays the neighboring nodes that are closer to each other than a prescribed threshold. It is not uncommon to define multiple nodes for the exact same position, which will typically be an issue for ``geos`` and should be fixed. -.. command-output:: mesh-doctor collocated_nodes --help - :shell: +.. code-block:: + + $ mesh-doctor collocated_nodes --help + usage: mesh_doctor.py collocated_nodes [-h] --tolerance TOLERANCE + options: + -h, --help show this help message and exit + --tolerance TOLERANCE [float]: The absolute distance between two nodes for them to be considered collocated. ``element_volumes`` """"""""""""""""""" @@ -72,8 +132,13 @@ It is not uncommon to define multiple nodes for the exact same position, which w Computes the volumes of all the cells and displays the ones that are below a prescribed threshold. Cells with negative volumes will typically be an issue for ``geos`` and should be fixed. -.. command-output:: mesh-doctor element_volumes --help - :shell: +.. code-block:: + + $ mesh-doctor element_volumes --help + usage: mesh_doctor.py element_volumes [-h] --min 0.0 + options: + -h, --help show this help message and exit + --min 0.0 [float]: The minimum acceptable volume. Defaults to 0.0. ``fix_elements_orderings`` """""""""""""""""""""""""" @@ -82,8 +147,28 @@ It sometimes happens that an exported mesh does not abide by the ``vtk`` orderin The ``fix_elements_orderings`` module can rearrange the nodes of given types of elements. This can be convenient if you cannot regenerate the mesh. -.. command-output:: mesh-doctor fix_elements_orderings --help - :shell: +.. code-block:: + + $ mesh-doctor fix_elements_orderings --help + usage: mesh_doctor.py fix_elements_orderings [-h] [--Hexahedron 1,6,5,4,7,0,2,3] [--Prism5 8,2,0,7,6,9,5,1,4,3] + [--Prism6 11,2,8,10,5,0,9,7,6,1,4,3] [--Pyramid 3,4,0,2,1] + [--Tetrahedron 2,0,3,1] [--Voxel 1,6,5,4,7,0,2,3] + [--Wedge 3,5,4,0,2,1] --output OUTPUT [--data-mode binary, ascii] + options: + -h, --help show this help message and exit + --Hexahedron 1,6,5,4,7,0,2,3 + [list of integers]: node permutation for "Hexahedron". + --Prism5 8,2,0,7,6,9,5,1,4,3 + [list of integers]: node permutation for "Prism5". + --Prism6 11,2,8,10,5,0,9,7,6,1,4,3 + [list of integers]: node permutation for "Prism6". + --Pyramid 3,4,0,2,1 [list of integers]: node permutation for "Pyramid". + --Tetrahedron 2,0,3,1 [list of integers]: node permutation for "Tetrahedron". + --Voxel 1,6,5,4,7,0,2,3 [list of integers]: node permutation for "Voxel". + --Wedge 3,5,4,0,2,1 [list of integers]: node permutation for "Wedge". + --output OUTPUT [string]: The vtk output file destination. + --data-mode binary, ascii + [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. ``generate_cube`` """"""""""""""""" @@ -92,8 +177,29 @@ This module conveniently generates cubic meshes in ``vtk``. It can also generate fields with simple values. This tool can also be useful to generate a trial mesh that will later be refined or customized. -.. command-output:: mesh-doctor generate_cube --help - :shell: +.. code-block:: + + $ mesh-doctor generate_cube --help + usage: mesh_doctor.py generate_cube [-h] [--x 0:1.5:3] [--y 0:5:10] [--z 0:1] [--nx 2:2] [--ny 1:1] [--nz 4] + [--fields name:support:dim [name:support:dim ...]] [--cells] [--no-cells] + [--points] [--no-points] --output OUTPUT [--data-mode binary, ascii] + options: + -h, --help show this help message and exit + --x 0:1.5:3 [list of floats]: X coordinates of the points. + --y 0:5:10 [list of floats]: Y coordinates of the points. + --z 0:1 [list of floats]: Z coordinates of the points. + --nx 2:2 [list of integers]: Number of elements in the X direction. + --ny 1:1 [list of integers]: Number of elements in the Y direction. + --nz 4 [list of integers]: Number of elements in the Z direction. + --fields name:support:dim + [name:support:dim ...]: Create fields on CELLS or POINTS, with given dimension (typically 1 or 3). + --cells [bool]: Generate global ids for cells. Defaults to true. + --no-cells [bool]: Don't generate global ids for cells. + --points [bool]: Generate global ids for points. Defaults to true. + --no-points [bool]: Don't generate global ids for points. + --output OUTPUT [string]: The vtk output file destination. + --data-mode binary, ascii + [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. ``generate_fractures`` """""""""""""""""""""" @@ -101,8 +207,29 @@ This tool can also be useful to generate a trial mesh that will later be refined For a conformal fracture to be defined in a mesh, ``geos`` requires the mesh to be split at the faces where the fracture gets across the mesh. The ``generate_fractures`` module will split the mesh and generate the multi-block ``vtk`` files. -.. command-output:: mesh-doctor generate_fractures --help - :shell: +.. code-block:: + + $ mesh-doctor generate_fractures --help + usage: mesh_doctor.py generate_fractures [-h] --policy field, internal_surfaces [--name NAME] [--values VALUES] --output OUTPUT + [--data-mode binary, ascii] [--fractures_output_dir FRACTURES_OUTPUT_DIR] + options: + -h, --help show this help message and exit + --policy field, internal_surfaces + [string]: The criterion to define the surfaces that will be changed into fracture zones. Possible values are "field, internal_surfaces" + --name NAME [string]: If the "field" policy is selected, defines which field will be considered to define the fractures. + If the "internal_surfaces" policy is selected, defines the name of the attribute will be considered to identify the fractures. + --values VALUES [list of comma separated integers]: If the "field" policy is selected, which changes of the field will be considered as a fracture. + If the "internal_surfaces" policy is selected, list of the fracture attributes. + You can create multiple fractures by separating the values with ':' like shown in this example. + --values 10,12:13,14,16,18:22 will create 3 fractures identified respectively with the values (10,12), (13,14,16,18) and (22). + If no ':' is found, all values specified will be assumed to create only 1 single fracture. + --output OUTPUT [string]: The vtk output file destination. + --data-mode binary, ascii + [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. + --fractures_output_dir FRACTURES_OUTPUT_DIR + [string]: The output directory for the fractures meshes that will be generated from the mesh. + --fractures_data_mode FRACTURES_DATA_MODE + [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. ``generate_global_ids`` """"""""""""""""""""""" @@ -110,8 +237,20 @@ The ``generate_fractures`` module will split the mesh and generate the multi-blo When running ``geos`` in parallel, `global ids` can be used to refer to data across multiple ranks. The ``generate_global_ids`` can generate `global ids` for the imported ``vtk`` mesh. -.. command-output:: mesh-doctor generate_global_ids --help - :shell: +.. code-block:: + + $ mesh-doctor generate_global_ids --help + usage: mesh_doctor.py generate_global_ids [-h] [--cells] [--no-cells] [--points] [--no-points] --output OUTPUT + [--data-mode binary, ascii] + options: + -h, --help show this help message and exit + --cells [bool]: Generate global ids for cells. Defaults to true. + --no-cells [bool]: Don't generate global ids for cells. + --points [bool]: Generate global ids for points. Defaults to true. + --no-points [bool]: Don't generate global ids for points. + --output OUTPUT [string]: The vtk output file destination. + --data-mode binary, ascii + [string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary. ``non_conformal`` """"""""""""""""" @@ -121,8 +260,18 @@ This module will detect elements which are close enough (there's a user defined The angle between two faces can also be precribed. This module can be a bit time consuming. -.. command-output:: mesh-doctor non_conformal --help - :shell: +.. code-block:: + + $ mesh-doctor non_conformal --help + usage: mesh_doctor.py non_conformal [-h] [--angle_tolerance 10.0] [--point_tolerance POINT_TOLERANCE] + [--face_tolerance FACE_TOLERANCE] + options: + -h, --help show this help message and exit + --angle_tolerance 10.0 [float]: angle tolerance in degrees. Defaults to 10.0 + --point_tolerance POINT_TOLERANCE + [float]: tolerance for two points to be considered collocated. + --face_tolerance FACE_TOLERANCE + [float]: tolerance for two faces to be considered "touching". ``self_intersecting_elements`` """""""""""""""""""""""""""""" @@ -130,8 +279,14 @@ This module can be a bit time consuming. Some meshes can have cells that auto-intersect. This module will display the elements that have faces intersecting. -.. command-output:: mesh-doctor self_intersecting_elements --help - :shell: +.. code-block:: + + $ mesh-doctor self_intersecting_elements --help + usage: mesh_doctor.py self_intersecting_elements [-h] [--min 2.220446049250313e-16] + options: + -h, --help show this help message and exit + --min 2.220446049250313e-16 + [float]: The tolerance in the computation. Defaults to your machine precision 2.220446049250313e-16. ``supported_elements`` """""""""""""""""""""" @@ -144,5 +299,11 @@ But also prismes up to 11 faces. The ``supported_elements`` check will validate that no unsupported element is included in the input mesh. It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get converted into a supported type of element. -.. command-output:: mesh-doctor supported_elements --help - :shell: \ No newline at end of file +.. code-block:: + + $ mesh-doctor supported_elements --help + usage: mesh_doctor.py supported_elements [-h] [--chunck_size 1] [--nproc 8] + options: + -h, --help show this help message and exit + --chunck_size 1 [int]: Defaults chunk size for parallel processing to 1 + --nproc 8 [int]: Number of threads used for parallel processing. Defaults to your CPU count 8. \ No newline at end of file diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 1dfbadea..2ffbf606 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -69,11 +69,13 @@ class CustomLoggerFormatter( logging.Formatter ): # Pre-compiled formatters for efficiency _compiled_formatters: dict[ int, logging.Formatter ] = { - level: logging.Formatter( fmt ) for level, fmt in FORMATS_PLAIN.items() + level: logging.Formatter( fmt ) + for level, fmt in FORMATS_PLAIN.items() } _compiled_color_formatters: dict[ int, logging.Formatter ] = { - level: logging.Formatter( fmt ) for level, fmt in FORMATS_COLOR.items() + level: logging.Formatter( fmt ) + for level, fmt in FORMATS_COLOR.items() } def __init__( self: Self, use_color=False ): From a8e4a7c9702adfcfa4918c9ec75f5541b4bb0eb6 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 5 Jun 2025 19:11:15 -0700 Subject: [PATCH 12/20] ruff checking --- geos-utils/src/geos/utils/Logger.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 2ffbf606..9cb6eb03 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -78,7 +78,13 @@ class CustomLoggerFormatter( logging.Formatter ): for level, fmt in FORMATS_COLOR.items() } - def __init__( self: Self, use_color=False ): + def __init__( self: Self, use_color: bool = False ) -> None: + """Initialize the log formatter. + + Args: + use_color (bool): If True, use color-coded log formatters. + Defaults to False. + """ if use_color: self.active_formatters = self._compiled_color_formatters else: @@ -128,6 +134,8 @@ def getLogger( title: str, use_color: bool = False ) -> Logger: Args: title (str): Name of the logger. + use_color (bool): If True, configure the logger to output with color. + Defaults to False. Returns: Logger: logger From a502e2350db67aed9aad9e5dc1f3d64aa8603ed7 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 10 Jun 2025 16:14:02 -0700 Subject: [PATCH 13/20] Add RESULTS log level to Logger --- geos-utils/src/geos/utils/Logger.py | 35 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 9cb6eb03..2a45316c 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -11,8 +11,24 @@ Code was modified from """ -# types redefinition to import logging.* from this module -Logger = logging.Logger #: logger type + +# Add the convenience method for the logger +def results( self, message: str, *args: any, **kws: any ) -> None: # noqa: ANN001 + """Logs a message with the custom 'RESULTS' severity level. + + This level is designed for summary information that should always be + visible, regardless of the logger's verbosity setting. + + Args: + self (Self): The logger instance. + message (str): The primary log message, with optional format specifiers + (e.g., "Found %d issues."). + *args: The arguments to be substituted into the `message` string. + **kws: Keyword arguments for special functionality. + """ + if self.isEnabledFor( RESULTS_LEVEL_NUM ): + self._log( RESULTS_LEVEL_NUM, message, args, **kws ) + # Define logging levels at the module level so they are available for the Formatter class DEBUG: int = logging.DEBUG @@ -21,6 +37,12 @@ ERROR: int = logging.ERROR CRITICAL: int = logging.CRITICAL +# Define and register the new level for check results +RESULTS_LEVEL_NUM: int = 60 +RESULTS_LEVEL_NAME: str = "RESULTS" +logging.addLevelName( RESULTS_LEVEL_NUM, RESULTS_LEVEL_NAME ) +logging.Logger.results = results # type: ignore[attr-defined] + class CustomLoggerFormatter( logging.Formatter ): """Custom formatter for the logger. @@ -39,6 +61,7 @@ class CustomLoggerFormatter( logging.Formatter ): logger.addHandler(ch) """ # define color codes + green: str = "\x1b[32;20m" grey: str = "\x1b[38;20m" yellow: str = "\x1b[33;20m" red: str = "\x1b[31;20m" @@ -48,6 +71,7 @@ class CustomLoggerFormatter( logging.Formatter ): # define prefix of log messages format1: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" format2: str = ( "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" ) + format_results: str = "%(name)s - %(levelname)s - %(message)s" #: format for each logger output type with colors FORMATS_COLOR: dict[ int, str ] = { @@ -56,6 +80,7 @@ class CustomLoggerFormatter( logging.Formatter ): WARNING: yellow + format1 + reset, ERROR: red + format1 + reset, CRITICAL: bold_red + format2 + reset, + RESULTS_LEVEL_NUM: green + format_results + reset, } #: format for each logger output type without colors (e.g., for Paraview) @@ -65,6 +90,7 @@ class CustomLoggerFormatter( logging.Formatter ): WARNING: format1, ERROR: format1, CRITICAL: format2, + RESULTS_LEVEL_NUM: format_results, } # Pre-compiled formatters for efficiency @@ -85,6 +111,7 @@ def __init__( self: Self, use_color: bool = False ) -> None: use_color (bool): If True, use color-coded log formatters. Defaults to False. """ + super().__init__() if use_color: self.active_formatters = self._compiled_color_formatters else: @@ -108,7 +135,7 @@ def format( self: Self, record: logging.LogRecord ) -> str: return logging.Formatter().format( record ) -def getLogger( title: str, use_color: bool = False ) -> Logger: +def getLogger( title: str, use_color: bool = False ) -> logging.Logger: """Return the Logger with pre-defined configuration. This function is now idempotent regarding handler addition. @@ -140,7 +167,7 @@ def getLogger( title: str, use_color: bool = False ) -> Logger: Returns: Logger: logger """ - logger: Logger = logging.getLogger( title ) + logger = logging.getLogger( title ) # Only configure the logger (add handlers, set level) if it hasn't been configured before. if not logger.hasHandlers(): # More Pythonic way to check if logger.handlers is empty logger.setLevel( INFO ) # Set the desired default level for this logger From c05762f73f55d2fec7ddf40f125210b71c876b12 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 10 Jun 2025 16:18:32 -0700 Subject: [PATCH 14/20] Replace logger by setup_logger + use of new "results" attribute --- .../geos/mesh/doctor/actions/all_checks.py | 6 ++--- .../mesh/doctor/actions/check_fractures.py | 10 +++---- .../mesh/doctor/actions/collocated_nodes.py | 6 ++--- .../mesh/doctor/actions/element_volumes.py | 6 ++--- .../geos/mesh/doctor/actions/generate_cube.py | 6 ++--- .../mesh/doctor/actions/generate_fractures.py | 27 +++++++++---------- .../doctor/actions/generate_global_ids.py | 10 +++---- .../geos/mesh/doctor/actions/reorient_mesh.py | 6 ++--- .../mesh/doctor/actions/supported_elements.py | 16 +++++------ geos-mesh/src/geos/mesh/doctor/mesh_doctor.py | 2 +- .../mesh/doctor/parsing/all_checks_parsing.py | 27 +++++++++---------- .../geos/mesh/doctor/parsing/cli_parsing.py | 5 ++-- .../parsing/collocated_nodes_parsing.py | 18 ++++++------- .../doctor/parsing/element_volumes_parsing.py | 10 +++---- .../parsing/fix_elements_orderings_parsing.py | 15 +++++------ .../doctor/parsing/generate_cube_parsing.py | 6 ++--- .../parsing/generate_global_ids_parsing.py | 6 ++--- .../doctor/parsing/non_conformal_parsing.py | 6 ++--- .../self_intersecting_elements_parsing.py | 11 ++++---- .../parsing/supported_elements_parsing.py | 14 +++++----- .../mesh/doctor/parsing/vtk_output_parsing.py | 6 ++--- geos-mesh/tests/test_all_checks.py | 14 +++++----- 22 files changed, 102 insertions(+), 131 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py index 091e4395..253165d9 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from geos.mesh.doctor.register import __load_module_action -from geos.utils.Logger import getLogger - -logger = getLogger( "All_checks" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger @dataclass( frozen=True ) @@ -21,7 +19,7 @@ def action( vtk_input_file: str, options: Options ) -> list[ Result ]: check_results: dict[ str, any ] = dict() for check_name in options.checks_to_perform: check_action = __load_module_action( check_name ) - logger.info( f"Performing check '{check_name}'." ) + setup_logger.info( f"Performing check '{check_name}'." ) option = options.checks_options[ check_name ] check_result = check_action( vtk_input_file, option ) check_results[ check_name ] = check_result diff --git a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py index ce6fcd6a..17d3f893 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/check_fractures.py @@ -7,10 +7,8 @@ from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.util.numpy_support import vtk_to_numpy from geos.mesh.doctor.actions.generate_fractures import Coordinates3D +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.utils.genericHelpers import vtk_iter -from geos.utils.Logger import getLogger - -logger = getLogger( "check_fractures" ) @dataclass( frozen=True ) @@ -115,8 +113,8 @@ def __check_neighbors( matrix: vtkUnstructuredGrid, fracture: vtkUnstructuredGri if f in fracture_faces: found += 1 if found != 2: - logger.warning( f"Something went wrong since we should have found 2 fractures faces (we found {found})" + - f" for collocated nodes {cns}." ) + setup_logger.warning( "Something went wrong since we should have found 2 fractures faces (we found" + + f" {found}) for collocated nodes {cns}." ) def __action( vtk_input_file: str, options: Options ) -> Result: @@ -154,5 +152,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: try: return __action( vtk_input_file, options ) except BaseException as e: - logger.error( e ) + setup_logger.error( e ) return Result( errors=() ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py index 5f63dbbf..4881a1d3 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py @@ -4,10 +4,8 @@ from typing import Collection, Iterable from vtkmodules.vtkCommonCore import reference, vtkPoints from vtkmodules.vtkCommonDataModel import vtkIncrementalOctreePointLocator +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_mesh -from geos.utils.Logger import getLogger - -logger = getLogger( "collocated_nodes" ) @dataclass( frozen=True ) @@ -40,7 +38,7 @@ def __action( mesh, options: Options ) -> Result: # If it's not inserted, `point_id` contains the node that was already at that location. # But in that case, `point_id` is the new numbering in the destination points array. # It's more useful for the user to get the old index in the original mesh, so he can look for it in his data. - logger.debug( + setup_logger.debug( f"Point {i} at {points.GetPoint(i)} has been rejected, point {filtered_to_original[point_id.get()]} is already inserted." ) rejected_points[ point_id.get() ].append( i ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py index 20a16f27..0bf5859b 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py @@ -4,10 +4,8 @@ from vtkmodules.vtkCommonDataModel import VTK_HEXAHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_WEDGE from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter, vtkMeshQuality from vtkmodules.util.numpy_support import vtk_to_numpy +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_mesh -from geos.utils.Logger import getLogger - -logger = getLogger( "element_volumes" ) @dataclass( frozen=True ) @@ -45,7 +43,7 @@ def __action( mesh, options: Options ) -> Result: mq.SetWedgeQualityMeasureToVolume() SUPPORTED_TYPES.append( VTK_WEDGE ) else: - logger.warning( + setup_logger.warning( "Your \"pyvtk\" version does not bring pyramid nor wedge support with vtkMeshQuality. Using the fallback solution." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py index 2edca5fe..f30d2089 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -6,10 +6,8 @@ from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkHexahedron, vtkRectilinearGrid, vtkUnstructuredGrid, VTK_HEXAHEDRON ) from geos.mesh.doctor.actions.generate_global_ids import __build_global_ids +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh -from geos.utils.Logger import getLogger - -logger = getLogger( "generate_cube" ) @dataclass( frozen=True ) @@ -144,5 +142,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: try: return __action( options ) except BaseException as e: - logger.error( e ) + setup_logger.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py index a755b7e8..32f809db 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py @@ -12,15 +12,14 @@ from vtkmodules.util.numpy_support import numpy_to_vtk, vtk_to_numpy from vtkmodules.util.vtkConstants import VTK_ID_TYPE from geos.mesh.doctor.actions.vtk_polyhedron import FaceStream +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.utils.arrayHelpers import has_array from geos.mesh.utils.genericHelpers import to_vtk_id_list, vtk_iter from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh -from geos.utils.Logger import getLogger """ TypeAliases cannot be used with Python 3.9. A simple assignment like described there will be used: https://docs.python.org/3/library/typing.html#typing.TypeAlias:~:text=through%20simple%20assignment%3A-,Vector%20%3D%20list%5Bfloat%5D,-Or%20marked%20with """ -logger = getLogger( "generate_fractures" ) IDMapping = Mapping[ int, int ] CellsPointsCoords = dict[ int, list[ tuple[ float ] ] ] @@ -255,7 +254,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v input_cell_data = old_mesh.GetCellData() for i in range( input_cell_data.GetNumberOfArrays() ): input_array: vtkDataArray = input_cell_data.GetArray( i ) - logger.info( f"Copying cell field \"{input_array.GetName()}\"." ) + setup_logger.info( f"Copying cell field \"{input_array.GetName()}\"." ) tmp = input_array.NewInstance() tmp.DeepCopy( input_array ) splitted_mesh.GetCellData().AddArray( input_array ) @@ -264,7 +263,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v input_field_data = old_mesh.GetFieldData() for i in range( input_field_data.GetNumberOfArrays() ): input_array = input_field_data.GetArray( i ) - logger.info( f"Copying field data \"{input_array.GetName()}\"." ) + setup_logger.info( f"Copying field data \"{input_array.GetName()}\"." ) tmp = input_array.NewInstance() tmp.DeepCopy( input_array ) splitted_mesh.GetFieldData().AddArray( input_array ) @@ -275,7 +274,7 @@ def __copy_fields_splitted_mesh( old_mesh: vtkUnstructuredGrid, splitted_mesh: v for i in range( input_point_data.GetNumberOfArrays() ): old_points_array = vtk_to_numpy( input_point_data.GetArray( i ) ) name: str = input_point_data.GetArrayName( i ) - logger.info( f"Copying point data \"{name}\"." ) + setup_logger.info( f"Copying point data \"{name}\"." ) old_nrows: int = old_points_array.shape[ 0 ] old_ncols: int = 1 if len( old_points_array.shape ) == 1 else old_points_array.shape[ 1 ] # Reshape old_points_array if it is 1-dimensional @@ -314,7 +313,7 @@ def __copy_fields_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_mesh: v if len( old_cells_array.shape ) == 1: old_cells_array = old_cells_array.reshape( ( old_nrows, 1 ) ) name: str = input_cell_data.GetArrayName( i ) - logger.info( f"Copying cell data \"{name}\"." ) + setup_logger.info( f"Copying cell data \"{name}\"." ) new_array = old_cells_array[ face_cell_id, : ] # Reshape the VTK array to match the original dimensions old_ncols: int = 1 if len( old_cells_array.shape ) == 1 else old_cells_array.shape[ 1 ] @@ -335,7 +334,7 @@ def __copy_fields_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_mesh: v if len( old_points_array.shape ) == 1: old_points_array = old_points_array.reshape( ( old_nrows, 1 ) ) name = input_point_data.GetArrayName( i ) - logger.info( f"Copying point data \"{name}\"." ) + setup_logger.info( f"Copying point data \"{name}\"." ) new_array = old_points_array[ list( node_3d_to_node_2d.keys() ), : ] old_ncols = 1 if len( old_points_array.shape ) == 1 else old_points_array.shape[ 1 ] if old_ncols > 1: @@ -434,7 +433,7 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac :param cell_to_node_mapping: For each cell, gives the nodes that must be duplicated and their new index. :return: The fracture mesh. """ - logger.info( "Generating the meshes" ) + setup_logger.info( "Generating the meshes" ) mesh_points: vtkPoints = old_mesh.GetPoints() is_node_duplicated = zeros( mesh_points.GetNumberOfPoints(), dtype=bool ) # defaults to False @@ -467,12 +466,12 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac # for dfns in discarded_face_nodes: # tmp.append(", ".join(map(str, dfns))) msg: str = "(" + '), ('.join( map( lambda dfns: ", ".join( map( str, dfns ) ), discarded_face_nodes ) ) + ")" - # logger.info(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" - # + "from the fracture mesh because none of their/its nodes were duplicated.") + # setup_logger.info(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" + # + "from the fracture mesh because none of their/its nodes were duplicated.") # print(f"The {len(tmp)} faces made of nodes ({'), ('.join(tmp)}) were/was discarded" # + "from the fracture mesh because none of their/its nodes were duplicated.") - logger.info( f"The faces made of nodes [{msg}] were/was discarded" + - "from the fracture mesh because none of their/its nodes were duplicated." ) + setup_logger.info( f"The faces made of nodes [{msg}] were/was discarded" + + "from the fracture mesh because none of their/its nodes were duplicated." ) fracture_nodes_tmp = ones( mesh_points.GetNumberOfPoints(), dtype=int ) * -1 for ns in face_nodes: @@ -563,9 +562,9 @@ def action( vtk_input_file: str, options: Options ) -> Result: if has_array( mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): err_msg: str = ( "The mesh cannot contain global ids for neither cells nor points. The correct procedure " + " is to split the mesh and then generate global ids for new split meshes." ) - logger.error( err_msg ) + setup_logger.error( err_msg ) raise ValueError( err_msg ) return __action( mesh, options ) except BaseException as e: - logger.error( e ) + setup_logger.error( e ) return Result( info="Something went wrong" ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py index 97b88339..f4df1871 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_global_ids.py @@ -1,9 +1,7 @@ from dataclasses import dataclass from vtkmodules.vtkCommonCore import vtkIdTypeArray +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh -from geos.utils.Logger import getLogger - -logger = getLogger( "generate_global_ids" ) @dataclass( frozen=True ) @@ -27,7 +25,7 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g # Building GLOBAL_IDS for points and cells.g GLOBAL_IDS for points and cells. # First for points... if mesh.GetPointData().GetGlobalIds(): - logger.error( "Mesh already has globals ids for points; nothing done." ) + setup_logger.error( "Mesh already has globals ids for points; nothing done." ) elif generate_points_global_ids: point_global_ids = vtkIdTypeArray() point_global_ids.SetName( "GLOBAL_IDS_POINTS" ) @@ -37,7 +35,7 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g mesh.GetPointData().SetGlobalIds( point_global_ids ) # ... then for cells. if mesh.GetCellData().GetGlobalIds(): - logger.error( "Mesh already has globals ids for cells; nothing done." ) + setup_logger.error( "Mesh already has globals ids for cells; nothing done." ) elif generate_cells_global_ids: cells_global_ids = vtkIdTypeArray() cells_global_ids.SetName( "GLOBAL_IDS_CELLS" ) @@ -58,5 +56,5 @@ def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) return __action( mesh, options ) except BaseException as e: - logger.error( e ) + setup_logger.error( e ) return Result( info="Something went wrong." ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py b/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py index 5f32c94c..7b10d313 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/reorient_mesh.py @@ -7,10 +7,8 @@ vtkUnstructuredGrid, vtkTetra ) from vtkmodules.vtkFiltersCore import vtkTriangleFilter from geos.mesh.doctor.actions.vtk_polyhedron import FaceStream, build_face_to_face_connectivity_through_edges +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.utils.genericHelpers import to_vtk_id_list -from geos.utils.Logger import getLogger - -logger = getLogger( "reorient_mesh" ) def __compute_volume( mesh_points: vtkPoints, face_stream: FaceStream ) -> float: @@ -131,7 +129,7 @@ def reorient_mesh( mesh, cell_indices: Iterator[ int ] ) -> vtkUnstructuredGrid: # I did not manage to call `output_mesh.CopyStructure(mesh)` because I could not modify the polyhedron in place. # Therefore, I insert the cells one by one... output_mesh.SetPoints( mesh.GetPoints() ) - logger.info( "Reorienting the polyhedron cells to enforce normals directed outward." ) + setup_logger.info( "Reorienting the polyhedron cells to enforce normals directed outward." ) with tqdm( total=needs_to_be_reoriented.sum(), desc="Reorienting polyhedra" ) as progress_bar: # For smoother progress, we only update on reoriented elements. for ic in range( num_cells ): diff --git a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py index 0fa8fc63..8d9fd46a 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -9,11 +9,9 @@ VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, VTK_WEDGE ) from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream -from geos.mesh.utils.genericHelpers import vtk_iter +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_mesh -from geos.utils.Logger import getLogger - -logger = getLogger( "supported_elements" ) +from geos.mesh.utils.genericHelpers import vtk_iter @dataclass( frozen=True ) @@ -40,12 +38,12 @@ def init_worker_mesh( input_file_for_worker: str ): input_file_for_worker (str): Filepath to vtk grid """ global MESH - logger.debug( + setup_logger.debug( f"Worker process (PID: {multiprocessing.current_process().pid}) initializing MESH from file: {input_file_for_worker}" ) MESH = read_mesh( input_file_for_worker ) if MESH is None: - logger.error( + setup_logger.error( f"Worker process (PID: {multiprocessing.current_process().pid}) failed to load mesh from {input_file_for_worker}" ) # You might want to raise an error here or ensure MESH being None is handled downstream @@ -127,10 +125,10 @@ def __call__( self, ic: int ) -> int: face_stream = FaceStream.build_from_vtk_id_list( pt_ids ) converted_type_name = self.__is_polyhedron_supported( face_stream ) if converted_type_name: - logger.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) + setup_logger.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) return -1 else: - logger.debug( + setup_logger.debug( f"Polyhedron cell {ic} (in PID {multiprocessing.current_process().pid}) cannot be converted into any supported element." ) return ic @@ -140,7 +138,7 @@ def __action( vtk_input_file: str, options: Options ) -> Result: # Main process loads the mesh for its own use mesh = read_mesh( vtk_input_file ) if mesh is None: - logger.error( f"Main process failed to load mesh from {vtk_input_file}. Aborting." ) + setup_logger.error( f"Main process failed to load mesh from {vtk_input_file}. Aborting." ) # Return an empty/error result or raise an exception return Result( unsupported_std_elements_types=frozenset(), unsupported_polyhedron_elements=frozenset() ) diff --git a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py index ec4218a9..0144c232 100644 --- a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py +++ b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py @@ -21,7 +21,7 @@ def main(): try: action = all_actions[ args.subparsers ] except KeyError: - setup_logger.critical( f"Action {args.subparsers} is not a valid action." ) + setup_logger.error( f"Action {args.subparsers} is not a valid action." ) sys.exit( 1 ) helper: ActionHelper = all_actions_helpers[ args.subparsers ] result = action( args.vtk_input_file, action_options ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 57927080..b913e425 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -20,10 +20,7 @@ from geos.mesh.doctor.parsing import non_conformal_parsing as nc_parser from geos.mesh.doctor.parsing import self_intersecting_elements_parsing as sie_parser from geos.mesh.doctor.parsing import supported_elements_parsing as se_parser -from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string -from geos.utils.Logger import getLogger - -logger = getLogger( "All_checks_parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string, setup_logger # --- Centralized Configuration for Check Features --- # This structure makes it easier to manage checks and their properties. @@ -133,20 +130,20 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: # 1. Determine which checks to perform final_selected_check_names: list[ str ] = deepcopy( ORDERED_CHECK_NAMES ) if not parsed_args[ CHECKS_TO_DO_ARG ]: # handles default and if user explicitly provides --checks_to_perform "" - logger.info( "All current available checks in mesh-doctor will be performed." ) + setup_logger.info( "All current available checks in mesh-doctor will be performed." ) else: # the user specifically entered check names to perform checks_to_do: list[ str ] = parse_comma_separated_string( parsed_args[ CHECKS_TO_DO_ARG ] ) final_selected_check_names = list() for name in checks_to_do: if name not in CHECK_FEATURES_CONFIG: - logger.warning( f"The given check '{name}' does not exist. Cannot perform this check." - f" Choose from: {ORDERED_CHECK_NAMES}." ) + setup_logger.warning( f"The given check '{name}' does not exist. Cannot perform this check." + f" Choose from: {ORDERED_CHECK_NAMES}." ) elif name not in final_selected_check_names: # Add if valid and not already added final_selected_check_names.append( name ) # If after parsing, no valid checks are selected (e.g., all inputs were invalid) if not final_selected_check_names: - logger.error( "No valid checks selected based on input. No operations will be configured." ) + setup_logger.error( "No valid checks selected based on input. No operations will be configured." ) raise ValueError( "No valid checks selected based on input. No operations will be configured." ) # 2. Prepare parameters of Options for every check feature that will be used @@ -156,12 +153,13 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: del final_selected_check_params[ name ] # Remove non-used check features if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" - logger.info( "Default configuation of parameters adopted for every check to perform." ) + setup_logger.info( "Default configuation of parameters adopted for every check to perform." ) else: set_parameters = parse_comma_separated_string( parsed_args[ PARAMETERS_ARG ] ) for param in set_parameters: if ':' not in param: - logger.warning( f"Parameter '{param}' in --{PARAMETERS_ARG} is not in 'name:value' format. Skipping." ) + setup_logger.warning( + f"Parameter '{param}' in --{PARAMETERS_ARG} is not in 'name:value' format. Skipping." ) continue name, *value = param.split( ':', 1 ) name = name.strip() @@ -169,12 +167,12 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: value_str = value[ 0 ].strip() else: # Handle cases where there's nothing after the colon, if necessary - logger.warning( f"Parameter '{name}' has no value after the colon. Skipping or using default." ) + setup_logger.warning( f"Parameter '{name}' has no value after the colon. Skipping or using default." ) continue try: value_float = float( value_str ) except ValueError: - logger.warning( + setup_logger.warning( f"Invalid value for parameter '{name}': '{value_str}'. Must be a number. Skipping this override." ) continue @@ -193,7 +191,7 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: individual_check_options[ check_name ] = feature_config.options_cls( **options_constructor_params ) individual_check_display[ check_name ] = feature_config.display except Exception as e: # Catch potential errors during options instantiation - logger.error( + setup_logger.error( f"Failed to create options for check '{check_name}' with params {options_constructor_params}: {e}." f" Therefore the check '{check_name}' will not be performed." ) final_selected_check_names.remove( check_name ) @@ -206,7 +204,8 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: # --- Display Results --- def display_results( options: AllChecksOptions, result: AllChecksResult ) -> None: """Displays the results of the checks.""" + max_length = max( len( name ) for name in options.checks_to_perform ) # Implementation for displaying results based on the structured options and results. - logger.info( f"Displaying results for checks: {options.checks_to_perform}" ) for name, res in result.check_results.items(): + setup_logger.results( f"******** {name:<{max_length}} ********" ) options.check_displays[ name ]( options.checks_options[ name ], res ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py index 0f3d697a..e7cd6348 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/cli_parsing.py @@ -2,7 +2,7 @@ import logging import textwrap from typing import List -from geos.utils.Logger import getLogger as get_custom_logger # Alias for clarity +from geos.utils.Logger import getLogger # Alias for clarity __VERBOSE_KEY = "verbose" __QUIET_KEY = "quiet" @@ -12,7 +12,8 @@ # Get a logger for this setup module itself, using your custom logger # This ensures its messages (like the "Logger level set to...") use your custom format. -setup_logger = get_custom_logger( "mesh-doctor" ) +setup_logger = getLogger( "mesh-doctor" ) +setup_logger.propagate = False # --- Conversion Logic --- diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py index 17ddc6a2..2f9b7baf 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py @@ -1,8 +1,6 @@ from geos.mesh.doctor.actions.collocated_nodes import Options, Result from geos.mesh.doctor.parsing import COLLOCATES_NODES -from geos.utils.Logger import getLogger - -logger = getLogger( "Collocated_nodes parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __TOLERANCE = "tolerance" __TOLERANCE_DEFAULT = 0. @@ -31,18 +29,20 @@ def display_results( options: Options, result: Result ): all_collocated_nodes.append( node ) all_collocated_nodes: frozenset[ int ] = frozenset( all_collocated_nodes ) # Surely useless if all_collocated_nodes: - logger.error( f"You have {len(all_collocated_nodes)} collocated nodes (tolerance = {options.tolerance})." ) + setup_logger.results( + f"You have {len(all_collocated_nodes)} collocated nodes (tolerance = {options.tolerance})." ) - logger.info( "Here are all the buckets of collocated nodes." ) + setup_logger.info( "Here are all the buckets of collocated nodes." ) tmp: list[ str ] = [] for bucket in result.nodes_buckets: tmp.append( f"({', '.join(map(str, bucket))})" ) - logger.info( f"({', '.join(tmp)})" ) + setup_logger.info( f"({', '.join(tmp)})" ) else: - logger.error( f"You have no collocated node (tolerance = {options.tolerance})." ) + setup_logger.results( f"You have no collocated node (tolerance = {options.tolerance})." ) if result.wrong_support_elements: tmp: str = ", ".join( map( str, result.wrong_support_elements ) ) - logger.error( f"You have {len(result.wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) + setup_logger.results( + f"You have {len(result.wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) else: - logger.error( "You have no element with duplicated support nodes." ) + setup_logger.results( "You have no element with duplicated support nodes." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py index 2292226b..959ae093 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py @@ -1,8 +1,6 @@ from geos.mesh.doctor.actions.element_volumes import Options, Result from geos.mesh.doctor.parsing import ELEMENT_VOLUMES -from geos.utils.Logger import getLogger - -logger = getLogger( "element_volumes parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __MIN_VOLUME = "min_volume" __MIN_VOLUME_DEFAULT = 0. @@ -31,6 +29,8 @@ def convert( parsed_options ) -> Options: def display_results( options: Options, result: Result ): - logger.error( f"You have {len(result.element_volumes)} elements with volumes smaller than {options.min_volume}." ) + setup_logger.results( + f"You have {len(result.element_volumes)} elements with volumes smaller than {options.min_volume}." ) if result.element_volumes: - logger.error( "The elements indices and their volumes are:\n\n".join( map( str, result.element_volumes ) ) ) + setup_logger.results( "The elements indices and their volumes are:\n\n".join( map( + str, result.element_volumes ) ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py index 8bfa5fed..76a6e3e7 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/fix_elements_orderings_parsing.py @@ -10,9 +10,7 @@ ) from geos.mesh.doctor.actions.fix_elements_orderings import Options, Result from geos.mesh.doctor.parsing import vtk_output_parsing, FIX_ELEMENTS_ORDERINGS -from geos.utils.Logger import getLogger - -logger = getLogger( "fix_elements_orderings parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __CELL_TYPE_MAPPING = { "Hexahedron": VTK_HEXAHEDRON, @@ -62,7 +60,7 @@ def convert( parsed_options ) -> Options: tmp = tuple( map( int, raw_mapping.split( "," ) ) ) if not set( tmp ) == set( range( __CELL_TYPE_SUPPORT_SIZE[ vtk_key ] ) ): err_msg = f"Permutation {raw_mapping} for type {key} is not valid." - logger.error( err_msg ) + setup_logger.error( err_msg ) raise ValueError( err_msg ) cell_type_to_ordering[ vtk_key ] = tmp vtk_output = vtk_output_parsing.convert( parsed_options ) @@ -71,10 +69,11 @@ def convert( parsed_options ) -> Options: def display_results( options: Options, result: Result ): if result.output: - logger.info( f"New mesh was written to file '{result.output}'" ) + setup_logger.info( f"New mesh was written to file '{result.output}'" ) if result.unchanged_cell_types: - logger.info( f"Those vtk types were not reordered: [{', '.join(map(str, result.unchanged_cell_types))}]." ) + setup_logger.info( + f"Those vtk types were not reordered: [{', '.join(map(str, result.unchanged_cell_types))}]." ) else: - logger.info( "All the cells of the mesh were reordered." ) + setup_logger.info( "All the cells of the mesh were reordered." ) else: - logger.info( "No output file was written." ) + setup_logger.info( "No output file was written." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py index b83b1f39..c717e99a 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/generate_cube_parsing.py @@ -1,9 +1,7 @@ from geos.mesh.doctor.actions.generate_cube import Options, Result, FieldInfo from geos.mesh.doctor.parsing import vtk_output_parsing, generate_global_ids_parsing, GENERATE_CUBE +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.doctor.parsing.generate_global_ids_parsing import GlobalIdsInfo -from geos.utils.Logger import getLogger - -logger = getLogger( "generate_cube parsing" ) __X, __Y, __Z, __NX, __NY, __NZ = "x", "y", "z", "nx", "ny", "nz" __FIELDS = "fields" @@ -84,4 +82,4 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - logger.info( result.info ) + setup_logger.info( result.info ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py index 5902a403..2c1a09cd 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/generate_global_ids_parsing.py @@ -1,9 +1,7 @@ from dataclasses import dataclass from geos.mesh.doctor.actions.generate_global_ids import Options, Result from geos.mesh.doctor.parsing import vtk_output_parsing, GENERATE_GLOBAL_IDS -from geos.utils.Logger import getLogger - -logger = getLogger( "generate_global_ids parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __CELLS, __POINTS = "cells", "points" @@ -51,4 +49,4 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - logger.info( result.info ) + setup_logger.info( result.info ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py index bdc327a4..6e63f1de 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py @@ -1,8 +1,6 @@ from geos.mesh.doctor.actions.non_conformal import Options, Result from geos.mesh.doctor.parsing import NON_CONFORMAL -from geos.utils.Logger import getLogger - -logger = getLogger( "non_conformal parsing" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __ANGLE_TOLERANCE = "angle_tolerance" __POINT_TOLERANCE = "point_tolerance" @@ -51,6 +49,6 @@ def display_results( options: Options, result: Result ): for i, j in result.non_conformal_cells: non_conformal_cells += i, j non_conformal_cells: frozenset[ int ] = frozenset( non_conformal_cells ) - logger.error( + setup_logger.results( f"You have {len(non_conformal_cells)} non conformal cells.\n{', '.join(map(str, sorted(non_conformal_cells)))}" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py index 40ff7a56..558293c0 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py @@ -1,9 +1,7 @@ import numpy from geos.mesh.doctor.actions.self_intersecting_elements import Options, Result from geos.mesh.doctor.parsing import SELF_INTERSECTING_ELEMENTS -from geos.utils.Logger import getLogger - -logger = getLogger( "self_intersecting_elements" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __MIN_DISTANCE = "min_distance" __MIN_DISTANCE_DEFAULT = numpy.finfo( float ).eps @@ -14,7 +12,7 @@ def convert( parsed_options ) -> Options: min_distance = parsed_options[ __MIN_DISTANCE ] if min_distance == 0: - logger.warning( + setup_logger.warning( "Having minimum distance set to 0 can induce lots of false positive results (adjacent faces may be considered intersecting)." ) elif min_distance < 0: @@ -38,6 +36,7 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): - logger.error( f"You have {len(result.intersecting_faces_elements)} elements with self intersecting faces." ) + setup_logger.results( f"You have {len(result.intersecting_faces_elements)} elements with self intersecting faces." ) if result.intersecting_faces_elements: - logger.error( "The elements indices are:\n" + ", ".join( map( str, result.intersecting_faces_elements ) ) ) + setup_logger.results( "The elements indices are:\n" + + ", ".join( map( str, result.intersecting_faces_elements ) ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py index e39032a7..7142f176 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py @@ -1,9 +1,7 @@ import multiprocessing from geos.mesh.doctor.actions.supported_elements import Options, Result from geos.mesh.doctor.parsing import SUPPORTED_ELEMENTS -from geos.utils.Logger import getLogger - -logger = getLogger( "supported_elements" ) +from geos.mesh.doctor.parsing.cli_parsing import setup_logger __CHUNK_SIZE = "chunk_size" __NUM_PROC = "nproc" @@ -39,16 +37,16 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): if result.unsupported_polyhedron_elements: - logger.error( + setup_logger.results( f"There is/are {len(result.unsupported_polyhedron_elements)} polyhedra that may not be converted to supported elements." ) - logger.error( + setup_logger.results( f"The list of the unsupported polyhedra is\n{tuple(sorted(result.unsupported_polyhedron_elements))}." ) else: - logger.info( "All the polyhedra (if any) can be converted to supported elements." ) + setup_logger.results( "All the polyhedra (if any) can be converted to supported elements." ) if result.unsupported_std_elements_types: - logger.error( + setup_logger.results( f"There are unsupported vtk standard element types. The list of those vtk types is {tuple(sorted(result.unsupported_std_elements_types))}." ) else: - logger.info( "All the standard vtk element types (if any) are supported." ) + setup_logger.results( "All the standard vtk element types (if any) are supported." ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py index 5d5a9416..06fedd0c 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/vtk_output_parsing.py @@ -1,9 +1,7 @@ import os.path import textwrap +from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput -from geos.utils.Logger import getLogger - -logger = getLogger( "vtk_output_parsing" ) __OUTPUT_FILE = "output" __OUTPUT_BINARY_MODE = "data-mode" @@ -42,6 +40,6 @@ def convert( parsed_options, prefix="" ) -> VtkOutput: binary_mode_key = __build_arg( prefix, __OUTPUT_BINARY_MODE ).replace( "-", "_" ) output = parsed_options[ output_key ] if parsed_options[ binary_mode_key ] and os.path.splitext( output )[ -1 ] == ".vtk": - logger.info( "VTK data mode will be ignored for legacy file format \"vtk\"." ) + setup_logger.info( "VTK data mode will be ignored for legacy file format \"vtk\"." ) is_data_mode_binary: bool = parsed_options[ binary_mode_key ] == __OUTPUT_BINARY_MODE_DEFAULT return VtkOutput( output=output, is_data_mode_binary=is_data_mode_binary ) diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py index cf1c2efa..5432fc6e 100644 --- a/geos-mesh/tests/test_all_checks.py +++ b/geos-mesh/tests/test_all_checks.py @@ -49,7 +49,7 @@ def test_fill_subparser( self, mock_parser ): def test_convert_with_default_checks( self ): # Test with empty string for checks_to_perform (should use all checks) args = { "checks_to_perform": "", "set_parameters": "" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ) as mock_logger: + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: options = convert( args ) # Should log that all checks will be performed @@ -63,7 +63,7 @@ def test_convert_with_default_checks( self ): assert check_name in options.checks_options def test_convert_with_specific_checks( self, mock_args ): - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): options = convert( mock_args ) # Should only include the specified checks @@ -75,7 +75,7 @@ def test_convert_with_specific_checks( self, mock_args ): def test_convert_with_invalid_check( self ): args = { "checks_to_perform": "invalid_check_name", "set_parameters": "" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ) as mock_logger: + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: with pytest.raises( ValueError, match="No valid checks selected" ): convert( args ) @@ -87,7 +87,7 @@ def test_convert_with_parameter_override( self ): check_name = "collocated_nodes" param_name = next( iter( CHECK_FEATURES_CONFIG[ check_name ].default_params.keys() ) ) args = { "checks_to_perform": check_name, "set_parameters": f"{param_name}:99.9" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): options = convert( args ) # Get the options object for the check @@ -106,7 +106,7 @@ def test_display_results( self ): checks_options={ check_name: "mock_options" }, check_displays={ check_name: mock_display_func } ) result = AllChecksResult( check_results={ check_name: "mock_result" } ) - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.logger' ): + with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): display_results( options, result ) # Verify display function was called with correct arguments @@ -125,7 +125,7 @@ def test_action_calls_check_modules( self, mock_check_action ): # Mock the module loading function with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', return_value=mock_check_action ) as mock_load: - with patch( 'geos.mesh.doctor.actions.all_checks.logger' ): + with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): result = action( "test_file.vtk", mock_options ) # Verify the module was loaded @@ -151,7 +151,7 @@ def test_action_with_multiple_checks( self, mock_check_action ): # Mock the module loading function with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', return_value=mock_check_action ) as mock_load: - with patch( 'geos.mesh.doctor.actions.all_checks.logger' ): + with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): result = action( "test_file.vtk", mock_options ) # Verify the modules were loaded From 9c9efb95c7716eb001a0be139749cde1edc19e4e Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 10 Jun 2025 17:25:27 -0700 Subject: [PATCH 15/20] typing and restore constant name Logger --- geos-utils/src/geos/utils/Logger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 2a45316c..dcedee0f 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. # SPDX-FileContributor: Martin Lemay import logging -from typing import Union +from typing import Any, Union from typing_extensions import Self __doc__ = """ @@ -13,7 +13,7 @@ # Add the convenience method for the logger -def results( self, message: str, *args: any, **kws: any ) -> None: # noqa: ANN001 +def results( self, message: str, *args: Any, **kws: Any ) -> None: # noqa: ANN001 """Logs a message with the custom 'RESULTS' severity level. This level is designed for summary information that should always be @@ -42,6 +42,8 @@ def results( self, message: str, *args: any, **kws: any ) -> None: # noqa: ANN0 RESULTS_LEVEL_NAME: str = "RESULTS" logging.addLevelName( RESULTS_LEVEL_NUM, RESULTS_LEVEL_NAME ) logging.Logger.results = results # type: ignore[attr-defined] +# types redefinition to import logging.* from this module +Logger = logging.Logger #: logger type class CustomLoggerFormatter( logging.Formatter ): @@ -135,7 +137,7 @@ def format( self: Self, record: logging.LogRecord ) -> str: return logging.Formatter().format( record ) -def getLogger( title: str, use_color: bool = False ) -> logging.Logger: +def getLogger( title: str, use_color: bool = False ) -> Logger: """Return the Logger with pre-defined configuration. This function is now idempotent regarding handler addition. From 2dba83a9d36e85a5b271dbed9983509441479eba Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 12 Jun 2025 14:02:18 -0700 Subject: [PATCH 16/20] Change default checks + update documentation --- docs/geos_mesh_docs/doctor.rst | 16 +++++++++----- .../mesh/doctor/parsing/all_checks_parsing.py | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index d1a1499e..fa71b332 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -87,12 +87,14 @@ Here is a list and brief description of all the modules available. ``all_checks`` """""""""""""" -``mesh-doctor`` modules are called ``actions`` and they can be splitted into 2 different categories: +``mesh-doctor`` modules are called ``actions`` and they can be split into 2 different categories: ``check actions`` that will give you a feedback on a .vtu mesh that you would like to use in GEOS. -``operate actions`` that will either create a new mesh or modify a mesh. +``operate actions`` that will either create a new mesh or modify an existing mesh. -``all_checks`` aims at applying every single ``check`` action in one single command. The list is the following: +``all_checks`` aims at applying every single ``check`` action in one single command. The available list is of check is: ``collocated_nodes``, ``element_volumes``, ``non_conformal``, ``self_intersecting_elements``, ``supported_elements``. +By default, only ``collocated_nodes``, ``element_volumes``, ``self_intersecting_elements`` will be performed because +``non_conformal`` and ``supported_elements`` are slower to perform. .. code-block:: @@ -102,13 +104,15 @@ Here is a list and brief description of all the modules available. options: -h, --help show this help message and exit --checks_to_perform CHECKS_TO_PERFORM - Comma-separated list of mesh-doctor checks to perform. If no input was given, all of the following checks will be executed by default: - ['collocated_nodes', 'element_volumes', 'non_conformal', 'self_intersecting_elements', 'supported_elements']. + Comma-separated list of mesh-doctor checks to perform. + If no input was given, all of the following checks will be executed by default: ['collocated_nodes', 'element_volumes', 'self_intersecting_elements']. + The available choices for checks are ['collocated_nodes', 'element_volumes', 'non_conformal', 'self_intersecting_elements', 'supported_elements']. If you want to choose only certain of them, you can name them individually. Example: --checks_to_perform collocated_nodes,element_volumes (default: ) --set_parameters SET_PARAMETERS Comma-separated list of parameters to set for the checks (e.g., 'param_name:value'). These parameters override the defaults. - Default parameters are: For collocated_nodes: tolerance:0.0. For element_volumes: min_volume:0.0. For non_conformal: angle_tolerance:10.0, point_tolerance:0.0, face_tolerance:0.0. + Default parameters are: For collocated_nodes: tolerance:0.0. For element_volumes: min_volume:0.0. + For non_conformal: angle_tolerance:10.0, point_tolerance:0.0, face_tolerance:0.0. For self_intersecting_elements: min_distance:2.220446049250313e-16. For supported_elements: chunk_size:1, nproc:8. Example: --set_parameters parameter_name:10.5,other_param:25 (default: ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index b913e425..114f9a46 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -77,6 +77,12 @@ class CheckFeature: SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS, ] +# Because some checks are slower to perform, the default checks have to be only the following +DEFAULT_CHECK_NAMES: list[ str ] = [ + COLLOCATES_NODES, + ELEMENT_VOLUMES, + SELF_INTERSECTING_ELEMENTS, +] DEFAULT_PARAMS: dict[ str, dict[ str, float ] ] = { name: feature.default_params.copy() for name, feature in CHECK_FEATURES_CONFIG.items() @@ -110,8 +116,9 @@ def fill_subparser( subparsers: argparse._SubParsersAction ) -> None: default="", required=False, help=( "Comma-separated list of mesh-doctor checks to perform. If no input was given, all of" - f" the following checks will be executed by default: {ORDERED_CHECK_NAMES}. If you want" - " to choose only certain of them, you can name them individually." + f" the following checks will be executed by default: {DEFAULT_CHECK_NAMES}." + f" The available choices for checks are {ORDERED_CHECK_NAMES}." + " If you want to choose only certain of them, you can name them individually." f" Example: --{CHECKS_TO_DO_ARG} {ORDERED_CHECK_NAMES[0]},{ORDERED_CHECK_NAMES[1]}" ) ) parser.add_argument( f"--{PARAMETERS_ARG}", @@ -128,7 +135,7 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: Converts parsed command-line arguments into an AllChecksOptions object. """ # 1. Determine which checks to perform - final_selected_check_names: list[ str ] = deepcopy( ORDERED_CHECK_NAMES ) + final_selected_check_names: list[ str ] = deepcopy( DEFAULT_CHECK_NAMES ) if not parsed_args[ CHECKS_TO_DO_ARG ]: # handles default and if user explicitly provides --checks_to_perform "" setup_logger.info( "All current available checks in mesh-doctor will be performed." ) else: # the user specifically entered check names to perform @@ -153,7 +160,7 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: del final_selected_check_params[ name ] # Remove non-used check features if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" - setup_logger.info( "Default configuation of parameters adopted for every check to perform." ) + setup_logger.info( "Default configuration of parameters adopted for every check to perform." ) else: set_parameters = parse_comma_separated_string( parsed_args[ PARAMETERS_ARG ] ) for param in set_parameters: @@ -203,7 +210,12 @@ def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: # --- Display Results --- def display_results( options: AllChecksOptions, result: AllChecksResult ) -> None: - """Displays the results of the checks.""" + """Displays the results of all the checks that have been performed. + + Args: + options (AllChecksOptions): The options chosen for every check performed. + result (AllChecksResult): The result obtained for every check performed. + """ max_length = max( len( name ) for name in options.checks_to_perform ) # Implementation for displaying results based on the structured options and results. for name, res in result.check_results.items(): From 67deb6b619eb8a4c30aee5fa30a01bdee83b9b7a Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 12 Jun 2025 16:13:39 -0700 Subject: [PATCH 17/20] Update test and log output --- .../geos/mesh/doctor/parsing/all_checks_parsing.py | 2 ++ geos-mesh/tests/test_all_checks.py | 6 +++--- geos-utils/src/geos/utils/Logger.py | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 114f9a46..40ce80c9 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -219,5 +219,7 @@ def display_results( options: AllChecksOptions, result: AllChecksResult ) -> Non max_length = max( len( name ) for name in options.checks_to_perform ) # Implementation for displaying results based on the structured options and results. for name, res in result.check_results.items(): + setup_logger.results( "" ) # insert blank line for better visibility between each results setup_logger.results( f"******** {name:<{max_length}} ********" ) options.check_displays[ name ]( options.checks_options[ name ], res ) + setup_logger.results( "" ) diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py index 5432fc6e..12a073d4 100644 --- a/geos-mesh/tests/test_all_checks.py +++ b/geos-mesh/tests/test_all_checks.py @@ -5,7 +5,7 @@ from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult from geos.mesh.doctor.actions.all_checks import action from geos.mesh.doctor.parsing.all_checks_parsing import convert, fill_subparser, display_results -from geos.mesh.doctor.parsing.all_checks_parsing import ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG +from geos.mesh.doctor.parsing.all_checks_parsing import DEFAULT_CHECK_NAMES, ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG # Mock data and fixtures @@ -56,10 +56,10 @@ def test_convert_with_default_checks( self ): mock_logger.info.assert_any_call( "All current available checks in mesh-doctor will be performed." ) # Should include all checks - assert options.checks_to_perform == ORDERED_CHECK_NAMES + assert options.checks_to_perform == DEFAULT_CHECK_NAMES # Should use default parameters - for check_name in ORDERED_CHECK_NAMES: + for check_name in DEFAULT_CHECK_NAMES: assert check_name in options.checks_options def test_convert_with_specific_checks( self, mock_args ): diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index dcedee0f..69ec6ec5 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -13,16 +13,15 @@ # Add the convenience method for the logger -def results( self, message: str, *args: Any, **kws: Any ) -> None: # noqa: ANN001 +def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> None: """Logs a message with the custom 'RESULTS' severity level. This level is designed for summary information that should always be visible, regardless of the logger's verbosity setting. Args: - self (Self): The logger instance. - message (str): The primary log message, with optional format specifiers - (e.g., "Found %d issues."). + self (logging.Logger): The logger instance. + message (str): The primary log message, with optional format specifiers (e.g., "Found %d issues."). *args: The arguments to be substituted into the `message` string. **kws: Keyword arguments for special functionality. """ @@ -42,8 +41,9 @@ def results( self, message: str, *args: Any, **kws: Any ) -> None: # noqa: ANN0 RESULTS_LEVEL_NAME: str = "RESULTS" logging.addLevelName( RESULTS_LEVEL_NUM, RESULTS_LEVEL_NAME ) logging.Logger.results = results # type: ignore[attr-defined] + # types redefinition to import logging.* from this module -Logger = logging.Logger #: logger type +Logger = logging.Logger # logger type class CustomLoggerFormatter( logging.Formatter ): @@ -111,7 +111,7 @@ def __init__( self: Self, use_color: bool = False ) -> None: Args: use_color (bool): If True, use color-coded log formatters. - Defaults to False. + Defaults to False. """ super().__init__() if use_color: @@ -160,6 +160,7 @@ def getLogger( title: str, use_color: bool = False ) -> Logger: logger.warning("warning message") logger.error("error message") logger.critical("critical message") + logger.results("results message") Args: title (str): Name of the logger. From 0bd933486686418e1cfc86fd6e6a44fc7843de10 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 16 Jun 2025 18:54:20 -0700 Subject: [PATCH 18/20] Add main_checks feature by refactoring all_checks feature + doc update + TODO reimplement test --- docs/geos_mesh_docs/doctor.rst | 12 +- .../geos/mesh/doctor/actions/main_checks.py | 1 + geos-mesh/src/geos/mesh/doctor/mesh_doctor.py | 8 - .../src/geos/mesh/doctor/parsing/__init__.py | 1 + .../parsing/_shared_checks_parsing_logic.py | 162 +++++++++ .../mesh/doctor/parsing/all_checks_parsing.py | 195 ++-------- .../doctor/parsing/main_checks_parsing.py | 69 ++++ geos-mesh/src/geos/mesh/doctor/register.py | 4 +- geos-mesh/tests/test_all_checks.py | 335 +++++++++--------- 9 files changed, 438 insertions(+), 349 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/actions/main_checks.py create mode 100644 geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py create mode 100644 geos-mesh/src/geos/mesh/doctor/parsing/main_checks_parsing.py diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index fa71b332..0e66d84f 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -84,8 +84,8 @@ You can solve this issue by installing the dependencies of ``mesh-doctor`` defin Here is a list and brief description of all the modules available. -``all_checks`` -"""""""""""""" +``all_checks`` and ``main_checks`` +"""""""""""""""""""""""""""""""""" ``mesh-doctor`` modules are called ``actions`` and they can be split into 2 different categories: ``check actions`` that will give you a feedback on a .vtu mesh that you would like to use in GEOS. @@ -93,8 +93,12 @@ Here is a list and brief description of all the modules available. ``all_checks`` aims at applying every single ``check`` action in one single command. The available list is of check is: ``collocated_nodes``, ``element_volumes``, ``non_conformal``, ``self_intersecting_elements``, ``supported_elements``. -By default, only ``collocated_nodes``, ``element_volumes``, ``self_intersecting_elements`` will be performed because -``non_conformal`` and ``supported_elements`` are slower to perform. + +``main_checks`` does only the fastest checks ``collocated_nodes``, ``element_volumes`` and ``self_intersecting_elements`` +that can quickly highlight some issues to deal with before investigating the other checks. + +Both ``all_checks`` and ``main_checks`` have the same keywords and can be operated in the same way. The example below shows +the case of ``all_checks``, but it can be swapped for ``main_checks``. .. code-block:: diff --git a/geos-mesh/src/geos/mesh/doctor/actions/main_checks.py b/geos-mesh/src/geos/mesh/doctor/actions/main_checks.py new file mode 100644 index 00000000..2ae3b9da --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/actions/main_checks.py @@ -0,0 +1 @@ +from geos.mesh.doctor.actions.all_checks import action diff --git a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py index 0144c232..3c6187d4 100644 --- a/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py +++ b/geos-mesh/src/geos/mesh/doctor/mesh_doctor.py @@ -1,12 +1,4 @@ import sys - -min_python_version = ( 3, 7 ) -try: - assert sys.version_info >= min_python_version -except AssertionError: - print( f"Please update python to at least version {'.'.join(map(str, min_python_version))}." ) - sys.exit( 1 ) - from geos.mesh.doctor.parsing import ActionHelper from geos.mesh.doctor.parsing.cli_parsing import parse_and_set_verbosity, setup_logger from geos.mesh.doctor.register import register_parsing_actions diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py index c37fa92b..deb553cf 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/__init__.py @@ -3,6 +3,7 @@ from typing import Callable, Any ALL_CHECKS = "all_checks" +MAIN_CHECKS = "main_checks" COLLOCATES_NODES = "collocated_nodes" ELEMENT_VOLUMES = "element_volumes" FIX_ELEMENTS_ORDERINGS = "fix_elements_orderings" diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py b/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py new file mode 100644 index 00000000..1d6dc739 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py @@ -0,0 +1,162 @@ +import argparse +from copy import deepcopy +from dataclasses import dataclass +from typing import Type, Any + +from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult +from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string, setup_logger + + +# --- Data Structure for Check Features --- +@dataclass( frozen=True ) +class CheckFeature: + """A container for a check's configuration and associated classes.""" + name: str + options_cls: Type[ Any ] + result_cls: Type[ Any ] + default_params: dict[ str, Any ] + display: Type[ Any ] + + +# --- Argument Parser Constants --- +CHECKS_TO_DO_ARG = "checks_to_perform" +PARAMETERS_ARG = "set_parameters" + + +def _generate_parameters_help( ordered_check_names: list[ str ], check_features_config: dict[ str, + CheckFeature ] ) -> str: + """Dynamically generates the help text for the set_parameters argument.""" + help_text: str = "" + for check_name in ordered_check_names: + config = check_features_config.get( check_name ) + if config and config.default_params: + config_params = [ f"{name}:{value}" for name, value in config.default_params.items() ] + help_text += f"For {check_name}: {', '.join(config_params)}. " + return help_text + + +# --- Generic Argument Parser Setup --- +def fill_subparser( subparsers: argparse._SubParsersAction, subparser_name: str, help_message: str, + ordered_check_names: list[ str ], check_features_config: dict[ str, CheckFeature ] ) -> None: + """ + Fills a subparser with arguments for performing a set of checks. + + Args: + subparsers: The subparsers action from argparse. + subparser_name: The name for this specific subparser (e.g., 'all-checks'). + help_message: The help message for this subparser. + ordered_check_names: The list of check names to be used in help messages. + check_features_config: The configuration dictionary for the checks. + """ + parser = subparsers.add_parser( subparser_name, + help=help_message, + formatter_class=argparse.ArgumentDefaultsHelpFormatter ) + + parameters_help: str = _generate_parameters_help( ordered_check_names, check_features_config ) + + parser.add_argument( f"--{CHECKS_TO_DO_ARG}", + type=str, + default="", + required=False, + help=( "Comma-separated list of checks to perform. " + f"If empty, all of the following are run by default: {ordered_check_names}. " + f"Available choices: {ordered_check_names}. " + f"Example: --{CHECKS_TO_DO_ARG} {ordered_check_names[0]},{ordered_check_names[1]}" ) ) + parser.add_argument( f"--{PARAMETERS_ARG}", + type=str, + default="", + required=False, + help=( "Comma-separated list of parameters to override defaults (e.g., 'param_name:value'). " + f"Default parameters are: {parameters_help}" + f"Example: --{PARAMETERS_ARG} parameter_name:10.5,other_param:25" ) ) + + +def convert( parsed_args: argparse.Namespace, ordered_check_names: list[ str ], + check_features_config: dict[ str, CheckFeature ] ) -> AllChecksOptions: + """ + Converts parsed command-line arguments into an AllChecksOptions object based on the provided configuration. + """ + # 1. Determine which checks to perform + if not parsed_args[ CHECKS_TO_DO_ARG ]: # handles default and if user explicitly provides --checks_to_perform "" + final_selected_check_names: list[ str ] = deepcopy( ordered_check_names ) + setup_logger.info( "All configured checks will be performed by default." ) + else: + user_checks = parse_comma_separated_string( parsed_args[ CHECKS_TO_DO_ARG ] ) + final_selected_check_names = list() + for name in user_checks: + if name not in check_features_config: + setup_logger.warning( f"Check '{name}' does not exist. Choose from: {ordered_check_names}." ) + elif name not in final_selected_check_names: + final_selected_check_names.append( name ) + + if not final_selected_check_names: + raise ValueError( "No valid checks were selected. No operations will be configured." ) + + # 2. Prepare parameters for the selected checks + default_params = { name: feature.default_params.copy() for name, feature in check_features_config.items() } + final_check_params = { name: default_params[ name ] for name in final_selected_check_names } + + if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" + setup_logger.info( "Default configuration of parameters adopted for every check to perform." ) + else: + set_parameters = parse_comma_separated_string( parsed_args[ PARAMETERS_ARG ] ) + for param in set_parameters: + if ':' not in param: + setup_logger.warning( f"Parameter '{param}' is not in 'name:value' format. Skipping." ) + continue + + name, _, value_str = param.partition( ':' ) + name = name.strip() + value_str = value_str.strip() + + if not value_str: + setup_logger.warning( f"Parameter '{name}' has no value. Skipping." ) + continue + + try: + value_float = float( value_str ) + except ValueError: + setup_logger.warning( f"Invalid value for '{name}': '{value_str}'. Must be a number. Skipping." ) + continue + + # Apply the parameter override to any check that uses it + for check_name_key in final_check_params: + if name in final_check_params[ check_name_key ]: + final_check_params[ check_name_key ][ name ] = value_float + + # 3. Instantiate Options objects for the selected checks + individual_check_options: dict[ str, Any ] = dict() + individual_check_display: dict[ str, Any ] = dict() + + for check_name in list( final_check_params.keys() ): + params = final_check_params[ check_name ] + feature_config = check_features_config[ check_name ] + try: + individual_check_options[ check_name ] = feature_config.options_cls( **params ) + individual_check_display[ check_name ] = feature_config.display + except Exception as e: + setup_logger.error( f"Failed to create options for check '{check_name}': {e}. This check will be skipped." ) + final_selected_check_names.remove( check_name ) + + return AllChecksOptions( checks_to_perform=final_selected_check_names, + checks_options=individual_check_options, + check_displays=individual_check_display ) + + +# Generic display of Results +def display_results( options: AllChecksOptions, result: AllChecksResult ) -> None: + """Displays the results of all the checks that have been performed.""" + if not options.checks_to_perform: + setup_logger.results( "No checks were performed or all failed during configuration." ) + return + + max_length = max( len( name ) for name in options.checks_to_perform ) + for name, res in result.check_results.items(): + setup_logger.results( "" ) # Blank line for visibility + setup_logger.results( f"******** {name:<{max_length}} ********" ) + display_func = options.check_displays.get( name ) + opts = options.checks_options.get( name ) + if display_func and opts: + display_func( opts, res ) + setup_logger.results( "" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py index 40ce80c9..53782f78 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/all_checks_parsing.py @@ -1,41 +1,39 @@ import argparse from copy import deepcopy -from dataclasses import dataclass -from typing import Type +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import ( CheckFeature, convert as shared_convert, + fill_subparser as shared_fill_subparser, + display_results ) from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions -from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult + # Import constants for check names from geos.mesh.doctor.parsing import ( - ALL_CHECKS, # Name for the subparser + ALL_CHECKS, COLLOCATES_NODES, ELEMENT_VOLUMES, NON_CONFORMAL, SELF_INTERSECTING_ELEMENTS, SUPPORTED_ELEMENTS, ) -# Import module-specific Options, Result, and defaults -# Using module aliases for clarity + +# Import module-specific parsing components from geos.mesh.doctor.parsing import collocated_nodes_parsing as cn_parser from geos.mesh.doctor.parsing import element_volumes_parsing as ev_parser from geos.mesh.doctor.parsing import non_conformal_parsing as nc_parser from geos.mesh.doctor.parsing import self_intersecting_elements_parsing as sie_parser from geos.mesh.doctor.parsing import supported_elements_parsing as se_parser -from geos.mesh.doctor.parsing.cli_parsing import parse_comma_separated_string, setup_logger - -# --- Centralized Configuration for Check Features --- -# This structure makes it easier to manage checks and their properties. +# --- Configuration Specific to "All Checks" --- -@dataclass( frozen=True ) # Consider using dataclass if appropriate, or a simple dict -class CheckFeature: - name: str - options_cls: Type[ any ] # Specific Options class (e.g., cn_parser.Options) - result_cls: Type[ any ] # Specific Result class (e.g., cn_parser.Result) - default_params: dict[ str, any ] # Parser keywords with default values - display: Type[ any ] # Specific display function for results - +# Ordered list of check names for this configuration +ORDERED_CHECK_NAMES = [ + COLLOCATES_NODES, + ELEMENT_VOLUMES, + NON_CONFORMAL, + SELF_INTERSECTING_ELEMENTS, + SUPPORTED_ELEMENTS, +] -# Deepcopy to prevent accidental modification of originals default parameters +# Centralized configuration for the checks managed by this module CHECK_FEATURES_CONFIG = { COLLOCATES_NODES: CheckFeature( name=COLLOCATES_NODES, @@ -69,157 +67,18 @@ class CheckFeature: display=se_parser.display_results ), } -# Ordered list of check names, defining the default order and for consistent help messages -ORDERED_CHECK_NAMES: list[ str ] = [ - COLLOCATES_NODES, - ELEMENT_VOLUMES, - NON_CONFORMAL, - SELF_INTERSECTING_ELEMENTS, - SUPPORTED_ELEMENTS, -] -# Because some checks are slower to perform, the default checks have to be only the following -DEFAULT_CHECK_NAMES: list[ str ] = [ - COLLOCATES_NODES, - ELEMENT_VOLUMES, - SELF_INTERSECTING_ELEMENTS, -] -DEFAULT_PARAMS: dict[ str, dict[ str, float ] ] = { - name: feature.default_params.copy() - for name, feature in CHECK_FEATURES_CONFIG.items() -} -# --- Argument Parser Constants --- -CHECKS_TO_DO_ARG = "checks_to_perform" -PARAMETERS_ARG = "set_parameters" - -# Generate help text for set_parameters dynamically -PARAMETERS_ARG_HELP: str = "" -for check_name in ORDERED_CHECK_NAMES: - config = CHECK_FEATURES_CONFIG[ check_name ] - if config.default_params: - config_params: list[ str ] = list() - for name, value in config.default_params.items(): - config_params.append( f"{name}:{value}" ) - PARAMETERS_ARG_HELP += f"For {check_name}: {', '.join( config_params )}. " - - -# --- Argument Parser Setup --- def fill_subparser( subparsers: argparse._SubParsersAction ) -> None: - """Fills the subparser for 'ALL_CHECKS' with its arguments.""" - parser = subparsers.add_parser( - ALL_CHECKS, - help="Perform one or multiple mesh-doctor check operations in one command line on the same mesh.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter # Shows defaults in help - ) - parser.add_argument( f"--{CHECKS_TO_DO_ARG}", - type=str, - default="", - required=False, - help=( "Comma-separated list of mesh-doctor checks to perform. If no input was given, all of" - f" the following checks will be executed by default: {DEFAULT_CHECK_NAMES}." - f" The available choices for checks are {ORDERED_CHECK_NAMES}." - " If you want to choose only certain of them, you can name them individually." - f" Example: --{CHECKS_TO_DO_ARG} {ORDERED_CHECK_NAMES[0]},{ORDERED_CHECK_NAMES[1]}" ) ) - parser.add_argument( - f"--{PARAMETERS_ARG}", - type=str, - default="", - required=False, - help=( "Comma-separated list of parameters to set for the checks (e.g., 'param_name:value'). " - "These parameters override the defaults. Default parameters are:" - f" {PARAMETERS_ARG_HELP} Example: --{PARAMETERS_ARG} parameter_name:10.5,other_param:25" ) ) + """Fills the subparser by calling the shared logic with the specific 'all_checks' configuration.""" + shared_fill_subparser( subparsers=subparsers, + subparser_name=ALL_CHECKS, + help_message="Perform one or multiple mesh-doctor checks from the complete set available.", + ordered_check_names=ORDERED_CHECK_NAMES, + check_features_config=CHECK_FEATURES_CONFIG ) def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: - """ - Converts parsed command-line arguments into an AllChecksOptions object. - """ - # 1. Determine which checks to perform - final_selected_check_names: list[ str ] = deepcopy( DEFAULT_CHECK_NAMES ) - if not parsed_args[ CHECKS_TO_DO_ARG ]: # handles default and if user explicitly provides --checks_to_perform "" - setup_logger.info( "All current available checks in mesh-doctor will be performed." ) - else: # the user specifically entered check names to perform - checks_to_do: list[ str ] = parse_comma_separated_string( parsed_args[ CHECKS_TO_DO_ARG ] ) - final_selected_check_names = list() - for name in checks_to_do: - if name not in CHECK_FEATURES_CONFIG: - setup_logger.warning( f"The given check '{name}' does not exist. Cannot perform this check." - f" Choose from: {ORDERED_CHECK_NAMES}." ) - elif name not in final_selected_check_names: # Add if valid and not already added - final_selected_check_names.append( name ) - - # If after parsing, no valid checks are selected (e.g., all inputs were invalid) - if not final_selected_check_names: - setup_logger.error( "No valid checks selected based on input. No operations will be configured." ) - raise ValueError( "No valid checks selected based on input. No operations will be configured." ) - - # 2. Prepare parameters of Options for every check feature that will be used - final_selected_check_params: dict[ str, dict[ str, float ] ] = deepcopy( DEFAULT_PARAMS ) - for name in list( final_selected_check_params.keys() ): - if name not in final_selected_check_names: - del final_selected_check_params[ name ] # Remove non-used check features - - if not parsed_args[ PARAMETERS_ARG ]: # handles default and if user explicitly provides --set_parameters "" - setup_logger.info( "Default configuration of parameters adopted for every check to perform." ) - else: - set_parameters = parse_comma_separated_string( parsed_args[ PARAMETERS_ARG ] ) - for param in set_parameters: - if ':' not in param: - setup_logger.warning( - f"Parameter '{param}' in --{PARAMETERS_ARG} is not in 'name:value' format. Skipping." ) - continue - name, *value = param.split( ':', 1 ) - name = name.strip() - if value: # Check if there is anything after the first colon - value_str = value[ 0 ].strip() - else: - # Handle cases where there's nothing after the colon, if necessary - setup_logger.warning( f"Parameter '{name}' has no value after the colon. Skipping or using default." ) - continue - try: - value_float = float( value_str ) - except ValueError: - setup_logger.warning( - f"Invalid value for parameter '{name}': '{value_str}'. Must be a number. Skipping this override." ) - continue - - for check_name_key in final_selected_check_params.keys(): # Iterate through all possible checks - if name in final_selected_check_params[ check_name_key ]: - final_selected_check_params[ check_name_key ][ name ] = value_float - break - - # 3. Instantiate the Options objects for the selected checks using their effective parameters - individual_check_options: dict[ str, any ] = dict() - individual_check_display: dict[ str, any ] = dict() - for check_name in list( final_selected_check_params.keys() ): - options_constructor_params = final_selected_check_params[ check_name ] - feature_config = CHECK_FEATURES_CONFIG[ check_name ] - try: - individual_check_options[ check_name ] = feature_config.options_cls( **options_constructor_params ) - individual_check_display[ check_name ] = feature_config.display - except Exception as e: # Catch potential errors during options instantiation - setup_logger.error( - f"Failed to create options for check '{check_name}' with params {options_constructor_params}: {e}." - f" Therefore the check '{check_name}' will not be performed." ) - final_selected_check_names.remove( check_name ) - - return AllChecksOptions( checks_to_perform=final_selected_check_names, - checks_options=individual_check_options, - check_displays=individual_check_display ) - - -# --- Display Results --- -def display_results( options: AllChecksOptions, result: AllChecksResult ) -> None: - """Displays the results of all the checks that have been performed. - - Args: - options (AllChecksOptions): The options chosen for every check performed. - result (AllChecksResult): The result obtained for every check performed. - """ - max_length = max( len( name ) for name in options.checks_to_perform ) - # Implementation for displaying results based on the structured options and results. - for name, res in result.check_results.items(): - setup_logger.results( "" ) # insert blank line for better visibility between each results - setup_logger.results( f"******** {name:<{max_length}} ********" ) - options.check_displays[ name ]( options.checks_options[ name ], res ) - setup_logger.results( "" ) + """Converts arguments by calling the shared logic with the 'all_checks' configuration.""" + return shared_convert( parsed_args=parsed_args, + ordered_check_names=ORDERED_CHECK_NAMES, + check_features_config=CHECK_FEATURES_CONFIG ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/main_checks_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/main_checks_parsing.py new file mode 100644 index 00000000..8c396930 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/parsing/main_checks_parsing.py @@ -0,0 +1,69 @@ +import argparse +from copy import deepcopy +from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import ( CheckFeature, convert as shared_convert, + fill_subparser as shared_fill_subparser, + display_results ) + +# Import constants for check names +from geos.mesh.doctor.parsing import ( + MAIN_CHECKS, + COLLOCATES_NODES, + ELEMENT_VOLUMES, + SELF_INTERSECTING_ELEMENTS, +) + +# Import module-specific parsing components +from geos.mesh.doctor.parsing import collocated_nodes_parsing as cn_parser +from geos.mesh.doctor.parsing import element_volumes_parsing as ev_parser +from geos.mesh.doctor.parsing import self_intersecting_elements_parsing as sie_parser + +# --- Configuration Specific to "Main Checks" --- + +# Ordered list of check names for this configuration +ORDERED_CHECK_NAMES = [ + COLLOCATES_NODES, + ELEMENT_VOLUMES, + SELF_INTERSECTING_ELEMENTS, +] + +# Centralized configuration for the checks managed by this module +CHECK_FEATURES_CONFIG = { + COLLOCATES_NODES: + CheckFeature( name=COLLOCATES_NODES, + options_cls=cn_parser.Options, + result_cls=cn_parser.Result, + default_params=deepcopy( cn_parser.__COLLOCATED_NODES_DEFAULT ), + display=cn_parser.display_results ), + ELEMENT_VOLUMES: + CheckFeature( name=ELEMENT_VOLUMES, + options_cls=ev_parser.Options, + result_cls=ev_parser.Result, + default_params=deepcopy( ev_parser.__ELEMENT_VOLUMES_DEFAULT ), + display=ev_parser.display_results ), + SELF_INTERSECTING_ELEMENTS: + CheckFeature( name=SELF_INTERSECTING_ELEMENTS, + options_cls=sie_parser.Options, + result_cls=sie_parser.Result, + default_params=deepcopy( sie_parser.__SELF_INTERSECTING_ELEMENTS_DEFAULT ), + display=sie_parser.display_results ), +} + + +def fill_subparser( subparsers: argparse._SubParsersAction ) -> None: + """Fills the subparser by calling the shared logic with the specific 'main_checks' configuration.""" + shared_fill_subparser( subparsers=subparsers, + subparser_name=MAIN_CHECKS, + help_message="Perform a curated set of main mesh-doctor checks.", + ordered_check_names=ORDERED_CHECK_NAMES, + check_features_config=CHECK_FEATURES_CONFIG ) + + +def convert( parsed_args: argparse.Namespace ) -> AllChecksOptions: + """Converts arguments by calling the shared logic with the 'main_checks' configuration.""" + return shared_convert( parsed_args=parsed_args, + ordered_check_names=ORDERED_CHECK_NAMES, + check_features_config=CHECK_FEATURES_CONFIG ) + + +# The display_results function is imported directly as it needs no special configuration. diff --git a/geos-mesh/src/geos/mesh/doctor/register.py b/geos-mesh/src/geos/mesh/doctor/register.py index 31ac712f..41c52bd6 100644 --- a/geos-mesh/src/geos/mesh/doctor/register.py +++ b/geos-mesh/src/geos/mesh/doctor/register.py @@ -54,8 +54,8 @@ def closure_trick( cn: str ): # Register the modules to load here. for action_name in ( parsing.ALL_CHECKS, parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, parsing.FIX_ELEMENTS_ORDERINGS, parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, - parsing.GENERATE_GLOBAL_IDS, parsing.NON_CONFORMAL, parsing.SELF_INTERSECTING_ELEMENTS, - parsing.SUPPORTED_ELEMENTS ): + parsing.GENERATE_GLOBAL_IDS, parsing.MAIN_CHECKS, parsing.NON_CONFORMAL, + parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS ): closure_trick( action_name ) loaded_actions: Dict[ str, Callable[ [ str, Any ], Any ] ] = __load_actions() loaded_actions_helpers: Dict[ str, ActionHelper ] = dict() diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py index 12a073d4..ddf24f89 100644 --- a/geos-mesh/tests/test_all_checks.py +++ b/geos-mesh/tests/test_all_checks.py @@ -1,167 +1,168 @@ -import pytest -import argparse -from unittest.mock import patch, MagicMock, call -from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions -from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult -from geos.mesh.doctor.actions.all_checks import action -from geos.mesh.doctor.parsing.all_checks_parsing import convert, fill_subparser, display_results -from geos.mesh.doctor.parsing.all_checks_parsing import DEFAULT_CHECK_NAMES, ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG - - -# Mock data and fixtures -@pytest.fixture -def mock_parser(): - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers( dest="action" ) - return parser, subparsers - - -@pytest.fixture -def mock_check_action(): - return MagicMock( return_value={ "status": "success" } ) - - -@pytest.fixture -def mock_args(): - return { - "checks_to_perform": "collocated_nodes, element_volumes", - "set_parameters": "tolerance:1.0, min_volume:0.5" - } - - -# Tests for all_checks_parsing.py -class TestAllChecksParsing: - - def test_fill_subparser( self, mock_parser ): - parser, subparsers = mock_parser - fill_subparser( subparsers ) - - # Verify subparser was created - subparsers_actions = [ - action for action in parser._subparsers._actions if isinstance( action, argparse._SubParsersAction ) - ] - assert len( subparsers_actions ) == 1 - - # Check if our subparser is in the choices - subparser_choices = subparsers_actions[ 0 ].choices - assert "all_checks" in subparser_choices # assuming ALL_CHECKS is "all_checks" - - def test_convert_with_default_checks( self ): - # Test with empty string for checks_to_perform (should use all checks) - args = { "checks_to_perform": "", "set_parameters": "" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: - options = convert( args ) - - # Should log that all checks will be performed - mock_logger.info.assert_any_call( "All current available checks in mesh-doctor will be performed." ) - - # Should include all checks - assert options.checks_to_perform == DEFAULT_CHECK_NAMES - - # Should use default parameters - for check_name in DEFAULT_CHECK_NAMES: - assert check_name in options.checks_options - - def test_convert_with_specific_checks( self, mock_args ): - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): - options = convert( mock_args ) - - # Should only include the specified checks - expected_checks = [ "collocated_nodes", "element_volumes" ] - assert options.checks_to_perform == expected_checks - - # Should only have options for specified checks - assert set( options.checks_options.keys() ) == set( expected_checks ) - - def test_convert_with_invalid_check( self ): - args = { "checks_to_perform": "invalid_check_name", "set_parameters": "" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: - with pytest.raises( ValueError, match="No valid checks selected" ): - convert( args ) - - # Should log warning about invalid check - mock_logger.warning.assert_called() - - def test_convert_with_parameter_override( self ): - # Choose a check and parameter that exists in DEFAULT_PARAMS - check_name = "collocated_nodes" - param_name = next( iter( CHECK_FEATURES_CONFIG[ check_name ].default_params.keys() ) ) - args = { "checks_to_perform": check_name, "set_parameters": f"{param_name}:99.9" } - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): - options = convert( args ) - - # Get the options object for the check - check_options = options.checks_options[ check_name ] - - # Verify the parameter was overridden - # This assumes the parameter is accessible as an attribute of the options object - # May need adjustment based on your actual implementation - assert getattr( check_options, param_name, None ) == 99.9 - - def test_display_results( self ): - # Create mock options and results - mock_display_func = MagicMock() - check_name = ORDERED_CHECK_NAMES[ 0 ] - options = AllChecksOptions( checks_to_perform=[ check_name ], - checks_options={ check_name: "mock_options" }, - check_displays={ check_name: mock_display_func } ) - result = AllChecksResult( check_results={ check_name: "mock_result" } ) - with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): - display_results( options, result ) - - # Verify display function was called with correct arguments - mock_display_func.assert_called_once_with( "mock_options", "mock_result" ) - - -# Tests for all_checks.py -class TestAllChecks: - - def test_action_calls_check_modules( self, mock_check_action ): - # Setup mock options - check_name = ORDERED_CHECK_NAMES[ 0 ] - mock_options = AllChecksOptions( checks_to_perform=[ check_name ], - checks_options={ check_name: "mock_options" }, - check_displays={ check_name: MagicMock() } ) - # Mock the module loading function - with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', - return_value=mock_check_action ) as mock_load: - with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): - result = action( "test_file.vtk", mock_options ) - - # Verify the module was loaded - mock_load.assert_called_once_with( check_name ) - - # Verify the check action was called with correct args - mock_check_action.assert_called_once_with( "test_file.vtk", "mock_options" ) - - # Verify result contains the check result - assert check_name in result.check_results - assert result.check_results[ check_name ] == { "status": "success" } - - def test_action_with_multiple_checks( self, mock_check_action ): - # Setup mock options with multiple checks - check_names = [ ORDERED_CHECK_NAMES[ 0 ], ORDERED_CHECK_NAMES[ 1 ] ] - mock_options = AllChecksOptions( checks_to_perform=check_names, - checks_options={ - name: f"mock_options_{i}" - for i, name in enumerate( check_names ) - }, - check_displays={ name: MagicMock() - for name in check_names } ) - # Mock the module loading function - with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', - return_value=mock_check_action ) as mock_load: - with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): - result = action( "test_file.vtk", mock_options ) - - # Verify the modules were loaded - assert mock_load.call_count == 2 - mock_load.assert_has_calls( [ call( check_names[ 0 ] ), call( check_names[ 1 ] ) ] ) - - # Verify all checks were called - assert mock_check_action.call_count == 2 - - # Verify result contains all check results - for name in check_names: - assert name in result.check_results - assert result.check_results[ name ] == { "status": "success" } +# TODO Reimplement the tests +# import pytest +# import argparse +# from unittest.mock import patch, MagicMock, call +# from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +# from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult +# from geos.mesh.doctor.actions.all_checks import action +# from geos.mesh.doctor.parsing.all_checks_parsing import convert, fill_subparser, display_results +# from geos.mesh.doctor.parsing.all_checks_parsing import DEFAULT_CHECK_NAMES, ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG + + +# # Mock data and fixtures +# @pytest.fixture +# def mock_parser(): +# parser = argparse.ArgumentParser() +# subparsers = parser.add_subparsers( dest="action" ) +# return parser, subparsers + + +# @pytest.fixture +# def mock_check_action(): +# return MagicMock( return_value={ "status": "success" } ) + + +# @pytest.fixture +# def mock_args(): +# return { +# "checks_to_perform": "collocated_nodes, element_volumes", +# "set_parameters": "tolerance:1.0, min_volume:0.5" +# } + + +# # Tests for all_checks_parsing.py +# class TestAllChecksParsing: + +# def test_fill_subparser( self, mock_parser ): +# parser, subparsers = mock_parser +# fill_subparser( subparsers ) + +# # Verify subparser was created +# subparsers_actions = [ +# action for action in parser._subparsers._actions if isinstance( action, argparse._SubParsersAction ) +# ] +# assert len( subparsers_actions ) == 1 + +# # Check if our subparser is in the choices +# subparser_choices = subparsers_actions[ 0 ].choices +# assert "all_checks" in subparser_choices # assuming ALL_CHECKS is "all_checks" + +# def test_convert_with_default_checks( self ): +# # Test with empty string for checks_to_perform (should use all checks) +# args = { "checks_to_perform": "", "set_parameters": "" } +# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: +# options = convert( args ) + +# # Should log that all checks will be performed +# mock_logger.info.assert_any_call( "All current available checks in mesh-doctor will be performed." ) + +# # Should include all checks +# assert options.checks_to_perform == DEFAULT_CHECK_NAMES + +# # Should use default parameters +# for check_name in DEFAULT_CHECK_NAMES: +# assert check_name in options.checks_options + +# def test_convert_with_specific_checks( self, mock_args ): +# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): +# options = convert( mock_args ) + +# # Should only include the specified checks +# expected_checks = [ "collocated_nodes", "element_volumes" ] +# assert options.checks_to_perform == expected_checks + +# # Should only have options for specified checks +# assert set( options.checks_options.keys() ) == set( expected_checks ) + +# def test_convert_with_invalid_check( self ): +# args = { "checks_to_perform": "invalid_check_name", "set_parameters": "" } +# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: +# with pytest.raises( ValueError, match="No valid checks selected" ): +# convert( args ) + +# # Should log warning about invalid check +# mock_logger.warning.assert_called() + +# def test_convert_with_parameter_override( self ): +# # Choose a check and parameter that exists in DEFAULT_PARAMS +# check_name = "collocated_nodes" +# param_name = next( iter( CHECK_FEATURES_CONFIG[ check_name ].default_params.keys() ) ) +# args = { "checks_to_perform": check_name, "set_parameters": f"{param_name}:99.9" } +# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): +# options = convert( args ) + +# # Get the options object for the check +# check_options = options.checks_options[ check_name ] + +# # Verify the parameter was overridden +# # This assumes the parameter is accessible as an attribute of the options object +# # May need adjustment based on your actual implementation +# assert getattr( check_options, param_name, None ) == 99.9 + +# def test_display_results( self ): +# # Create mock options and results +# mock_display_func = MagicMock() +# check_name = ORDERED_CHECK_NAMES[ 0 ] +# options = AllChecksOptions( checks_to_perform=[ check_name ], +# checks_options={ check_name: "mock_options" }, +# check_displays={ check_name: mock_display_func } ) +# result = AllChecksResult( check_results={ check_name: "mock_result" } ) +# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): +# display_results( options, result ) + +# # Verify display function was called with correct arguments +# mock_display_func.assert_called_once_with( "mock_options", "mock_result" ) + + +# # Tests for all_checks.py +# class TestAllChecks: + +# def test_action_calls_check_modules( self, mock_check_action ): +# # Setup mock options +# check_name = ORDERED_CHECK_NAMES[ 0 ] +# mock_options = AllChecksOptions( checks_to_perform=[ check_name ], +# checks_options={ check_name: "mock_options" }, +# check_displays={ check_name: MagicMock() } ) +# # Mock the module loading function +# with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', +# return_value=mock_check_action ) as mock_load: +# with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): +# result = action( "test_file.vtk", mock_options ) + +# # Verify the module was loaded +# mock_load.assert_called_once_with( check_name ) + +# # Verify the check action was called with correct args +# mock_check_action.assert_called_once_with( "test_file.vtk", "mock_options" ) + +# # Verify result contains the check result +# assert check_name in result.check_results +# assert result.check_results[ check_name ] == { "status": "success" } + +# def test_action_with_multiple_checks( self, mock_check_action ): +# # Setup mock options with multiple checks +# check_names = [ ORDERED_CHECK_NAMES[ 0 ], ORDERED_CHECK_NAMES[ 1 ] ] +# mock_options = AllChecksOptions( checks_to_perform=check_names, +# checks_options={ +# name: f"mock_options_{i}" +# for i, name in enumerate( check_names ) +# }, +# check_displays={ name: MagicMock() +# for name in check_names } ) +# # Mock the module loading function +# with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', +# return_value=mock_check_action ) as mock_load: +# with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): +# result = action( "test_file.vtk", mock_options ) + +# # Verify the modules were loaded +# assert mock_load.call_count == 2 +# mock_load.assert_has_calls( [ call( check_names[ 0 ] ), call( check_names[ 1 ] ) ] ) + +# # Verify all checks were called +# assert mock_check_action.call_count == 2 + +# # Verify result contains all check results +# for name in check_names: +# assert name in result.check_results +# assert result.check_results[ name ] == { "status": "success" } From 8a280753499f906c263e2b8a48cfc663d96d7220 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 20 Jun 2025 17:05:08 -0700 Subject: [PATCH 19/20] Improve message output from certain checks + add description of parameters used when doing checks --- .../mesh/doctor/actions/element_volumes.py | 2 +- .../parsing/_shared_checks_parsing_logic.py | 18 +++++++++++++++++- .../doctor/parsing/collocated_nodes_parsing.py | 12 ++++++------ .../doctor/parsing/element_volumes_parsing.py | 9 +++++++-- .../doctor/parsing/non_conformal_parsing.py | 7 ++++--- .../self_intersecting_elements_parsing.py | 2 ++ .../parsing/supported_elements_parsing.py | 2 ++ 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py index 0bf5859b..e5380c3c 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py @@ -62,7 +62,7 @@ def __action( mesh, options: Options ) -> Result: v, q = pack vol = q if mesh.GetCellType( i ) in SUPPORTED_TYPES else v if vol < options.min_volume: - small_volumes.append( ( i, vol ) ) + small_volumes.append( ( i, float( vol ) ) ) return Result( element_volumes=small_volumes ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py b/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py index 1d6dc739..a2aa6538 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/_shared_checks_parsing_logic.py @@ -36,6 +36,22 @@ def _generate_parameters_help( ordered_check_names: list[ str ], check_features_ return help_text +def get_options_used_message( options_used: dataclass ) -> str: + """Dynamically generates the description of every parameter used when loaching a check. + + Args: + options_used (dataclass) + + Returns: + str: A message like "Parameters used: ( param1:value1 param2:value2 )" for as many paramters found. + """ + options_msg: str = "Parameters used: (" + for attr_name in options_used.__dataclass_fields__: + attr_value = getattr( options_used, attr_name ) + options_msg += f" {attr_name} = {attr_value}" + return options_msg + " )." + + # --- Generic Argument Parser Setup --- def fill_subparser( subparsers: argparse._SubParsersAction, subparser_name: str, help_message: str, ordered_check_names: list[ str ], check_features_config: dict[ str, CheckFeature ] ) -> None: @@ -151,7 +167,7 @@ def display_results( options: AllChecksOptions, result: AllChecksResult ) -> Non setup_logger.results( "No checks were performed or all failed during configuration." ) return - max_length = max( len( name ) for name in options.checks_to_perform ) + max_length: int = max( len( name ) for name in options.checks_to_perform ) for name, res in result.check_results.items(): setup_logger.results( "" ) # Blank line for visibility setup_logger.results( f"******** {name:<{max_length}} ********" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py index 2f9b7baf..ac93feb8 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/collocated_nodes_parsing.py @@ -1,5 +1,6 @@ from geos.mesh.doctor.actions.collocated_nodes import Options, Result from geos.mesh.doctor.parsing import COLLOCATES_NODES +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import get_options_used_message from geos.mesh.doctor.parsing.cli_parsing import setup_logger __TOLERANCE = "tolerance" @@ -23,22 +24,21 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): + setup_logger.results( get_options_used_message( options ) ) all_collocated_nodes: list[ int ] = [] for bucket in result.nodes_buckets: for node in bucket: all_collocated_nodes.append( node ) all_collocated_nodes: frozenset[ int ] = frozenset( all_collocated_nodes ) # Surely useless if all_collocated_nodes: - setup_logger.results( - f"You have {len(all_collocated_nodes)} collocated nodes (tolerance = {options.tolerance})." ) - - setup_logger.info( "Here are all the buckets of collocated nodes." ) + setup_logger.results( f"You have {len( all_collocated_nodes )} collocated nodes." ) + setup_logger.results( "Here are all the buckets of collocated nodes." ) tmp: list[ str ] = [] for bucket in result.nodes_buckets: tmp.append( f"({', '.join(map(str, bucket))})" ) - setup_logger.info( f"({', '.join(tmp)})" ) + setup_logger.results( f"({', '.join(tmp)})" ) else: - setup_logger.results( f"You have no collocated node (tolerance = {options.tolerance})." ) + setup_logger.results( "You have no collocated node." ) if result.wrong_support_elements: tmp: str = ", ".join( map( str, result.wrong_support_elements ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py index 959ae093..23315785 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/element_volumes_parsing.py @@ -1,5 +1,6 @@ from geos.mesh.doctor.actions.element_volumes import Options, Result from geos.mesh.doctor.parsing import ELEMENT_VOLUMES +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import get_options_used_message from geos.mesh.doctor.parsing.cli_parsing import setup_logger __MIN_VOLUME = "min_volume" @@ -29,8 +30,12 @@ def convert( parsed_options ) -> Options: def display_results( options: Options, result: Result ): + setup_logger.results( get_options_used_message( options ) ) setup_logger.results( f"You have {len(result.element_volumes)} elements with volumes smaller than {options.min_volume}." ) if result.element_volumes: - setup_logger.results( "The elements indices and their volumes are:\n\n".join( map( - str, result.element_volumes ) ) ) + setup_logger.results( "Elements index | Volumes calculated" ) + setup_logger.results( "-----------------------------------" ) + max_length: int = len( "Elements index " ) + for ( ind, volume ) in result.element_volumes: + setup_logger.results( f"{ind:<{max_length}}" + "| " + str( volume ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py index 6e63f1de..801e04f4 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/non_conformal_parsing.py @@ -1,5 +1,6 @@ from geos.mesh.doctor.actions.non_conformal import Options, Result from geos.mesh.doctor.parsing import NON_CONFORMAL +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import get_options_used_message from geos.mesh.doctor.parsing.cli_parsing import setup_logger __ANGLE_TOLERANCE = "angle_tolerance" @@ -45,10 +46,10 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): + setup_logger.results( get_options_used_message( options ) ) non_conformal_cells: list[ int ] = [] for i, j in result.non_conformal_cells: non_conformal_cells += i, j non_conformal_cells: frozenset[ int ] = frozenset( non_conformal_cells ) - setup_logger.results( - f"You have {len(non_conformal_cells)} non conformal cells.\n{', '.join(map(str, sorted(non_conformal_cells)))}" - ) + setup_logger.results( f"You have {len( non_conformal_cells )} non conformal cells." ) + setup_logger.results( f"{', '.join( map( str, sorted( non_conformal_cells ) ) )}" ) diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py index 558293c0..430a2532 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/self_intersecting_elements_parsing.py @@ -1,6 +1,7 @@ import numpy from geos.mesh.doctor.actions.self_intersecting_elements import Options, Result from geos.mesh.doctor.parsing import SELF_INTERSECTING_ELEMENTS +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import get_options_used_message from geos.mesh.doctor.parsing.cli_parsing import setup_logger __MIN_DISTANCE = "min_distance" @@ -36,6 +37,7 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): + setup_logger.results( get_options_used_message( options ) ) setup_logger.results( f"You have {len(result.intersecting_faces_elements)} elements with self intersecting faces." ) if result.intersecting_faces_elements: setup_logger.results( "The elements indices are:\n" + diff --git a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py index 7142f176..f9f8dd84 100644 --- a/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py +++ b/geos-mesh/src/geos/mesh/doctor/parsing/supported_elements_parsing.py @@ -1,6 +1,7 @@ import multiprocessing from geos.mesh.doctor.actions.supported_elements import Options, Result from geos.mesh.doctor.parsing import SUPPORTED_ELEMENTS +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import get_options_used_message from geos.mesh.doctor.parsing.cli_parsing import setup_logger __CHUNK_SIZE = "chunk_size" @@ -36,6 +37,7 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): + setup_logger.results( get_options_used_message( options ) ) if result.unsupported_polyhedron_elements: setup_logger.results( f"There is/are {len(result.unsupported_polyhedron_elements)} polyhedra that may not be converted to supported elements." From f45246a3f3052dc214f76d7b11f74ef14d8e611e Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 23 Jun 2025 15:34:23 -0700 Subject: [PATCH 20/20] Update test file --- geos-mesh/tests/test_all_checks.py | 168 ----------------- .../tests/test_shared_checks_parsing_logic.py | 172 ++++++++++++++++++ 2 files changed, 172 insertions(+), 168 deletions(-) delete mode 100644 geos-mesh/tests/test_all_checks.py create mode 100644 geos-mesh/tests/test_shared_checks_parsing_logic.py diff --git a/geos-mesh/tests/test_all_checks.py b/geos-mesh/tests/test_all_checks.py deleted file mode 100644 index ddf24f89..00000000 --- a/geos-mesh/tests/test_all_checks.py +++ /dev/null @@ -1,168 +0,0 @@ -# TODO Reimplement the tests -# import pytest -# import argparse -# from unittest.mock import patch, MagicMock, call -# from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions -# from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult -# from geos.mesh.doctor.actions.all_checks import action -# from geos.mesh.doctor.parsing.all_checks_parsing import convert, fill_subparser, display_results -# from geos.mesh.doctor.parsing.all_checks_parsing import DEFAULT_CHECK_NAMES, ORDERED_CHECK_NAMES, CHECK_FEATURES_CONFIG - - -# # Mock data and fixtures -# @pytest.fixture -# def mock_parser(): -# parser = argparse.ArgumentParser() -# subparsers = parser.add_subparsers( dest="action" ) -# return parser, subparsers - - -# @pytest.fixture -# def mock_check_action(): -# return MagicMock( return_value={ "status": "success" } ) - - -# @pytest.fixture -# def mock_args(): -# return { -# "checks_to_perform": "collocated_nodes, element_volumes", -# "set_parameters": "tolerance:1.0, min_volume:0.5" -# } - - -# # Tests for all_checks_parsing.py -# class TestAllChecksParsing: - -# def test_fill_subparser( self, mock_parser ): -# parser, subparsers = mock_parser -# fill_subparser( subparsers ) - -# # Verify subparser was created -# subparsers_actions = [ -# action for action in parser._subparsers._actions if isinstance( action, argparse._SubParsersAction ) -# ] -# assert len( subparsers_actions ) == 1 - -# # Check if our subparser is in the choices -# subparser_choices = subparsers_actions[ 0 ].choices -# assert "all_checks" in subparser_choices # assuming ALL_CHECKS is "all_checks" - -# def test_convert_with_default_checks( self ): -# # Test with empty string for checks_to_perform (should use all checks) -# args = { "checks_to_perform": "", "set_parameters": "" } -# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: -# options = convert( args ) - -# # Should log that all checks will be performed -# mock_logger.info.assert_any_call( "All current available checks in mesh-doctor will be performed." ) - -# # Should include all checks -# assert options.checks_to_perform == DEFAULT_CHECK_NAMES - -# # Should use default parameters -# for check_name in DEFAULT_CHECK_NAMES: -# assert check_name in options.checks_options - -# def test_convert_with_specific_checks( self, mock_args ): -# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): -# options = convert( mock_args ) - -# # Should only include the specified checks -# expected_checks = [ "collocated_nodes", "element_volumes" ] -# assert options.checks_to_perform == expected_checks - -# # Should only have options for specified checks -# assert set( options.checks_options.keys() ) == set( expected_checks ) - -# def test_convert_with_invalid_check( self ): -# args = { "checks_to_perform": "invalid_check_name", "set_parameters": "" } -# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ) as mock_logger: -# with pytest.raises( ValueError, match="No valid checks selected" ): -# convert( args ) - -# # Should log warning about invalid check -# mock_logger.warning.assert_called() - -# def test_convert_with_parameter_override( self ): -# # Choose a check and parameter that exists in DEFAULT_PARAMS -# check_name = "collocated_nodes" -# param_name = next( iter( CHECK_FEATURES_CONFIG[ check_name ].default_params.keys() ) ) -# args = { "checks_to_perform": check_name, "set_parameters": f"{param_name}:99.9" } -# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): -# options = convert( args ) - -# # Get the options object for the check -# check_options = options.checks_options[ check_name ] - -# # Verify the parameter was overridden -# # This assumes the parameter is accessible as an attribute of the options object -# # May need adjustment based on your actual implementation -# assert getattr( check_options, param_name, None ) == 99.9 - -# def test_display_results( self ): -# # Create mock options and results -# mock_display_func = MagicMock() -# check_name = ORDERED_CHECK_NAMES[ 0 ] -# options = AllChecksOptions( checks_to_perform=[ check_name ], -# checks_options={ check_name: "mock_options" }, -# check_displays={ check_name: mock_display_func } ) -# result = AllChecksResult( check_results={ check_name: "mock_result" } ) -# with patch( 'geos.mesh.doctor.parsing.all_checks_parsing.setup_logger' ): -# display_results( options, result ) - -# # Verify display function was called with correct arguments -# mock_display_func.assert_called_once_with( "mock_options", "mock_result" ) - - -# # Tests for all_checks.py -# class TestAllChecks: - -# def test_action_calls_check_modules( self, mock_check_action ): -# # Setup mock options -# check_name = ORDERED_CHECK_NAMES[ 0 ] -# mock_options = AllChecksOptions( checks_to_perform=[ check_name ], -# checks_options={ check_name: "mock_options" }, -# check_displays={ check_name: MagicMock() } ) -# # Mock the module loading function -# with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', -# return_value=mock_check_action ) as mock_load: -# with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): -# result = action( "test_file.vtk", mock_options ) - -# # Verify the module was loaded -# mock_load.assert_called_once_with( check_name ) - -# # Verify the check action was called with correct args -# mock_check_action.assert_called_once_with( "test_file.vtk", "mock_options" ) - -# # Verify result contains the check result -# assert check_name in result.check_results -# assert result.check_results[ check_name ] == { "status": "success" } - -# def test_action_with_multiple_checks( self, mock_check_action ): -# # Setup mock options with multiple checks -# check_names = [ ORDERED_CHECK_NAMES[ 0 ], ORDERED_CHECK_NAMES[ 1 ] ] -# mock_options = AllChecksOptions( checks_to_perform=check_names, -# checks_options={ -# name: f"mock_options_{i}" -# for i, name in enumerate( check_names ) -# }, -# check_displays={ name: MagicMock() -# for name in check_names } ) -# # Mock the module loading function -# with patch( 'geos.mesh.doctor.actions.all_checks.__load_module_action', -# return_value=mock_check_action ) as mock_load: -# with patch( 'geos.mesh.doctor.actions.all_checks.setup_logger' ): -# result = action( "test_file.vtk", mock_options ) - -# # Verify the modules were loaded -# assert mock_load.call_count == 2 -# mock_load.assert_has_calls( [ call( check_names[ 0 ] ), call( check_names[ 1 ] ) ] ) - -# # Verify all checks were called -# assert mock_check_action.call_count == 2 - -# # Verify result contains all check results -# for name in check_names: -# assert name in result.check_results -# assert result.check_results[ name ] == { "status": "success" } diff --git a/geos-mesh/tests/test_shared_checks_parsing_logic.py b/geos-mesh/tests/test_shared_checks_parsing_logic.py new file mode 100644 index 00000000..f02c697f --- /dev/null +++ b/geos-mesh/tests/test_shared_checks_parsing_logic.py @@ -0,0 +1,172 @@ +import argparse +from dataclasses import dataclass +import pytest +from unittest.mock import patch +# Import the module to test +from geos.mesh.doctor.actions.all_checks import Options as AllChecksOptions +from geos.mesh.doctor.actions.all_checks import Result as AllChecksResult +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import ( CheckFeature, _generate_parameters_help, + get_options_used_message, fill_subparser, convert, + display_results, CHECKS_TO_DO_ARG, PARAMETERS_ARG ) + + +# Mock dataclasses and functions we depend on +@dataclass +class MockOptions: + param1: float = 1.0 + param2: float = 2.0 + + +@dataclass +class MockResult: + value: str = "test_result" + + +def mock_display_func( options, result ): + pass + + +@pytest.fixture +def check_features_config(): + return { + "check1": + CheckFeature( name="check1", + options_cls=MockOptions, + result_cls=MockResult, + default_params={ + "param1": 1.0, + "param2": 2.0 + }, + display=mock_display_func ), + "check2": + CheckFeature( name="check2", + options_cls=MockOptions, + result_cls=MockResult, + default_params={ + "param1": 3.0, + "param2": 4.0 + }, + display=mock_display_func ) + } + + +@pytest.fixture +def ordered_check_names(): + return [ "check1", "check2" ] + + +def test_generate_parameters_help( check_features_config, ordered_check_names ): + help_text = _generate_parameters_help( ordered_check_names, check_features_config ) + assert "For check1: param1:1.0, param2:2.0" in help_text + assert "For check2: param1:3.0, param2:4.0" in help_text + + +def test_get_options_used_message(): + options = MockOptions( param1=10.0, param2=20.0 ) + message = get_options_used_message( options ) + assert "Parameters used: (" in message + assert "param1 = 10.0" in message + assert "param2 = 20.0" in message + assert ")." in message + + +def test_fill_subparser( check_features_config, ordered_check_names ): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers( dest="command" ) + fill_subparser( subparsers, "test-command", "Test help message", ordered_check_names, check_features_config ) + # Parse with no args should use defaults + args = parser.parse_args( [ "test-command" ] ) + assert getattr( args, CHECKS_TO_DO_ARG ) == "" + assert getattr( args, PARAMETERS_ARG ) == "" + # Parse with specified args + args = parser.parse_args( + [ "test-command", f"--{CHECKS_TO_DO_ARG}", "check1", f"--{PARAMETERS_ARG}", "param1:10.5" ] ) + assert getattr( args, CHECKS_TO_DO_ARG ) == "check1" + assert getattr( args, PARAMETERS_ARG ) == "param1:10.5" + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_default_checks( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "", PARAMETERS_ARG: "" } + options = convert( parsed_args, ordered_check_names, check_features_config ) + assert options.checks_to_perform == ordered_check_names + assert len( options.checks_options ) == 2 + assert options.checks_options[ "check1" ].param1 == 1.0 + assert options.checks_options[ "check2" ].param2 == 4.0 + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_specific_checks( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "check1", PARAMETERS_ARG: "" } + options = convert( parsed_args, ordered_check_names, check_features_config ) + assert options.checks_to_perform == [ "check1" ] + assert len( options.checks_options ) == 1 + assert "check1" in options.checks_options + assert "check2" not in options.checks_options + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_with_parameters( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "", PARAMETERS_ARG: "param1:10.5,param2:20.5" } + options = convert( parsed_args, ordered_check_names, check_features_config ) + assert options.checks_to_perform == ordered_check_names + assert options.checks_options[ "check1" ].param1 == 10.5 + assert options.checks_options[ "check1" ].param2 == 20.5 + assert options.checks_options[ "check2" ].param1 == 10.5 + assert options.checks_options[ "check2" ].param2 == 20.5 + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_with_invalid_parameters( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "", PARAMETERS_ARG: "param1:invalid,param2:20.5" } + options = convert( parsed_args, ordered_check_names, check_features_config ) + # The invalid parameter should be skipped, but the valid one applied + assert options.checks_options[ "check1" ].param1 == 1.0 # Default maintained + assert options.checks_options[ "check1" ].param2 == 20.5 # Updated + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_with_invalid_check( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "invalid_check,check1", PARAMETERS_ARG: "" } + options = convert( parsed_args, ordered_check_names, check_features_config ) + # The invalid check should be skipped + assert options.checks_to_perform == [ "check1" ] + assert "check1" in options.checks_options + assert "invalid_check" not in options.checks_options + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_convert_with_all_invalid_checks( mock_logger, check_features_config, ordered_check_names ): + parsed_args = { CHECKS_TO_DO_ARG: "invalid_check1,invalid_check2", PARAMETERS_ARG: "" } + # Should raise ValueError since no valid checks were selected + with pytest.raises( ValueError, match="No valid checks were selected" ): + convert( parsed_args, ordered_check_names, check_features_config ) + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_display_results_with_checks( mock_logger, check_features_config, ordered_check_names ): + options = AllChecksOptions( checks_to_perform=[ "check1", "check2" ], + checks_options={ + "check1": MockOptions(), + "check2": MockOptions() + }, + check_displays={ + "check1": mock_display_func, + "check2": mock_display_func + } ) + result = AllChecksResult( check_results={ + "check1": MockResult( value="result1" ), + "check2": MockResult( value="result2" ) + } ) + display_results( options, result ) + # Check that results logger was called for each check + assert mock_logger.results.call_count >= 2 + + +@patch( 'geos.mesh.doctor.parsing._shared_checks_parsing_logic.setup_logger' ) +def test_display_results_no_checks( mock_logger ): + options = AllChecksOptions( checks_to_perform=[], checks_options={}, check_displays={} ) + result = AllChecksResult( check_results={} ) + display_results( options, result ) + # Should display a message that no checks were performed + mock_logger.results.assert_called_with( "No checks were performed or all failed during configuration." )